Created
October 7, 2017 21:47
-
-
Save amosavian/bf5c9f106dc31551c06e2d201c3cb878 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // ZipFileProvider | |
| // ExtDownloader | |
| // | |
| // Created by Amir Abbas on 1/30/1396 AP. | |
| // Copyright © 1396 AP Mousavian. All rights reserved. | |
| // | |
| import Foundation | |
| import FilesProvider | |
| import UnrarKit | |
| // TODO: Progress | |
| class RarFileProvider: FileProvider { | |
| open class var type: String { return "RarCompressed" } | |
| open fileprivate(set) var baseURL: URL? | |
| open var currentPath: String | |
| open var dispatch_queue: DispatchQueue | |
| open var operation_queue: OperationQueue | |
| open weak var delegate: FileProviderDelegate? | |
| open var credential: URLCredential? { | |
| didSet { | |
| archive.password = credential?.password | |
| } | |
| } | |
| fileprivate var archive: URKArchive | |
| /// Initializes provider for the specified zip file in local URL. | |
| /// | |
| /// - Parameter baseURL: Local URL location for base directory. | |
| public init?(baseURL: URL) { | |
| guard baseURL.isFileURL || baseURL.scheme == "compressed" else { | |
| fatalError("Cannot initialize a Local provider from remote URL.") | |
| } | |
| guard let archive = try? URKArchive(path: baseURL.path) else { | |
| return nil | |
| } | |
| self.archive = archive | |
| let base = (baseURL.path.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")).absoluteURL | |
| var components = URLComponents(url: base, resolvingAgainstBaseURL: true) | |
| components?.scheme = "compressed" | |
| self.baseURL = components?.url ?? base | |
| self.currentPath = "" | |
| self.credential = nil | |
| dispatch_queue = DispatchQueue(label: "FileProvider.\(Swift.type(of: self).type)", attributes: .concurrent) | |
| operation_queue = OperationQueue() | |
| operation_queue.name = "FileProvider.\(Swift.type(of: self).type).Operation" | |
| } | |
| public required convenience init?(coder aDecoder: NSCoder) { | |
| guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else { | |
| return nil | |
| } | |
| self.init(baseURL: baseURL) | |
| self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? "" | |
| } | |
| open func encode(with aCoder: NSCoder) { | |
| aCoder.encode(self.baseURL, forKey: "baseURL") | |
| aCoder.encode(self.currentPath, forKey: "currentPath") | |
| } | |
| public static var supportsSecureCoding: Bool { | |
| return true | |
| } | |
| public func copy(with zone: NSZone? = nil) -> Any { | |
| let copy = RarFileProvider(baseURL: self.baseURL!)! | |
| copy.currentPath = self.currentPath | |
| copy.delegate = self.delegate | |
| copy.fileOperationDelegate = self.fileOperationDelegate | |
| return copy | |
| } | |
| open func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) { | |
| dispatch_queue.async { | |
| do { | |
| let barePath = path.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")) | |
| let list = try self.archive.listFileInfo() | |
| let contents = list.filter({ $0.filename.deletingLastPathComponent == barePath }).map { RarFileObject(baseURL: self.baseURL!, info: $0) } | |
| completionHandler(contents, nil) | |
| } catch { | |
| completionHandler([], error) | |
| } | |
| } | |
| } | |
| open func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) { | |
| let barePath = path.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")) | |
| let parentPath = path.deletingLastPathComponent.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")) | |
| self.contentsOfDirectory(path: parentPath) { (contents, error) in | |
| let file = contents.first(where: { $0.path == barePath }) | |
| completionHandler(file, error) | |
| } | |
| } | |
| open func storageProperties(completionHandler: (@escaping (_ volume: VolumeObject?) -> Void)) { | |
| dispatch_queue.async { | |
| do { | |
| let list = try self.archive.listFileInfo() | |
| let used: Int64 = list.reduce(0) { $0 + $1.uncompressedSize } | |
| var allValues: [URLResourceKey: Any] = [.volumeAvailableCapacityKey: -used] | |
| allValues[.volumeURLKey] = self.archive.fileURL | |
| allValues[.volumeNameKey] = self.archive.fileURL?.lastPathComponent | |
| allValues[.volumeIsReadOnlyKey] = true | |
| let volume = VolumeObject(allValues: allValues) | |
| completionHandler(volume) | |
| } catch { | |
| completionHandler(nil) | |
| } | |
| } | |
| } | |
| open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { | |
| dispatch_queue.async { | |
| do { | |
| let list = try self.archive.listFileInfo() | |
| let contents = list.map({ RarFileObject(baseURL: self.baseURL!, info: $0) }).filter { query.evaluate(with: $0.mapPredicate()) } | |
| completionHandler(contents, nil) | |
| } catch { | |
| completionHandler([], error) | |
| } | |
| } | |
| return nil | |
| } | |
| open func isReachable(completionHandler: @escaping (Bool) -> Void) { | |
| dispatch_queue.async { | |
| completionHandler(self.archive.filename != nil && self.archive.validatePassword()) | |
| } | |
| } | |
| open func isPasswordProtected() -> Bool { | |
| return self.archive.isPasswordProtected() && !self.archive.validatePassword() | |
| } | |
| open weak var fileOperationDelegate : FileOperationDelegate? | |
| @discardableResult | |
| open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? { | |
| let operation = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/") | |
| return self.doOperation(operation, completionHandler: completionHandler) | |
| } | |
| @discardableResult | |
| open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { | |
| let operation = FileOperationType.move(source: path, destination: toPath) | |
| if !overwrite && FileManager.default.fileExists(atPath: self.url(of: toPath).path) { | |
| let errorpath = self.baseURL!.lastPathComponent.appendingPathComponent(toPath) | |
| let error = CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: errorpath]) | |
| completionHandler?(error) | |
| return nil | |
| } | |
| return self.doOperation(operation, overwrite: overwrite, completionHandler: completionHandler) | |
| } | |
| @discardableResult | |
| open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { | |
| let operation = FileOperationType.copy(source: path, destination: toPath) | |
| if !overwrite && FileManager.default.fileExists(atPath: self.url(of: toPath).path) { | |
| self.dispatch_queue.async { | |
| let errorpath = self.baseURL!.lastPathComponent.appendingPathComponent(toPath) | |
| let error = CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: errorpath]) | |
| completionHandler?(error) | |
| } | |
| return nil | |
| } | |
| return self.doOperation(operation, overwrite: overwrite, completionHandler: completionHandler) | |
| } | |
| @discardableResult | |
| open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? { | |
| let operation = FileOperationType.remove(path: path) | |
| return self.doOperation(operation, completionHandler: completionHandler) | |
| } | |
| @discardableResult | |
| open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { | |
| let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath) | |
| return self.doOperation(operation, overwrite: overwrite, completionHandler: completionHandler) | |
| } | |
| @discardableResult | |
| open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? { | |
| let operation = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString) | |
| return self.doOperation(operation, completionHandler: completionHandler) | |
| } | |
| @discardableResult | |
| fileprivate func doOperation(_ operation: FileOperationType, data: Data? = nil, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { | |
| func askPassword(file: URKFileInfo) { | |
| if file.isEncryptedWithPassword { | |
| let group = DispatchGroup() | |
| let msg = "Please enter password to access \(self.type)" | |
| let okBtn = Utility.UI.AlertButton(title: NSLocalizedString("OK", comment: "button"), buttonType: Utility.UI.ButtonType.default, buttonHandler: { fields in | |
| let credential = URLCredential(user: "anonymous", password: fields[0], persistence: .forSession) | |
| self.credential = credential | |
| group.leave() | |
| }) | |
| let cancel = Utility.UI.AlertButton(title: NSLocalizedString("Cancel", comment: "button"), buttonType: .cancel, buttonHandler: { _ in | |
| group.leave() | |
| }) | |
| let passBox = Utility.UI.AlertTextField(placeHolder: "password", defaultValue: "", textInputTraits: TextInputTraits()) | |
| group.enter() | |
| DispatchQueue.main.async { | |
| Utility.UI.askAlert(msg, withTitle: NSLocalizedString("\(self.type) Credential", comment: "Remote credential"), viewController: UIApplication.shared.mn_topViewController()!, buttons: [okBtn, cancel], textFields: [passBox]) | |
| } | |
| group.wait() | |
| } | |
| } | |
| let pathTrim = CharacterSet(charactersIn: "/") | |
| operation_queue.addOperation { | |
| do { | |
| switch operation { | |
| case .create(path: let path), .modify(path: let path), .remove(path: let path): | |
| let errorpath = self.baseURL!.lastPathComponent.appendingPathComponent(path) | |
| let error = CocoaError(.fileWriteVolumeReadOnly, userInfo: [NSFilePathErrorKey: errorpath]) | |
| throw error | |
| case .copy(source: let source, destination: let dest): | |
| if dest.hasPrefix("file://"), let destURL = URL(string: dest) { | |
| let files = try self.archive.listFileInfo().filter { $0.filename.hasPrefix(source.trimmingCharacters(in: pathTrim)) } | |
| for (i, file) in files.enumerated() { | |
| askPassword(file: file) | |
| let finalRelPath = file.filename.replacingOccurrences(of: source.trimmingCharacters(in: pathTrim), with: "", options: .anchored).trimmingCharacters(in: pathTrim) | |
| let finalDestURL = destURL.appendingPathComponent(finalRelPath) | |
| if file.isDirectory { | |
| try FileManager.default.createDirectory(atPath: finalDestURL.path, withIntermediateDirectories: true, attributes: [.modificationDate: file.timestamp]) | |
| } else { | |
| let data = try self.archive.extractData(fromFile: file.filename, progress: { progress in | |
| self.delegateNotify(operation, progress: (Double(i) + Double(progress)) / Double(files.count)) | |
| }) | |
| try data.write(to: finalDestURL) | |
| try? FileManager.default.setAttributes([FileAttributeKey.modificationDate:file.timestamp], ofItemAtPath: finalDestURL.path) | |
| } | |
| } | |
| } else { | |
| let errorpath = self.baseURL!.lastPathComponent.appendingPathComponent(dest) | |
| let error = CocoaError(.fileWriteVolumeReadOnly, userInfo: [NSFilePathErrorKey: errorpath]) | |
| throw error | |
| } | |
| case .move(source: _, destination: let dest): | |
| let errorpath = self.baseURL!.lastPathComponent.appendingPathComponent(dest) | |
| let error = CocoaError(.fileWriteVolumeReadOnly, userInfo: [NSFilePathErrorKey: errorpath]) | |
| throw error | |
| default: | |
| return | |
| } | |
| completionHandler?(nil) | |
| self.delegateNotify(operation) | |
| } catch { | |
| completionHandler?(error) | |
| self.delegateNotify(operation, error: error) | |
| } | |
| } | |
| return nil | |
| } | |
| @discardableResult | |
| open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { | |
| let operation = FileOperationType.fetch(path: path) | |
| let barePath = path.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")) | |
| dispatch_queue.async { | |
| do { | |
| let data = try self.archive.extractData(fromFile: barePath, progress: { (progress) in | |
| self.delegateNotify(operation, progress: Double(progress)) | |
| }) | |
| completionHandler(data, nil) | |
| self.delegateNotify(operation) | |
| } catch { | |
| completionHandler(nil, error) | |
| self.delegateNotify(operation, error: error) | |
| } | |
| } | |
| return nil | |
| } | |
| @discardableResult | |
| open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { | |
| return self.contents(path: path) { (data, error) in | |
| let range: Range<Int> = Int(offset)..<(Int(offset) + length) | |
| completionHandler(data?.subdata(in: range), error) | |
| } | |
| } | |
| @discardableResult | |
| open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { | |
| let operation: FileOperationType = .modify(path: path) | |
| return self.doOperation(operation, data: data ?? Data(), overwrite: overwrite, completionHandler: completionHandler) | |
| } | |
| fileprivate func delegateNotify(_ operation: FileOperationType, error: Error? = nil) { | |
| DispatchQueue.main.async(execute: { | |
| if let error = error { | |
| self.delegate?.fileproviderFailed(self, operation: operation, error: error) | |
| } else { | |
| self.delegate?.fileproviderSucceed(self, operation: operation) | |
| } | |
| }) | |
| } | |
| fileprivate func delegateNotify(_ operation: FileOperationType, progress: Double) { | |
| DispatchQueue.main.async(execute: { | |
| self.delegate?.fileproviderProgress(self, operation: operation, progress: Float(progress)) | |
| }) | |
| } | |
| } | |
| class RarFileObject: FileObject { | |
| init(baseURL: URL, info: URKFileInfo) { | |
| var allValues = [URLResourceKey: Any]() | |
| allValues = [URLResourceKey: Any]() | |
| var path = info.filename.trimmingCharacters(in: CharacterSet(charactersIn: "/")) | |
| if info.isDirectory { | |
| path.append("/") | |
| } | |
| allValues[.fileURLKey] = URL(string: path, relativeTo: baseURL) | |
| allValues[.nameKey] = info.filename.lastPathComponent | |
| allValues[.pathKey] = path | |
| allValues[.fileSizeKey] = Int64(info.uncompressedSize) | |
| allValues[.fileResourceTypeKey] = info.isDirectory ? URLFileResourceType.directory : URLFileResourceType.regular | |
| allValues[.documentIdentifierKey] = info.crc | |
| allValues[.fileAllocatedSizeKey] = Int64(info.compressedSize) | |
| allValues[.isEncryptedKey] = info.isEncryptedWithPassword | |
| super.init(allValues: allValues) | |
| } | |
| var compressedSize: Int64? { | |
| get { | |
| return allValues[.fileAllocatedSizeKey] as? Int64 | |
| } | |
| } | |
| var isEncrypted: Bool { | |
| get { | |
| return allValues[.isEncryptedKey] as? Bool ?? false | |
| } | |
| } | |
| var crc: UInt? { | |
| get { | |
| return allValues[.documentIdentifierKey] as? UInt | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment