From c8de7fdb69197b1f182adbe6f969fdd748ce7a2a Mon Sep 17 00:00:00 2001 From: Amir Abbas Date: Fri, 29 Sep 2017 18:11:26 +0330 Subject: [PATCH] Fixed NSProgress parenting, iCloud KVO, crash in url(of) --- Sources/CloudFileProvider.swift | 142 +++++++++++++++-------------- Sources/DropboxFileProvider.swift | 3 +- Sources/FTPFileProvider.swift | 5 +- Sources/FileProvider.swift | 2 +- Sources/HTTPFileProvider.swift | 4 +- Sources/LocalFileProvider.swift | 13 ++- Sources/OneDriveFileProvider.swift | 2 +- Sources/WebDAVFileProvider.swift | 2 +- 8 files changed, 90 insertions(+), 83 deletions(-) diff --git a/Sources/CloudFileProvider.swift b/Sources/CloudFileProvider.swift index c963e38..acf03b8 100644 --- a/Sources/CloudFileProvider.swift +++ b/Sources/CloudFileProvider.swift @@ -129,7 +129,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { let pathURL = self.url(of: path) let query = NSMetadataQuery() - query.predicate = NSPredicate(format: "%K BEGINSWITH %@", NSMetadataItemPathKey, pathURL.path) + query.predicate = NSPredicate(format: "%K BEGINSWITH[CD] %@", NSMetadataItemPathKey, pathURL.path) query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey] query.searchScopes = [self.scope.rawValue] var finishObserver: NSObjectProtocol? @@ -196,7 +196,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { dispatch_queue.async { let pathURL = self.url(of: path) let query = NSMetadataQuery() - query.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemPathKey, pathURL.path) + query.predicate = NSPredicate(format: "%K LIKE[CD] %@", NSMetadataItemPathKey, pathURL.path) query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey] query.searchScopes = [self.scope.rawValue] var finishObserver: NSObjectProtocol? @@ -282,7 +282,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { } } - let progress = Progress(parent: nil, userInfo: nil) + let progress = Progress(totalUnitCount: -1) dispatch_queue.async { let pathURL = self.url(of: path) @@ -303,10 +303,10 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { // FIXME: Remove this section as it won't work as expected on iCloud updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryGatheringProgress, object: mdquery, queue: nil, using: { (notification) in - mdquery.disableUpdates() - guard mdquery.resultCount > lastReportedCount else { return } + mdquery.disableUpdates() + for index in lastReportedCount.. Progress? { // TODO: Make use of overwrite parameter let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath) - let progress = Progress(parent: nil, userInfo: nil) + let progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) progress.kind = .file progress.isCancellable = false @@ -428,9 +428,11 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { let tmpFile = tempFolder.appendingPathComponent(UUID().uuidString) do { + progress.totalUnitCount = localFile.fileSize try self.opFileManager.copyItem(at: localFile, to: tmpFile) let toUrl = self.url(of: toPath) try self.opFileManager.setUbiquitous(true, itemAt: tmpFile, destinationURL: toUrl) + self.monitorFile(path: toPath, operation: operation, progress: progress) completionHandler?(nil) self.delegateNotify(operation) } catch { @@ -457,12 +459,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { @discardableResult open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? { let operation = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString) - let progress = Progress(parent: nil, userInfo: nil) - progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) - progress.kind = .file - progress.isCancellable = false - progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey) - progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + let progress = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler) monitorFile(path: path, operation: operation, progress: progress) do { try self.opFileManager.startDownloadingUbiquitousItem(at: self.url(of: path)) @@ -471,7 +468,6 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { self.delegateNotify(operation, error: error) return nil } - let _ = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler) return progress } @@ -489,13 +485,8 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { @discardableResult open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { let operation = FileOperationType.fetch(path: path) - let progress = Progress(parent: nil, userInfo: nil) - progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) - progress.kind = .file - progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey) - progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + let progress = super.contents(path: path, completionHandler: completionHandler) monitorFile(path: path, operation: operation, progress: progress) - _ = super.contents(path: path, completionHandler: completionHandler) return progress } @@ -515,13 +506,8 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { @discardableResult open override func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { let operation = FileOperationType.fetch(path: path) - let progress = Progress(parent: nil, userInfo: nil) - progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) - progress.kind = .file - progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey) - progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + let progress = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler) monitorFile(path: path, operation: operation, progress: progress) - _ = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler) return progress } @@ -539,7 +525,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { @discardableResult open override func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { let operation = FileOperationType.fetch(path: path) - let progress = Progress(parent: nil, userInfo: nil) + let progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) progress.kind = .file progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey) @@ -635,48 +621,20 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { return file } + lazy fileprivate var observer: KVOObserver = KVOObserver() + fileprivate func monitorFile(path: String, operation: FileOperationType, progress: Progress?) { - dispatch_queue.async { - let pathURL = self.url(of: path) - let size = pathURL.fileSize - progress?.totalUnitCount = size > 0 ? size : 0 - let query = NSMetadataQuery() - query.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemPathKey, pathURL.path) - query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataUbiquitousItemPercentDownloadedKey, NSMetadataUbiquitousItemPercentUploadedKey, NSMetadataItemFSSizeKey] - query.searchScopes = [self.scope.rawValue] - var updateObserver: NSObjectProtocol? - updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: nil, using: { (notification) in - query.disableUpdates() - - guard let item = (query.results as? [NSMetadataItem])?.first else { - return - } - - if progress?.totalUnitCount == 0, let size = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 { - progress?.totalUnitCount = size - } - let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0 - let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0 - if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) { - progress?.completedUnitCount = Int64(uploaded / 100 * Double(progress?.totalUnitCount ?? 0)) - self.delegateNotify(operation, progress: uploaded / 100) - } else if (uploaded == 0 || uploaded == 100) && (downloaded > 0 && downloaded < 100) { - progress?.completedUnitCount = Int64(downloaded / 100 * Double(progress?.totalUnitCount ?? 0)) - self.delegateNotify(operation, progress: downloaded / 100) - } else if uploaded == 100 || downloaded == 100 { - progress?.completedUnitCount = progress?.totalUnitCount ?? 0 - query.stop() - NotificationCenter.default.removeObserver(updateObserver!) - self.delegateNotify(operation) - } - - query.enableUpdates() - }) - - DispatchQueue.main.async { - progress?.setUserInfoObject(Date(), forKey: .startingTimeKey) - query.start() - } + let pathURL = self.url(of: path).standardizedFileURL + let query = NSMetadataQuery() + query.predicate = NSPredicate(format: "%K LIKE[CD] %@", NSMetadataItemPathKey, pathURL.path) + query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataUbiquitousItemPercentDownloadedKey, NSMetadataUbiquitousItemPercentUploadedKey, NSMetadataUbiquitousItemDownloadingStatusKey, NSMetadataItemFSSizeKey] + query.searchScopes = [self.scope.rawValue] + var context = QueryProgressWrapper(provider: self, progress: progress, operation: operation) + query.addObserver(self.observer, forKeyPath: "results", options: [.initial, .new, .old], context: &context) + + DispatchQueue.main.async { + query.start() + progress?.setUserInfoObject(Date(), forKey: .startingTimeKey) } } @@ -763,6 +721,56 @@ public enum UbiquitousScope: RawRepresentable { } } +struct QueryProgressWrapper { + weak var provider: CloudFileProvider? + weak var progress: Progress? + let operation: FileOperationType +} + +fileprivate class KVOObserver: NSObject { + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + guard let query = object as? NSMetadataQuery else { + return + } + guard let wrapper = context?.load(as: QueryProgressWrapper.self) else { + query.stop() + query.removeObserver(self, forKeyPath: "results") + return + } + let provider = wrapper.provider + let progress = wrapper.progress + let operation = wrapper.operation + + guard let results = change?[.newKey], let item = (results as? [NSMetadataItem])?.first else { + return + } + + query.disableUpdates() + var size = progress?.totalUnitCount ?? -1 + if size < 0, let size_d = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 { + size = size_d + progress?.totalUnitCount = size + } + let downloadStatus = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? String ?? "" + let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0 + let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0 + if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) { + progress?.completedUnitCount = Int64(uploaded / 100 * Double(size)) + provider?.delegateNotify(operation, progress: uploaded / 100) + } else if (uploaded == 0 || uploaded == 100) && downloadStatus != NSMetadataUbiquitousItemDownloadingStatusCurrent { + progress?.completedUnitCount = Int64(downloaded / 100 * Double(size)) + provider?.delegateNotify(operation, progress: downloaded / 100) + } else if uploaded == 100 || downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent { + progress?.completedUnitCount = size + query.stop() + query.removeObserver(self, forKeyPath: "results") + provider?.delegateNotify(operation) + } + + query.enableUpdates() + } +} + /* func getMetadataItem(url: URL) -> NSMetadataItem? { let query = NSMetadataQuery() diff --git a/Sources/DropboxFileProvider.swift b/Sources/DropboxFileProvider.swift index a545d8f..3c3cf95 100644 --- a/Sources/DropboxFileProvider.swift +++ b/Sources/DropboxFileProvider.swift @@ -58,6 +58,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { } open override func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) { + // We don't want this progress became in another progress' hierarchy let progress = Progress(parent: nil, userInfo: nil) list(path, progress: progress) { (contents, cursor, error) in completionHandler(contents, error) @@ -107,7 +108,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { } open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { - let progress = Progress(parent: nil, userInfo: nil) + let progress = Progress(totalUnitCount: -1) var foundFiles = [DropboxFileObject]() if let queryStr = query.findValue(forKey: "name", operator: .beginsWith) as? String { // Dropbox only support searching for file names begin with query in non-enterprise accounts. diff --git a/Sources/FTPFileProvider.swift b/Sources/FTPFileProvider.swift index a915d46..954d665 100644 --- a/Sources/FTPFileProvider.swift +++ b/Sources/FTPFileProvider.swift @@ -277,7 +277,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil } open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { - let progress = Progress(parent: nil, userInfo: nil) + let progress = Progress(totalUnitCount: -1) if recursive { return self.recursiveList(path: path, useMLST: true, foundItemsHandler: { items in if let foundItemHandler = foundItemHandler { @@ -859,8 +859,7 @@ extension FTPFileProvider { } progress.becomeCurrent(withPendingUnitCount: 1) - let recursiveProgress = Progress(parent: progress, userInfo: nil) - recursiveProgress.totalUnitCount = Int64(contents.count) + let recursiveProgress = Progress(totalUnitCount: Int64(contents.count)) let sortedContents = contents.sorted(by: { $0.path.localizedStandardCompare($1.path) == .orderedDescending }) diff --git a/Sources/FileProvider.swift b/Sources/FileProvider.swift index 25a2833..098be23 100644 --- a/Sources/FileProvider.swift +++ b/Sources/FileProvider.swift @@ -683,7 +683,7 @@ extension FileProviderBasic { } return URL(string: rpath, relativeTo: baseURL) ?? baseURL } else { - return URL(string: rpath)! + return URL(string: rpath) ?? URL(string: "/")! } } diff --git a/Sources/HTTPFileProvider.swift b/Sources/HTTPFileProvider.swift index 161e030..6ef51f4 100644 --- a/Sources/HTTPFileProvider.swift +++ b/Sources/HTTPFileProvider.swift @@ -307,7 +307,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi internal func upload_simple(_ targetPath: String, request: URLRequest, data: Data? = nil, localFile: URL? = nil, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? { let size = data?.count ?? Int((try? localFile?.resourceValues(forKeys: [.fileSizeKey]))??.fileSize ?? -1) - var progress = Progress(parent: nil, userInfo: nil) + var progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) progress.kind = .file progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) @@ -345,7 +345,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi } internal func download_simple(path: String, request: URLRequest, operation: FileOperationType, completionHandler: @escaping ((_ tempURL: URL?, _ error: Error?) -> Void)) -> Progress? { - var progress = Progress(parent: nil, userInfo: nil) + var progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) progress.kind = .file progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) diff --git a/Sources/LocalFileProvider.swift b/Sources/LocalFileProvider.swift index 0b36816..e316032 100644 --- a/Sources/LocalFileProvider.swift +++ b/Sources/LocalFileProvider.swift @@ -178,7 +178,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? { - let progress = Progress(parent: nil, userInfo: nil) + let progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey) dispatch_queue.async { @@ -293,7 +293,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo @discardableResult fileprivate func doOperation(_ operation: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { - let progress = Progress(parent: nil, userInfo: nil) + let progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) progress.kind = .file progress.isCancellable = false @@ -339,11 +339,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo progress.totalUnitCount = 1 try self.opFileManager.createDirectory(at: source, withIntermediateDirectories: true, attributes: [:]) } else { - progress.totalUnitCount = Int64(data?.count ?? 0) + progress.totalUnitCount = Int64(data?.count ?? -1) try data?.write(to: source, options: .atomic) } case .modify: - progress.totalUnitCount = Int64(data?.count ?? 0) + progress.totalUnitCount = Int64(data?.count ?? -1) try data?.write(to: source, options: atomically ? [.atomic] : []) case .copy: guard let dest = dest else { return } @@ -420,13 +420,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo let operation = FileOperationType.fetch(path: path) let url = self.url(of: path) - let progress = Progress(parent: nil, userInfo: nil) + let progress = Progress(totalUnitCount: url.fileSize) progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) progress.kind = .file progress.isCancellable = false progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey) progress.setUserInfoObject(url, forKey: .fileURLKey) - progress.totalUnitCount = url.fileSize let operationHandler: (URL) -> Void = { url in progress.setUserInfoObject(Date(), forKey: .startingTimeKey) @@ -479,7 +478,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo let operation = FileOperationType.fetch(path: path) let url = self.url(of: path) - let progress = Progress(parent: nil, userInfo: nil) + let progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) progress.kind = .file progress.isCancellable = false diff --git a/Sources/OneDriveFileProvider.swift b/Sources/OneDriveFileProvider.swift index dcbbe27..da19167 100644 --- a/Sources/OneDriveFileProvider.swift +++ b/Sources/OneDriveFileProvider.swift @@ -118,7 +118,7 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { var queryStr: String? queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first guard let finalQueryStr = queryStr else { return nil } - let progress = Progress(parent: nil, userInfo: nil) + let progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(url(of: path), forKey: .fileURLKey) search(path, query: finalQueryStr, recursive: recursive, progress: progress, foundItem: { (file) in if query.evaluate(with: file.mapPredicate()) { diff --git a/Sources/WebDAVFileProvider.swift b/Sources/WebDAVFileProvider.swift index 98b3107..1d651f4 100644 --- a/Sources/WebDAVFileProvider.swift +++ b/Sources/WebDAVFileProvider.swift @@ -184,7 +184,7 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { request.set(httpAuthentication: credential, with: credentialType) request.set(httpContentType: .xml, charset: .utf8) request.httpBody = WebDavFileObject.xmlProp([]) - let progress = Progress(parent: nil, userInfo: nil) + let progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(url, forKey: .fileURLKey) let task = session.dataTask(with: request) { (data, response, error) in // FIXME: paginating results