Created
October 7, 2017 21:47
-
-
Save amosavian/f59138c4c876609ca4044485d8f349e2 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 UnzipKit | |
| // TODO: Progress | |
| class ZipFileProvider: FileProvider { | |
| open class var type: String { return "Compressed" } | |
| 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: UZKArchive | |
| /// 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? UZKArchive(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 = ZipFileProvider(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 { CompressedFileObject(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: UInt64 = list.reduce(0) { $0 + $1.uncompressedSize } | |
| var allValues: [URLResourceKey: Any] = [.volumeAvailableCapacityKey: -Int64(used)] | |
| allValues[.volumeURLKey] = self.archive.fileURL | |
| allValues[.volumeNameKey] = self.archive.fileURL?.lastPathComponent | |
| 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({ CompressedFileObject(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: UZKFileInfo) { | |
| 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): | |
| var barePath = path.trimmingCharacters(in: pathTrim) | |
| if path.hasSuffix("/") { | |
| barePath.append("/") | |
| } | |
| try self.archive.write(data ?? Data(), filePath: barePath, fileDate: Date(), compressionMethod: .none, password: self.archive.password, overwrite: overwrite, progress: { progress in | |
| self.delegateNotify(operation, progress: Double(progress)) | |
| }) | |
| case .copy(source: let source, destination: let dest), .move(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 if source.hasPrefix("file://"), let sourceURL = URL(string: source) { | |
| let files: [URL] = [sourceURL].flatMap({ (url) -> [URL] in | |
| if url.fileIsDirectory { | |
| let subpaths: [String] = (try? FileManager.default.subpathsOfDirectory(atPath: url.path)) ?? [] | |
| let urlSubpaths: [URL] = subpaths.map({ | |
| let path = url.path.appendingPathComponent($0) | |
| return URL(fileURLWithPath: path) | |
| }) | |
| return [url] + urlSubpaths | |
| } else { | |
| return [url] | |
| } | |
| }) | |
| for (i, file) in files.enumerated() { | |
| let isDir = file.fileIsDirectory | |
| let data = isDir ? Data() : try Data(contentsOf: file) | |
| let date = source.fileInfo.modificationDate | |
| let relativePath = file.path.replacingOccurrences(of: sourceURL.deletingLastPathComponent().path, with: "", options: .anchored).trimmingCharacters(in: pathTrim) | |
| let destPath = isDir ? relativePath + "/" : relativePath | |
| try self.archive.write(data, filePath: destPath, fileDate: date, compressionMethod: .default, password: self.archive.password, overwrite: overwrite, progress: { progress in | |
| let fraction = (Double(i) + Double(progress)) / Double(files.count) | |
| self.delegateNotify(operation, progress: fraction) | |
| }) | |
| } | |
| } else { | |
| let bareSource = source.trimmingCharacters(in: pathTrim) | |
| let files = try self.archive.listFileInfo().filter { $0.filename.hasPrefix(source.trimmingCharacters(in: pathTrim)) } | |
| for (i, file) in files.enumerated() { | |
| let relativePath = file.filename.replacingOccurrences(of: bareSource, with: "", options: .anchored).trimmingCharacters(in: pathTrim) | |
| var finalPath = dest + relativePath | |
| let data: Data | |
| if file.isDirectory { | |
| finalPath.append("/") | |
| data = Data() | |
| } else { | |
| askPassword(file: file) | |
| data = try self.archive.extractData(fromFile: file.filename, progress: { progress in | |
| let fraction = (Double(i) + Double(progress)) / Double(files.count * 2) | |
| self.delegateNotify(operation, progress: fraction) | |
| }) | |
| } | |
| try self.archive.write(data, filePath: finalPath, fileDate: file.timestamp, compressionMethod: .default, password: self.archive.password, overwrite: overwrite, progress: { progress in | |
| let fraction = (Double(files.count) + Double(i) + Double(progress)) / Double(files.count * 2) | |
| self.delegateNotify(operation, progress: fraction) | |
| }) | |
| } | |
| } | |
| if case .move = operation { | |
| try self.archive.deleteFile(source.trimmingCharacters(in: pathTrim)) | |
| } | |
| case .remove(path: let path): | |
| try self.archive.deleteFile(path.trimmingCharacters(in: pathTrim)) | |
| 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 CompressedFileObject: FileObject { | |
| init(baseURL: URL, info: UZKFileInfo) { | |
| 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 | |
| } | |
| } | |
| } | |
| extension FileObject { | |
| func mapPredicate() -> [String: Any] { | |
| let mapDict: [URLResourceKey: String] = [.fileURLKey: "url", .nameKey: "name", .pathKey: "path", .fileSizeKey: "filesize", .creationDateKey: "creationDate", | |
| .contentModificationDateKey: "modifiedDate", .isHiddenKey: "isHidden", .isWritableKey: "isWritable", .serverDateKey: "serverDate", .entryTagKey: "entryTag", .mimeTypeKey: "mimeType"] | |
| let typeDict: [URLFileResourceType: String] = [.directory: "directory", .regular: "regular", .symbolicLink: "symbolicLink", .unknown: "unknown"] | |
| var result = [String: Any]() | |
| for (key, value) in allValues { | |
| if let convertkey = mapDict[key] { | |
| result[convertkey] = value | |
| } | |
| } | |
| result["eTag"] = result["entryTag"] | |
| result["isReadOnly"] = self.isReadOnly | |
| result["isDirectory"] = self.isDirectory | |
| result["isRegularFile"] = self.isRegularFile | |
| result["isSymLink"] = self.isSymLink | |
| result["type"] = typeDict[self.type ] ?? "unknown" | |
| return result | |
| } | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment