From 8aedd8e72a4e2d6d661e47edf73fd461cff2e282 Mon Sep 17 00:00:00 2001 From: Amir Abbas Date: Tue, 15 Aug 2017 13:42:41 +0430 Subject: [PATCH] Replaced OperationHandle with (NS)Progress --- Sources/CloudFileProvider.swift | 122 +++++++++++----- Sources/DropboxFileProvider.swift | 94 +++++++++--- Sources/DropboxHelper.swift | 51 +++++-- Sources/FPSStreamTask.swift | 84 +++++++++-- Sources/FTPFileProvider.swift | 208 +++++++++++++++++++-------- Sources/FTPHelper.swift | 12 +- Sources/FileProvider.swift | 112 +++++++-------- Sources/FileProviderExtensions.swift | 5 + Sources/LocalFileProvider.swift | 88 ++++++++---- Sources/LocalHelper.swift | 98 ------------- Sources/OneDriveFileProvider.swift | 91 +++++++++--- Sources/OneDriveHelper.swift | 35 ++++- Sources/RemoteSession.swift | 102 ++++++------- Sources/SMBFileProvider.swift | 21 +-- Sources/WebDAVFileProvider.swift | 149 ++++++++++++++----- 15 files changed, 819 insertions(+), 453 deletions(-) diff --git a/Sources/CloudFileProvider.swift b/Sources/CloudFileProvider.swift index 61e9316..f7bac8b 100644 --- a/Sources/CloudFileProvider.swift +++ b/Sources/CloudFileProvider.swift @@ -243,13 +243,13 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { - Note: For now only it's limited to file names. `query` parameter may take `NSPredicate` format in near future. - Parameters: - - path: location of directory to start search - - recursive: Searching subdirectories of path - - query: Simple string of file name to be search (for now). - - foundItemHandler: Closure which is called when a file is found - - completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured. + - path: location of directory to start search + - recursive: Searching subdirectories of path + - query: Simple string of file name to be search (for now). + - foundItemHandler: Closure which is called when a file is found + - completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured. */ - open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { + open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? { let mapDict: [String: String] = ["url": NSMetadataItemURLKey, "name": NSMetadataItemFSNameKey, "path": NSMetadataItemPathKey, "filesize": NSMetadataItemFSSizeKey, "modifiedDate": NSMetadataItemFSContentChangeDateKey, "creationDate": NSMetadataItemFSCreationDateKey, "contentType": NSMetadataItemContentTypeKey] @@ -282,6 +282,8 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { } } + let progress = Progress(parent: nil, userInfo: nil) + dispatch_queue.async { let pathURL = self.url(of: path) let mdquery = NSMetadataQuery() @@ -290,6 +292,10 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { var lastReportedCount = 0 + progress.cancellationHandler = { [weak mdquery] in + mdquery?.stop() + } + if let foundItemHandler = foundItemHandler { var updateObserver: NSObjectProtocol? @@ -314,6 +320,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { } } lastReportedCount = mdquery.resultCount + progress.totalUnitCount = Int64(lastReportedCount) mdquery.enableUpdates() }) @@ -346,12 +353,14 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { contents.append(file) } } + progress.completedUnitCount = Int64(contents.count) self.dispatch_queue.async { completionHandler(contents, nil) } }) DispatchQueue.main.async { + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) if !mdquery.start() { self.dispatch_queue.async { completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission)) @@ -359,6 +368,8 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { } } } + + return progress } open override func isReachable(completionHandler: @escaping (Bool) -> Void) { @@ -375,10 +386,10 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { - folder: Directory name. - at: Parent path of new directory. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`. + - Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`. */ @discardableResult - open override func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open override func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? { return super.create(folder: folderName, at: atPath, completionHandler: completionHandler) } @@ -392,10 +403,10 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { - to: destination path of file or directory, including file/directory name. - overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`. + - Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`. */ @discardableResult - open override func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open override func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { return super.moveItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler) } @@ -409,10 +420,10 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { - to: destination path of file or directory, including file/directory name. - overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`. + - Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`. */ @discardableResult - open override func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open override func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { return super.copyItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler) } @@ -426,11 +437,10 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { - Parameters: - path: file or directory path. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`. - + - Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`. */ @discardableResult - open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? { return super.removeItem(path: path, completionHandler: completionHandler) } @@ -443,13 +453,18 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { - to: destination path of file, including file/directory name. - overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. + - Returns: A `Progress` object to get progress or cancel progress. */ @discardableResult - open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { // TODO: Make use of overwrite parameter let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath) - let operationHandle = CloudOperationHandle(operationType: opType, baseURL: self.baseURL) + let progress = Progress(parent: nil, userInfo: nil) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.isCancellable = false + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + monitorFile(path: toPath, opType: opType, progress: progress) operation_queue.addOperation { let tempFolder: URL if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { @@ -477,7 +492,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { }) } } - return operationHandle + return progress } /** @@ -488,12 +503,17 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { - path: original file or directory path. - toLocalURL: destination local url of file, including file/directory name. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. + - Returns: A `Progress` object to get progress or cancel progress. */ @discardableResult - open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString) - + let progress = Progress(parent: nil, userInfo: nil) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.isCancellable = false + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + monitorFile(path: path, opType: opType, progress: progress) do { try self.opFileManager.startDownloadingUbiquitousItem(at: self.url(of: path)) } catch let e { @@ -503,8 +523,8 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { }) return nil } - let handle = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler) - return handle + let _ = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler) + return progress } /** @@ -516,11 +536,18 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { - completionHandler: a closure with result of file contents or error. `contents`: contents of file in a `Data` object. `error`: Error returned by system. - - Returns: An `OperationHandle` to get progress or cancel progress. + - Returns: A `Progress` object to get progress or cancel progress. */ @discardableResult - open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { - return super.contents(path: path, completionHandler: completionHandler) + 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(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + monitorFile(path: path, opType: operation, progress: progress) + _ = super.contents(path: path, completionHandler: completionHandler) + return progress } /** @@ -534,11 +561,18 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { - completionHandler: a closure with result of file contents or error. `contents`: contents of file in a `Data` object. `error`: Error returned by system. - - Returns: An `OperationHandle` to get progress or cancel progress. + - Returns: A `Progress` object to get progress or cancel progress. */ @discardableResult - open override func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { - return super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler) + 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(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + monitorFile(path: path, opType: operation, progress: progress) + _ = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler) + return progress } /** @@ -553,8 +587,15 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. */ @discardableResult - open override func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { - return super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler) + 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) + progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + monitorFile(path: path, opType: operation, progress: progress) + _ = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler) + return progress } fileprivate var monitors = [String: (NSMetadataQuery, NSObjectProtocol)]() @@ -639,32 +680,40 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { return file } - func monitorFile(path: String, opType: FileOperationType) { + func monitorFile(path: String, opType: 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: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in + 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)) DispatchQueue.main.async { self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(uploaded / 100)) } } else if (uploaded == 0 || uploaded == 100) && (downloaded > 0 && downloaded < 100) { + progress?.completedUnitCount = Int64(downloaded / 100 * Double(progress?.totalUnitCount ?? 0)) DispatchQueue.main.async { self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(downloaded / 100)) } } else if uploaded == 100 || downloaded == 100 { + progress?.completedUnitCount = progress?.totalUnitCount ?? 0 query.stop() NotificationCenter.default.removeObserver(updateObserver!) DispatchQueue.main.async { @@ -676,6 +725,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { }) DispatchQueue.main.async { + progress?.setUserInfoObject(Date(), forKey: .startingTimeKey) query.start() } } @@ -744,7 +794,7 @@ public enum UbiquitousScope: RawRepresentable { } } } - +/* /// Get progress of CloudFileProvider operations open class CloudOperationHandle: OperationHandle { /// Url of file which operation is doing on @@ -833,4 +883,4 @@ open class CloudOperationHandle: OperationHandle { _ = group.wait(timeout: .now() + 30) return item } -} +}*/ diff --git a/Sources/DropboxFileProvider.swift b/Sources/DropboxFileProvider.swift index 606cf5e..1ade275 100644 --- a/Sources/DropboxFileProvider.swift +++ b/Sources/DropboxFileProvider.swift @@ -44,7 +44,7 @@ open class DropboxFileProvider: FileProviderBasicRemote { public var validatingCache: Bool fileprivate var _session: URLSession? - fileprivate var sessionDelegate: SessionDelegate? + internal fileprivate(set) var sessionDelegate: SessionDelegate? public var session: URLSession { get { if _session == nil { @@ -153,7 +153,8 @@ open class DropboxFileProvider: FileProviderBasicRemote { } open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) { - list(path) { (contents, cursor, error) in + let progress = Progress(parent: nil, userInfo: nil) + list(path, progress: progress) { (contents, cursor, error) in completionHandler(contents, error) } } @@ -198,12 +199,13 @@ open class DropboxFileProvider: FileProviderBasicRemote { task.resume() } - open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { + 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) 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. // We will use it if there is a `name BEGINSWITH[c] "query"` in predicate, then filter to form final result. - search(path, query: queryStr, foundItem: { (file) in + search(path, query: queryStr, progress: progress, foundItem: { (file) in if query.evaluate(with: file.mapPredicate()) { foundFiles.append(file) foundItemHandler?(file) @@ -214,7 +216,7 @@ open class DropboxFileProvider: FileProviderBasicRemote { } else { // Dropbox doesn't support searching attributes natively. The workaround is to fallback to listing all files // and filter it locally. It may have a network burden in case there is many files in Dropbox, so please use it concisely. - list(path, recursive: true, progressHandler: { (files, _, error) in + list(path, recursive: true, progress: progress, progressHandler: { (files, _, error) in for file in files where query.evaluate(with: file.mapPredicate()) { foundItemHandler?(file) } @@ -223,6 +225,7 @@ open class DropboxFileProvider: FileProviderBasicRemote { completionHandler(predicatedFiles, error) }) } + return progress } open func isReachable(completionHandler: @escaping (Bool) -> Void) { @@ -235,27 +238,33 @@ open class DropboxFileProvider: FileProviderBasicRemote { } extension DropboxFileProvider: FileProviderOperations { - open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? { let path = (atPath as NSString).appendingPathComponent(folderName) + "/" return doOperation(.create(path: path), completionHandler: completionHandler) } - open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler) } - open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler) } - open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? { return doOperation(.remove(path: path), completionHandler: completionHandler) } - fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else { return nil } + + let progress = Progress(totalUnitCount: 1) + progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + let url: String guard let sourcePath = operation.source else { return nil } let destPath = operation.destination @@ -288,15 +297,24 @@ extension DropboxFileProvider: FileProviderOperations { if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) { serverError = FileProviderDropboxError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) } + if serverError == nil && error == nil { + progress.completedUnitCount = 1 + } else { + progress.cancel() + } completionHandler?(serverError ?? error) self.delegateNotify(operation, error: serverError ?? error) }) task.taskDescription = operation.json + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() - return RemoteOperationHandle(operationType: operation, tasks: [task]) + return progress } - open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { // check file is not a folder guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else { dispatch_queue.async { @@ -312,22 +330,36 @@ extension DropboxFileProvider: FileProviderOperations { return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler) } - open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } let url = URL(string: "files/download", relativeTo: contentURL)! + + var progress = Progress(parent: nil, userInfo: nil) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + var request = URLRequest(url: url) request.set(httpAuthentication: credential, with: .oAuth2) request.set(dropboxArgKey: ["path": path as NSString]) let task = session.downloadTask(with: request) - completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = completionHandler + completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in + if error != nil { + progress.cancel() + } + completionHandler?(error) + } downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else { let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1) let errorData : Data? = nil //Data(contentsOf:cacheURL) // TODO: Figure out how to get error response data for the error description let serverError : FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil + if serverError != nil { + progress.cancel() + } completionHandler?(serverError) return } @@ -339,13 +371,19 @@ extension DropboxFileProvider: FileProviderOperations { } } task.taskDescription = opType.json + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress) + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() - return RemoteOperationHandle(operationType: opType, tasks: [task]) + return progress } } extension DropboxFileProvider: FileProviderReadWrite { - open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { + open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { if length == 0 || offset < 0 { dispatch_queue.async { completionHandler(Data(), nil) @@ -355,19 +393,31 @@ extension DropboxFileProvider: FileProviderReadWrite { let opType = FileOperationType.fetch(path: path) let url = URL(string: "files/download", relativeTo: contentURL)! + + var progress = Progress(parent: nil, userInfo: nil) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + var request = URLRequest(url: url) request.set(httpAuthentication: credential, with: .oAuth2) request.set(rangeWithOffset: offset, length: length) request.set(dropboxArgKey: ["path": correctPath(path)! as NSString]) let task = session.downloadTask(with: request) completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in + if error != nil { + progress.cancel() + } completionHandler(nil, error) } - downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in + downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else { let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1) let errorData : Data? = nil //Data(contentsOf:cacheURL) // TODO: Figure out how to get error response data for the error description let serverError : FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil + if serverError != nil { + progress.cancel() + } completionHandler(nil, serverError) return } @@ -379,11 +429,17 @@ extension DropboxFileProvider: FileProviderReadWrite { } } task.taskDescription = opType.json + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress) + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() - return RemoteOperationHandle(operationType: opType, tasks: [task]) + return progress } - public func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + public func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.modify(path: path) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil diff --git a/Sources/DropboxHelper.swift b/Sources/DropboxHelper.swift index a624d43..3cff18a 100644 --- a/Sources/DropboxHelper.swift +++ b/Sources/DropboxHelper.swift @@ -71,7 +71,11 @@ public final class DropboxFileObject: FileObject { // codebeat:disable[ARITY] internal extension DropboxFileProvider { - func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, session: URLSession? = nil, progressHandler: ((_ contents: [FileObject], _ nextCursor: String?, _ error: Error?) -> Void)? = nil, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) { + + + func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, session: URLSession? = nil, progress: Progress, progressHandler: ((_ contents: [FileObject], _ nextCursor: String?, _ error: Error?) -> Void)? = nil, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) { + if progress.isCancelled { return } + var requestDictionary = [String: AnyObject]() let url: URL if let cursor = cursor { @@ -99,13 +103,14 @@ internal extension DropboxFileProvider { for entry in entries { if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) { files.append(file) + progress.totalUnitCount = Int64(files.count) } } let ncursor = json["cursor"] as? String let hasmore = (json["has_more"] as? NSNumber)?.boolValue ?? false - if hasmore { + if hasmore && !progress.isCancelled { progressHandler?(files, ncursor, responseError ?? error) - self.list(path, cursor: ncursor, prevContents: prevContents + files, completionHandler: completionHandler) + self.list(path, cursor: ncursor, prevContents: prevContents + files, progress: progress, completionHandler: completionHandler) return } } @@ -113,11 +118,15 @@ internal extension DropboxFileProvider { progressHandler?(files, nil, responseError ?? error) completionHandler(prevContents + files, nil, responseError ?? error) }) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.taskDescription = FileOperationType.fetch(path: path).json task.resume() } - func upload_simple(_ targetPath: String, data: Data? = nil, localFile: URL? = nil, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + func upload_simple(_ targetPath: String, data: Data? = nil, localFile: URL? = nil, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? { let size = data?.count ?? Int((try? localFile?.resourceValues(forKeys: [.fileSizeKey]))??.fileSize ?? -1) if size > 150 * 1024 * 1024 { let error = FileProviderDropboxError(code: .payloadTooLarge, path: targetPath, errorDescription: nil) @@ -125,6 +134,13 @@ internal extension DropboxFileProvider { self.delegateNotify(.create(path: targetPath), error: error) return nil } + + var progress = Progress(parent: nil, userInfo: nil) + progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + progress.totalUnitCount = Int64(size) + var requestDictionary = [String: AnyObject]() let url: URL url = URL(string: "files/upload", relativeTo: contentURL)! @@ -151,15 +167,25 @@ internal extension DropboxFileProvider { // We can't fetch server result from delegate! responseError = FileProviderDropboxError(code: rCode, path: targetPath, errorDescription: nil) } + if !(responseError == nil && error == nil) { + progress.cancel() + } completionHandler?(responseError ?? error) - self?.delegateNotify(.create(path: targetPath), error: responseError ?? error) + self?.delegateNotify(.modify(path: targetPath), error: responseError ?? error) } task.taskDescription = operation.json + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() - return RemoteOperationHandle(operationType: operation, tasks: [task]) + return progress } - func search(_ startPath: String = "", query: String, start: Int = 0, maxResultPerPage: Int = 25, maxResults: Int = -1, foundItem:@escaping ((_ file: DropboxFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) { + func search(_ startPath: String = "", query: String, start: Int = 0, maxResultPerPage: Int = 25, maxResults: Int = -1, progress: Progress, foundItem:@escaping ((_ file: DropboxFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) { + if progress.isCancelled { return } + let url = URL(string: "files/search", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" @@ -180,12 +206,13 @@ internal extension DropboxFileProvider { for entry in entries { if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) { foundItem(file) + progress.completedUnitCount += 1 } } let rstart = json["start"] as? Int let hasmore = (json["more"] as? NSNumber)?.boolValue ?? false - if hasmore, let rstart = rstart { - self.search(startPath, query: query, start: rstart + entries.count, maxResultPerPage: maxResultPerPage, foundItem: foundItem, completionHandler: completionHandler) + if hasmore && !progress.isCancelled, let rstart = rstart { + self.search(startPath, query: query, start: rstart + entries.count, maxResultPerPage: maxResultPerPage, progress: progress, foundItem: foundItem, completionHandler: completionHandler) } else { completionHandler(responseError ?? error) } @@ -193,7 +220,11 @@ internal extension DropboxFileProvider { } } completionHandler(responseError ?? error) - }) + }) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() } } diff --git a/Sources/FPSStreamTask.swift b/Sources/FPSStreamTask.swift index f7c8840..9f4e18c 100644 --- a/Sources/FPSStreamTask.swift +++ b/Sources/FPSStreamTask.swift @@ -26,7 +26,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { fileprivate var _taskDescription: String? /// Force using `URLSessionStreamTask` for iOS 9 and later - public var useURLSession = true + public let useURLSession: Bool @available(iOS 9.0, OSX 10.11, *) fileprivate static var streamTasks = [Int: URLSessionStreamTask]() @@ -121,8 +121,31 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { } } - fileprivate var _countOfBytesSent: Int64 = 0 - fileprivate var _countOfBytesRecieved: Int64 = 0 + fileprivate var _countOfBytesSent: Int64 = 0 { + willSet { + for observer in observers where observer.keyPath == "countOfBytesSent" { + observer.observer.observeValue(forKeyPath: observer.keyPath, of: self, change: [.oldKey: _countOfBytesSent, .oldKey: newValue], context: observer.context) + } + } + didSet { + for observer in observers where observer.keyPath == "countOfBytesSent" { + observer.observer.observeValue(forKeyPath: observer.keyPath, of: self, change: [.oldKey: oldValue, .oldKey: _countOfBytesSent], context: observer.context) + } + } + } + + fileprivate var _countOfBytesRecieved: Int64 = 0 { + willSet { + for observer in observers where observer.keyPath == "countOfBytesRecieved" { + observer.observer.observeValue(forKeyPath: observer.keyPath, of: self, change: [.oldKey: _countOfBytesRecieved, .oldKey: newValue], context: observer.context) + } + } + didSet { + for observer in observers where observer.keyPath == "countOfBytesRecieved" { + observer.observer.observeValue(forKeyPath: observer.keyPath, of: self, change: [.oldKey: oldValue, .oldKey: _countOfBytesRecieved], context: observer.context) + } + } + } /** * The number of bytes that the task has sent to the server in the request body. @@ -133,7 +156,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { * `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)` delegate method. */ override open var countOfBytesSent: Int64 { - if #available(iOS 9.0, OSX 10.11, *) { + if #available(iOS 9.0, macOS 10.11, *) { if self.useURLSession { return _underlyingTask!.countOfBytesSent } @@ -192,6 +215,49 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { return Int64(dataReceived.count) } + var observers: [(keyPath: String, observer: NSObject, context: UnsafeMutableRawPointer?)] = [] + + public override func addObserver(_ observer: NSObject, forKeyPath keyPath: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer?) { + if #available(iOS 9.0, macOS 10.11, *) { + if self.useURLSession { + self._underlyingTask?.addObserver(observer, forKeyPath: keyPath, options: options, context: context) + return + } + } + + switch keyPath { + case #keyPath(countOfBytesSent): + fallthrough + case #keyPath(countOfBytesReceived): + fallthrough + case #keyPath(countOfBytesExpectedToSend): + fallthrough + case #keyPath(countOfBytesExpectedToReceive): + observers.append((keyPath: keyPath, observer: observer, context: context)) + default: + break + } + super.addObserver(observer, forKeyPath: keyPath, options: options, context: context) + } + + public override func removeObserver(_ observer: NSObject, forKeyPath keyPath: String) { + var newObservers: [(keyPath: String, observer: NSObject, context: UnsafeMutableRawPointer?)] = [] + for observer in observers where observer.keyPath != keyPath { + newObservers.append(observer) + } + self.observers = newObservers + super.removeObserver(observer, forKeyPath: keyPath) + } + + public override func removeObserver(_ observer: NSObject, forKeyPath keyPath: String, context: UnsafeMutableRawPointer?) { + var newObservers: [(keyPath: String, observer: NSObject, context: UnsafeMutableRawPointer?)] = [] + for observer in observers where observer.keyPath != keyPath || observer.context != context { + newObservers.append(observer) + } + self.observers = newObservers + super.removeObserver(observer, forKeyPath: keyPath, context: context) + } + override public init() { fatalError("Use NSURLSession.fpstreamTask() method") } @@ -199,10 +265,11 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { fileprivate var host: (hostname: String, port: Int)? fileprivate var service: NetService? - internal init(session: URLSession, host: String, port: Int) { + internal init(session: URLSession, host: String, port: Int, useURLSession: Bool = true) { self._underlyingSession = session + self.useURLSession = useURLSession if #available(iOS 9.0, OSX 10.11, *) { - if self.useURLSession { + if useURLSession { let task = session.streamTask(withHostName: host, port: port) self._taskIdentifier = task.taskIdentifier FileProviderStreamTask.streamTasks[_taskIdentifier] = task @@ -218,10 +285,11 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { self.operation_queue.maxConcurrentOperationCount = 1 } - internal init(session: URLSession, netService: NetService) { + internal init(session: URLSession, netService: NetService, useURLSession: Bool = true) { self._underlyingSession = session + self.useURLSession = useURLSession if #available(iOS 9.0, OSX 10.11, *) { - if self.useURLSession { + if useURLSession { let task = session.streamTask(with: netService) self._taskIdentifier = task.taskIdentifier FileProviderStreamTask.streamTasks[_taskIdentifier] = task diff --git a/Sources/FTPFileProvider.swift b/Sources/FTPFileProvider.swift index ac60325..26374b0 100644 --- a/Sources/FTPFileProvider.swift +++ b/Sources/FTPFileProvider.swift @@ -279,12 +279,14 @@ open class FTPFileProvider: FileProviderBasicRemote { } } - open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { - self.recursiveList(path: path, useMLST: true, foundItemsHandler: { items in + 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) + _ = self.recursiveList(path: path, useMLST: true, foundItemsHandler: { items in if let foundItemHandler = foundItemHandler { for item in items where query.evaluate(with: item.mapPredicate()) { foundItemHandler(item) } + progress.totalUnitCount = Int64(items.count) } }, completionHandler: {files, error in if let error = error { @@ -295,6 +297,7 @@ open class FTPFileProvider: FileProviderBasicRemote { let foundFiles = files.filter { query.evaluate(with: $0.mapPredicate()) } completionHandler(foundFiles, nil) }) + return progress } public func url(of path: String?) -> URL { @@ -330,24 +333,24 @@ open class FTPFileProvider: FileProviderBasicRemote { } extension FTPFileProvider: FileProviderOperations { - open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? { let path = (atPath as NSString).appendingPathComponent(folderName) + "/" return doOperation(.create(path: path), completionHandler: completionHandler) } - open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler) } - open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler) } - open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? { return doOperation(.remove(path: path), completionHandler: completionHandler) } - fileprivate func doOperation(_ opType: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + fileprivate func doOperation(_ opType: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } @@ -369,7 +372,10 @@ extension FTPFileProvider: FileProviderOperations { default: // modify, fetch return nil } - let operationHandle = RemoteOperationHandle(operationType: opType, tasks: []) + let progress = Progress(totalUnitCount: 1) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!) self.ftpLogin(task) { (error) in @@ -412,13 +418,12 @@ extension FTPFileProvider: FileProviderOperations { case .modify: errorCode = URLError.cannotWriteToFile case .copy: - let opHandle = self.fallbackCopy(opType, completionHandler: completionHandler) as? RemoteOperationHandle - operationHandle.tasks = opHandle?.tasks ?? [] + self.fallbackCopy(opType, progress: progress, completionHandler: completionHandler) return case .move: errorCode = URLError.cannotMoveFile case .remove: - self.fallbackRemove(opType, on: task, completionHandler: completionHandler) + self.fallbackRemove(opType, progress: progress, on: task, completionHandler: completionHandler) return case .link: errorCode = URLError.cannotWriteToFile @@ -426,31 +431,37 @@ extension FTPFileProvider: FileProviderOperations { errorCode = URLError.cannotOpenFile } let error = self.throwError(sourcePath, code: errorCode) + progress.cancel() self.dispatch_queue.async { completionHandler?(error) - self.delegateNotify(opType, error: error) } + self.delegateNotify(opType, error: error) return } + progress.completedUnitCount = progress.totalUnitCount self.dispatch_queue.async { completionHandler?(nil) - self.delegateNotify(opType, error: nil) } + self.delegateNotify(opType, error: nil) }) } - operationHandle.add(task: task) - return operationHandle + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) + return progress } - private func fallbackCopy(_ opType: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? { - guard let sourcePath = opType.source else { return nil } - guard let destPath = opType.destination else { return nil } + private func fallbackCopy(_ opType: FileOperationType, progress: Progress, completionHandler: SimpleCompletionHandler) { + guard let sourcePath = opType.source else { return } + guard let destPath = opType.destination else { return } let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).appendingPathExtension("tmp") - let operationHandle = RemoteOperationHandle(operationType: opType, tasks: []) - let firstOp = self.copyItem(path: sourcePath, toLocalURL: localURL, completionHandler: { (error) in + + progress.becomeCurrent(withPendingUnitCount: 1) + _ = self.copyItem(path: sourcePath, toLocalURL: localURL) { (error) in if let error = error { self.dispatch_queue.async { completionHandler?(error) @@ -459,39 +470,42 @@ extension FTPFileProvider: FileProviderOperations { return } - let secondOp = self.copyItem(localFile: localURL, to: destPath, completionHandler: { error in + progress.becomeCurrent(withPendingUnitCount: 1) + _ = self.copyItem(localFile: localURL, to: destPath) { error in completionHandler?(nil) self.delegateNotify(opType, error: nil) - }) as? RemoteOperationHandle - operationHandle.tasks = secondOp?.tasks ?? [] - }) as? RemoteOperationHandle - operationHandle.tasks = firstOp?.tasks ?? [] - return operationHandle + } + progress.resignCurrent() + } + progress.resignCurrent() + return } - private func fallbackRemove(_ opType: FileOperationType, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) { + private func fallbackRemove(_ opType: FileOperationType, progress: Progress, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) { guard let sourcePath = opType.source else { return } self.execute(command: "SITE RMDIR \(ftpPath(sourcePath))", on: task) { (response, error) in if let error = error { + progress.cancel() self.dispatch_queue.async { completionHandler?(error) - self.delegateNotify(opType, error: error) } + self.delegateNotify(opType, error: error) return } guard let response = response else { + progress.cancel() let error = self.throwError(sourcePath, code: URLError.badServerResponse) self.dispatch_queue.async { completionHandler?(error) - self.delegateNotify(opType, error: error) } + self.delegateNotify(opType, error: error) return } if response.hasPrefix("50") { - self.fallbackRecursiveRemove(opType, on: task, completionHandler: completionHandler) + self.fallbackRecursiveRemove(opType, progress: progress, on: task, completionHandler: completionHandler) return } @@ -501,15 +515,15 @@ extension FTPFileProvider: FileProviderOperations { } self.dispatch_queue.async { completionHandler?(error) - self.delegateNotify(opType, error: error) } + self.delegateNotify(opType, error: error) } } - private func fallbackRecursiveRemove(_ opType: FileOperationType, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) { + private func fallbackRecursiveRemove(_ opType: FileOperationType, progress: Progress, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) { guard let sourcePath = opType.source else { return } - self.recursiveList(path: sourcePath, useMLST: true, completionHandler: { (contents, error) in + _ = self.recursiveList(path: sourcePath, useMLST: true, completionHandler: { (contents, error) in if let error = error { self.dispatch_queue.async { completionHandler?(error) @@ -518,6 +532,8 @@ extension FTPFileProvider: FileProviderOperations { return } + let recursiveProgress = Progress(parent: progress, userInfo: nil) + recursiveProgress.totalUnitCount = Int64(contents.count) let sortedContents = contents.sorted(by: { $0.path.localizedStandardCompare($1.path) == .orderedDescending }) @@ -528,6 +544,7 @@ extension FTPFileProvider: FileProviderOperations { command += "RMD \(self.ftpPath(sourcePath))" self.execute(command: command, on: task, completionHandler: { (response, error) in + recursiveProgress.completedUnitCount += 1 self.dispatch_queue.async { completionHandler?(error) self.delegateNotify(opType, error: error) @@ -537,7 +554,7 @@ extension FTPFileProvider: FileProviderOperations { }) } - open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { // check file is not a folder guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else { dispatch_queue.async { @@ -550,7 +567,11 @@ extension FTPFileProvider: FileProviderOperations { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } - let operation = RemoteOperationHandle(operationType: opType, tasks: []) + + let progress = Progress(totalUnitCount: 0) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!) self.ftpLogin(task) { (error) in @@ -562,13 +583,21 @@ extension FTPFileProvider: FileProviderOperations { return } - self.ftpStore(task, filePath: self.ftpPath(toPath), fromData: nil, fromFile: localFile, onTask: { - operation.add(task: $0) + self.ftpStore(task, filePath: self.ftpPath(toPath), fromData: nil, fromFile: localFile, onTask: { task in + weak var weakTask = task + progress.cancellationHandler = { + weakTask?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) }, onProgress: { bytesSent, totalSent, expectedBytes in + progress.completedUnitCount = totalSent DispatchQueue.main.async { - self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(Double(totalSent) / Double(expectedBytes))) + self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress.fractionCompleted)) } }, completionHandler: { (error) in + if error != nil { + progress.cancel() + } self.ftpQuit(task) self.dispatch_queue.async { completionHandler?(error) @@ -577,15 +606,18 @@ extension FTPFileProvider: FileProviderOperations { }) } - return operation + return progress } - open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } - let operation = RemoteOperationHandle(operationType: opType, tasks: []) + var progress = Progress(totalUnitCount: 0) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) if self.useAppleImplementation { self.attributesOfItem(path: path, completionHandler: { (file, error) in @@ -606,6 +638,8 @@ extension FTPFileProvider: FileProviderOperations { return } + progress.totalUnitCount = file?.size ?? 0 + let task = self.session.downloadTask(with: self.url(of: path)) completionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = completionHandler downloadCompletionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = { tempURL in @@ -616,8 +650,13 @@ extension FTPFileProvider: FileProviderOperations { completionHandler?(e) } } - operation.add(task: task) task.taskDescription = opType.json + task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress) + task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() }) } else { @@ -630,13 +669,21 @@ extension FTPFileProvider: FileProviderOperations { return } - self.ftpRetrieveFile(task, filePath: self.ftpPath(path), onTask: { - operation.add(task: $0) + self.ftpRetrieveFile(task, filePath: self.ftpPath(path), onTask: { task in + weak var weakTask = task + progress.cancellationHandler = { + weakTask?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) }, onProgress: { recevied, totalReceived, totalSize in - let progress = Double(totalReceived) / Double(totalSize) - self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress)) + progress.totalUnitCount = totalSize + progress.completedUnitCount = totalReceived + DispatchQueue.main.async { + self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress.fractionCompleted)) + } }) { (tmpurl, error) in if let error = error { + progress.cancel() self.dispatch_queue.async { completionHandler?(error) self.delegateNotify(opType, error: error) @@ -654,20 +701,28 @@ extension FTPFileProvider: FileProviderOperations { } } } - return operation + return progress } } extension FTPFileProvider: FileProviderReadWrite { - open func contents(path: String, completionHandler: @escaping ((Data?, Error?) -> Void)) -> OperationHandle? { + open func contents(path: String, completionHandler: @escaping ((Data?, Error?) -> Void)) -> Progress? { let opType = FileOperationType.fetch(path: path) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } if self.useAppleImplementation { + var progress = Progress(totalUnitCount: 0) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + let task = session.downloadTask(with: url(of: path)) completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in + if error != nil { + progress.cancel() + } completionHandler(nil, error) } downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in @@ -679,14 +734,20 @@ extension FTPFileProvider: FileProviderReadWrite { } } task.taskDescription = opType.json + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress) + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() - return RemoteOperationHandle(operationType: opType, tasks: [task]) + return progress } else { return self.contents(path: path, offset: 0, length: -1, completionHandler: completionHandler) } } - open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { + open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { let opType = FileOperationType.fetch(path: path) if length == 0 || offset < 0 { dispatch_queue.async { @@ -695,7 +756,10 @@ extension FTPFileProvider: FileProviderReadWrite { } return nil } - let operation = RemoteOperationHandle(operationType: opType, tasks: []) + let progress = Progress(totalUnitCount: 0) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!) self.ftpLogin(task) { (error) in @@ -706,13 +770,21 @@ extension FTPFileProvider: FileProviderReadWrite { return } - self.ftpRetrieveData(task, filePath: self.ftpPath(path), from: offset, length: length, onTask: { - operation.add(task: $0) + self.ftpRetrieveData(task, filePath: self.ftpPath(path), from: offset, length: length, onTask: { task in + weak var weakTask = task + progress.cancellationHandler = { + weakTask?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) }, onProgress: { recevied, totalReceived, totalSize in - let progress = Double(totalReceived) / Double(totalSize) - self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress)) + progress.totalUnitCount = totalSize + progress.completedUnitCount = totalReceived + DispatchQueue.main.async { + self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress.fractionCompleted)) + } }) { (data, error) in if let error = error { + progress.cancel() self.dispatch_queue.async { completionHandler(nil, error) self.delegateNotify(opType, error: error) @@ -729,16 +801,20 @@ extension FTPFileProvider: FileProviderReadWrite { } } - return operation + return progress } - open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.modify(path: path) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } - let operation = RemoteOperationHandle(operationType: opType, tasks: []) + let progress = Progress(totalUnitCount: Int64(data?.count ?? 0)) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!) self.ftpLogin(task) { (error) in if let error = error { @@ -750,13 +826,21 @@ extension FTPFileProvider: FileProviderReadWrite { } let storeHandler = { - self.ftpStore(task, filePath: self.ftpPath(path), fromData: data ?? Data(), fromFile: nil, onTask: { - operation.add(task: $0) + self.ftpStore(task, filePath: self.ftpPath(path), fromData: data ?? Data(), fromFile: nil, onTask: { task in + weak var weakTask = task + progress.cancellationHandler = { + weakTask?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) }, onProgress: { bytesSent, totalSent, expectedBytes in + progress.completedUnitCount = totalSent DispatchQueue.main.async { - self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(Double(totalSent) / Double(expectedBytes))) + self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress.fractionCompleted)) } }, completionHandler: { (error) in + if error != nil { + progress.cancel() + } self.ftpQuit(task) self.dispatch_queue.async { completionHandler?(error) @@ -776,7 +860,7 @@ extension FTPFileProvider: FileProviderReadWrite { } } - return operation + return progress } } diff --git a/Sources/FTPHelper.swift b/Sources/FTPHelper.swift index 5312381..7dad79e 100644 --- a/Sources/FTPHelper.swift +++ b/Sources/FTPHelper.swift @@ -399,8 +399,9 @@ internal extension FTPFileProvider { } } - func recursiveList(path: String, useMLST: Bool, foundItemsHandler: ((_ contents: [FileObject]) -> Void)? = nil, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) { - let queue = DispatchQueue(label: "test") + func recursiveList(path: String, useMLST: Bool, foundItemsHandler: ((_ contents: [FileObject]) -> Void)? = nil, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress? { + let progress = Progress(totalUnitCount: 0) + let queue = DispatchQueue(label: "\(self.type).recursiveList") queue.async { let group = DispatchGroup() var result = [FileObject]() @@ -415,12 +416,14 @@ internal extension FTPFileProvider { } result.append(contentsOf: files) + progress.completedUnitCount = Int64(files.count) foundItemsHandler?(files) let directories: [FileObject] = files.filter { $0.isDirectory } + progress.becomeCurrent(withPendingUnitCount: Int64(directories.count)) for dir in directories { group.enter() - self.recursiveList(path: dir.path, useMLST: useMLST, foundItemsHandler: foundItemsHandler, completionHandler: { (contents, error) in + _=self.recursiveList(path: dir.path, useMLST: useMLST, foundItemsHandler: foundItemsHandler, completionHandler: { (contents, error) in success = success && (error == nil) if let error = error { completionHandler([], error) @@ -430,9 +433,11 @@ internal extension FTPFileProvider { foundItemsHandler?(files) result.append(contentsOf: contents) + group.leave() }) } + progress.resignCurrent() group.leave() }) group.wait() @@ -443,6 +448,7 @@ internal extension FTPFileProvider { } } } + return progress } func ftpRetrieveData(_ task: FileProviderStreamTask, filePath: String, from position: Int64 = 0, length: Int = -1, onTask: ((_ task: FileProviderStreamTask) -> Void)?, onProgress: ((_ bytesReceived: Int64, _ totalReceived: Int64, _ expectedBytes: Int64) -> Void)?, completionHandler: @escaping (_ data: Data?, _ error: Error?) -> Void) { diff --git a/Sources/FileProvider.swift b/Sources/FileProvider.swift index 9c3f8c9..8115a1d 100644 --- a/Sources/FileProvider.swift +++ b/Sources/FileProvider.swift @@ -99,7 +99,8 @@ public protocol FileProviderBasic: class, NSSecureCoding { - foundItemHandler: Closure which is called when a file is found - completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured. */ - func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) + @discardableResult + func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? /** Search files inside directory using query asynchronously. @@ -121,8 +122,10 @@ public protocol FileProviderBasic: class, NSSecureCoding { - query: An `NSPredicate` object with keys like `FileObject` members, except `size` which becomes `filesize`. - foundItemHandler: Closure which is called when a file is found - completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured. + - Returns: An `Progress` to get progress or cancel progress. */ - func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) + @discardableResult + func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? /** Returns an independent url to access the file. Some providers like `Dropbox` due to their nature. @@ -146,9 +149,9 @@ public protocol FileProviderBasic: class, NSSecureCoding { } extension FileProviderBasic { - public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { + public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? { let predicate = NSPredicate(format: "name BEGINSWITH[c] %@", query) - self.searchFiles(path: path, recursive: recursive, query: predicate, foundItemHandler: foundItemHandler, completionHandler: completionHandler) + return self.searchFiles(path: path, recursive: recursive, query: predicate, foundItemHandler: foundItemHandler, completionHandler: completionHandler) } /// The maximum number of queued operations that can execute at the same time. @@ -162,8 +165,6 @@ extension FileProviderBasic { operation_queue.maxConcurrentOperationCount = newValue } } - - } /// Checking equality of two file provider, regardless of current path queues and delegates. @@ -241,7 +242,7 @@ internal extension FileProviderBasicRemote { return false } - func runDataTask(with request: URLRequest, operationHandle: RemoteOperationHandle? = nil, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) { + func runDataTask(with request: URLRequest, operation: FileOperationType? = nil, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) { let useCache = self.useCache let validatingCache = self.validatingCache dispatch_queue.async { @@ -251,8 +252,7 @@ internal extension FileProviderBasicRemote { } } let task = self.session.dataTask(with: request, completionHandler: completionHandler) - task.taskDescription = operationHandle?.operationType.json - operationHandle?.add(task: task) + task.taskDescription = operation?.json task.resume() } } @@ -271,10 +271,10 @@ public protocol FileProviderOperations: FileProviderBasic { - folder: Directory name. - at: Parent path of new directory. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. */ @discardableResult - func create(folder: String, at: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? + func create(folder: String, at: String, completionHandler: SimpleCompletionHandler) -> Progress? /** Moves a file or directory from `path` to designated path asynchronously. @@ -285,10 +285,10 @@ public protocol FileProviderOperations: FileProviderBasic { - path: original file or directory path. - to: destination path of file or directory, including file/directory name. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. */ @discardableResult - func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? + func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress? /** Moves a file or directory from `path` to designated path asynchronously. @@ -300,10 +300,10 @@ public protocol FileProviderOperations: FileProviderBasic { - to: destination path of file or directory, including file/directory name. - overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. */ @discardableResult - func moveItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? + func moveItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? /** Copies a file or directory from `path` to designated path asynchronously. @@ -314,10 +314,10 @@ public protocol FileProviderOperations: FileProviderBasic { - path: original file or directory path. - to: destination path of file or directory, including file/directory name. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. */ @discardableResult - func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? + func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress? /** Copies a file or directory from `path` to designated path asynchronously. @@ -329,10 +329,10 @@ public protocol FileProviderOperations: FileProviderBasic { - to: destination path of file or directory, including file/directory name. - overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. */ @discardableResult - func copyItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? + func copyItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? /** Removes the file or directory at the specified path. @@ -340,11 +340,11 @@ public protocol FileProviderOperations: FileProviderBasic { - Parameters: - path: file or directory path. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. */ @discardableResult - func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? + func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? /** Uploads a file from local file url to designated path asynchronously. @@ -356,10 +356,10 @@ public protocol FileProviderOperations: FileProviderBasic { - localFile: a file url to file. - to: destination path of file, including file/directory name. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. + - Returns: An `Progress` to get progress or cancel progress. */ @discardableResult - func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? + func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> Progress? /** Uploads a file from local file url to designated path asynchronously. @@ -372,10 +372,10 @@ public protocol FileProviderOperations: FileProviderBasic { - to: destination path of file, including file/directory name. - overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. + - Returns: An `Progress` to get progress or cancel progress. */ @discardableResult - func copyItem(localFile: URL, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? + func copyItem(localFile: URL, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? /** Download a file from `path` to designated local file url asynchronously. @@ -387,33 +387,33 @@ public protocol FileProviderOperations: FileProviderBasic { - path: original file or directory path. - toLocalURL: destination local url of file, including file/directory name. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. */ @discardableResult - func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? + func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? } public extension FileProviderOperations { /// *DEPRECATED:* Use Use FileProviderReadWrite.writeContents(path:, data:, completionHandler:) method instead. @available(*, deprecated, message: "Use FileProviderReadWrite.writeContents(path:, data:, completionHandler:) method instead.") @discardableResult - public func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + public func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> Progress? { let path = (at as NSString).appendingPathComponent(file) return (self as? FileProviderReadWrite)?.writeContents(path: path, contents: data, completionHandler: completionHandler) } @discardableResult - public func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + public func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress? { return self.moveItem(path: path, to: to, overwrite: false, completionHandler: completionHandler) } @discardableResult - public func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + public func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> Progress? { return self.copyItem(localFile: localFile, to: to, overwrite: false, completionHandler: completionHandler) } @discardableResult - public func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + public func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress? { return self.copyItem(path: path, to: to, overwrite: false, completionHandler: completionHandler) } } @@ -429,10 +429,10 @@ public protocol FileProviderReadWrite: FileProviderBasic { - completionHandler: a closure with result of file contents or error. - `contents`: contents of file in a `Data` object. - `error`: Error returned by system. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. */ @discardableResult - func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? + func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? /** Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler. @@ -445,10 +445,10 @@ public protocol FileProviderReadWrite: FileProviderBasic { - completionHandler: a closure with result of file contents or error. - `contents`: contents of file in a `Data` object. - `error`: Error returned by system. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. */ @discardableResult - func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? + func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? /** Write the contents of the `Data` to a location asynchronously. @@ -459,10 +459,10 @@ public protocol FileProviderReadWrite: FileProviderBasic { - path: Path of target file. - contents: Data to be written into file, pass nil to create empty file. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. */ @discardableResult - func writeContents(path: String, contents: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? + func writeContents(path: String, contents: Data?, completionHandler: SimpleCompletionHandler) -> Progress? /** Write the contents of the `Data` to a location asynchronously. @@ -473,10 +473,10 @@ public protocol FileProviderReadWrite: FileProviderBasic { - contents: Data to be written into file, pass nil to create empty file. - atomically: data will be written to a temporary file before writing to final location. Default is `false`. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. */ @discardableResult - func writeContents(path: String, contents: Data?, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? + func writeContents(path: String, contents: Data?, atomically: Bool, completionHandler: SimpleCompletionHandler) -> Progress? /** Write the contents of the `Data` to a location asynchronously. @@ -487,10 +487,10 @@ public protocol FileProviderReadWrite: FileProviderBasic { - contents: Data to be written into file, pass nil to create empty file. - overwrite: Destination file should be overwritten if file is already exists. Default is `false`. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. */ @discardableResult - func writeContents(path: String, contents: Data?, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? + func writeContents(path: String, contents: Data?, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? /** Write the contents of the `Data` to a location asynchronously. @@ -501,30 +501,30 @@ public protocol FileProviderReadWrite: FileProviderBasic { - overwrite: Destination file should be overwritten if file is already exists. Default is `false`. - atomically: data will be written to a temporary file before writing to final location. Default is `false`. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. */ @discardableResult - func writeContents(path: String, contents: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? + func writeContents(path: String, contents: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? } extension FileProviderReadWrite { @discardableResult - public func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle?{ + public func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { return self.contents(path: path, offset: 0, length: -1, completionHandler: completionHandler) } @discardableResult - public func writeContents(path: String, contents: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + public func writeContents(path: String, contents: Data?, completionHandler: SimpleCompletionHandler) -> Progress? { return self.writeContents(path: path, contents: contents, atomically: false, overwrite: false, completionHandler: completionHandler) } @discardableResult - public func writeContents(path: String, contents: Data?, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + public func writeContents(path: String, contents: Data?, atomically: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { return self.writeContents(path: path, contents: contents, atomically: atomically, overwrite: false, completionHandler: completionHandler) } @discardableResult - public func writeContents(path: String, contents: Data?, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + public func writeContents(path: String, contents: Data?, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { return self.writeContents(path: path, contents: contents, atomically: false, overwrite: overwrite, completionHandler: completionHandler) } } @@ -570,7 +570,7 @@ public protocol FileProvideUndoable: FileProviderOperations { var undoManager: UndoManager? { get set } /// UndoManager supports undoing this file operation - func canUndo(handle: OperationHandle) -> Bool + func canUndo(handle: Progress) -> Bool /// UndoManager supports undoing this operation func canUndo(operation: FileOperationType) -> Bool } @@ -580,8 +580,11 @@ public extension FileProvideUndoable { return undoOperation(for: operation) != nil } - public func canUndo(handle: OperationHandle) -> Bool { - return canUndo(operation: handle.operationType) + public func canUndo(handle: Progress) -> Bool { + if let operationType = handle.userInfo[.fileProvderOperationTypeKey] as? FileOperationType { + return canUndo(operation: operationType) + } + return false } internal func undoOperation(for operation: FileOperationType) -> FileOperationType? { @@ -1008,6 +1011,7 @@ public enum FileOperationType: CustomStringConvertible { } /// Allows to get progress or cancel an in-progress operation, useful for remote providers +@available(*, obsoleted: 1.0, message: "Use NSProgress class instead.") public protocol OperationHandle { /// Operation supposed to be done on files. Contains file paths as associated value. var operationType: FileOperationType { get } @@ -1028,14 +1032,6 @@ public protocol OperationHandle { func cancel() -> Bool } -public extension OperationHandle { - public var progress: Float { - let bytesSoFar = self.bytesSoFar - let totalBytes = self.totalBytes - return totalBytes > 0 ? Float(Double(bytesSoFar) / Double(totalBytes)) : Float.nan - } -} - /// Delegate methods for reporting provider's operation result and progress, when it's ready to update /// user interface. /// All methods are called in main thread to avoids UI bugs. diff --git a/Sources/FileProviderExtensions.swift b/Sources/FileProviderExtensions.swift index f3beec9..2a2470b 100755 --- a/Sources/FileProviderExtensions.swift +++ b/Sources/FileProviderExtensions.swift @@ -50,6 +50,11 @@ public extension URLResourceKey { public static let isEncryptedKey = URLResourceKey(rawValue: "NSURLIsEncryptedKey") } +public extension ProgressUserInfoKey { + public static let fileProvderOperationTypeKey = ProgressUserInfoKey("FilesProviderOperationTypeKey") + public static let startingTimeKey = ProgressUserInfoKey("NSProgressstartingTimeKey") +} + internal extension URL { var uw_scheme: String { return self.scheme ?? "" diff --git a/Sources/LocalFileProvider.swift b/Sources/LocalFileProvider.swift index 17f9927..47dff8b 100644 --- a/Sources/LocalFileProvider.swift +++ b/Sources/LocalFileProvider.swift @@ -173,22 +173,31 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo completionHandler(totalSize, totalSize - freeSize) } - open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { + 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) + dispatch_queue.async { + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) let iterator = self.fileManager.enumerator(at: self.url(of: path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in completionHandler([], e) return true } var result = [LocalFileObject]() while let fileURL = iterator?.nextObject() as? URL { + if progress.isCancelled { + break + } let path = self.relativePathOf(url: fileURL) if let fileObject = LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), query.evaluate(with: fileObject.mapPredicate()) { result.append(fileObject) + progress.completedUnitCount = Int64(result.count) foundItemHandler?(fileObject) } } completionHandler(result, nil) } + + return progress } open func isReachable(completionHandler: @escaping (Bool) -> Void) { @@ -200,13 +209,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo open weak var fileOperationDelegate : FileOperationDelegate? @discardableResult - open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/") return self.doOperation(opType, completionHandler: completionHandler) } @discardableResult - open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.move(source: path, destination: toPath) if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) { @@ -218,7 +227,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } @discardableResult - open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.copy(source: path, destination: toPath) if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) { @@ -232,13 +241,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } @discardableResult - open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.remove(path: path) return self.doOperation(opType, completionHandler: completionHandler) } @discardableResult - open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) { self.dispatch_queue.async { completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum)) @@ -250,7 +259,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } @discardableResult - open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString) return self.doOperation(opType, completionHandler: completionHandler) } @@ -263,9 +272,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } @discardableResult - fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { - let localOperationHandle = LocalOperationHandle(operationType: opType, baseURL: self.baseURL) - + fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { + let progress = Progress(parent: nil, userInfo: nil) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.isCancellable = false + progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey) func urlofpath(path: String) -> URL { if path.hasPrefix("file://") { let removedSchemePath = path.replacingOccurrences(of: "file://", with: "", options: .anchored) @@ -299,23 +311,31 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo let operationHandler: (URL, URL?) -> Void = { source, dest in do { - localOperationHandle.inProgress = true + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) switch opType { case .create: if sourcePath.hasSuffix("/") { + progress.totalUnitCount = 1 try self.opFileManager.createDirectory(at: source, withIntermediateDirectories: true, attributes: [:]) } else { + progress.totalUnitCount = Int64(data?.count ?? 0) try data?.write(to: source, options: .atomic) } case .modify: + progress.totalUnitCount = Int64(data?.count ?? 0) try data?.write(to: source, options: atomically ? [.atomic] : []) case .copy: guard let dest = dest else { return } + progress.setUserInfoObject(Progress.FileOperationKind.copying, forKey: .fileOperationKindKey) + progress.totalUnitCount = abs(source.fileSize) try self.opFileManager.copyItem(at: source, to: dest) case .move: + progress.setUserInfoObject(Progress.FileOperationKind.copying, forKey: .fileOperationKindKey) guard let dest = dest else { return } + progress.totalUnitCount = abs(source.fileSize) try self.opFileManager.moveItem(at: source, to: dest) case.remove: + progress.totalUnitCount = abs(source.fileSize) try self.opFileManager.removeItem(at: source) default: return @@ -324,7 +344,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo source.stopAccessingSecurityScopedResource() } - localOperationHandle.inProgress = false + progress.completedUnitCount = progress.totalUnitCount self.dispatch_queue.async { completionHandler?(nil) } @@ -335,6 +355,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo if successfulSecurityScopedResourceAccess { source.stopAccessingSecurityScopedResource() } + progress.cancel() self.dispatch_queue.async { completionHandler?(e) } @@ -376,24 +397,31 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo operationHandler(source, dest) } } - return localOperationHandle + return progress } @discardableResult - open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { + open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { let opType = FileOperationType.fetch(path: path) - let localOperationHandle = LocalOperationHandle(operationType: opType, baseURL: self.baseURL) + let progress = Progress(parent: nil, userInfo: nil) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.isCancellable = false + progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey) + let url = self.url(of: path) - + progress.totalUnitCount = url.fileSize + let operationHandler: (URL) -> Void = { url in + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) do { - localOperationHandle.inProgress = true let data = try Data(contentsOf: url) - localOperationHandle.inProgress = false + progress.completedUnitCount = progress.totalUnitCount self.dispatch_queue.async { completionHandler(data, nil) } } catch let e { + progress.cancel() self.dispatch_queue.async { completionHandler(nil, e) } @@ -416,11 +444,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } } - return localOperationHandle + return progress } @discardableResult - open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { + open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { if length == 0 || offset < 0 { dispatch_queue.async { completionHandler(Data(), nil) @@ -433,7 +461,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } let opType = FileOperationType.fetch(path: path) - let localOperationHandle = LocalOperationHandle(operationType: opType, baseURL: self.baseURL) + let progress = Progress(parent: nil, userInfo: nil) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.isCancellable = false + progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey) + let url = self.url(of: path) let operationHandler: (URL) -> Void = { url in @@ -448,18 +481,19 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo handle.closeFile() } - localOperationHandle.inProgress = true let size = LocalFileObject(fileWithURL: url)?.size ?? -1 + progress.totalUnitCount = size guard size > offset else { - localOperationHandle.inProgress = false + progress.cancel() self.dispatch_queue.async { completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum)) } return } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) handle.seek(toFileOffset: UInt64(offset)) guard Int64(handle.offsetInFile) == offset else { - localOperationHandle.inProgress = false + progress.cancel() self.dispatch_queue.async { completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum)) } @@ -467,7 +501,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } let data = handle.readData(ofLength: length) - localOperationHandle.inProgress = false + progress.completedUnitCount = progress.totalUnitCount self.dispatch_queue.async { completionHandler(data, nil) } @@ -487,11 +521,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } } - return localOperationHandle + return progress } @discardableResult - open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { let fileExists = fileManager.fileExists(atPath: url(of: path).path) let opType: FileOperationType = fileExists ? .modify(path: path) : .create(path: path) return self.doOperation(opType, data: data ?? Data(), atomically: atomically, completionHandler: completionHandler) diff --git a/Sources/LocalHelper.swift b/Sources/LocalHelper.swift index 81e6d49..c7738ec 100644 --- a/Sources/LocalHelper.swift +++ b/Sources/LocalHelper.swift @@ -217,104 +217,6 @@ internal class LocalFileProviderManagerDelegate: NSObject, FileManagerDelegate { } } -/// - Note: Local operation handling is limited. Please don't use as much as possible. -open class LocalOperationHandle: OperationHandle { - /// Url of file which operation is doing on - public let baseURL: URL - /// Type of operation - public let operationType: FileOperationType - - init (operationType: FileOperationType, baseURL: URL?) { - self.baseURL = baseURL ?? URL(fileURLWithPath: "/") - self.operationType = operationType - inProgress = false - } - - private var sourceURL: URL? { - guard let source = operationType.source else { return nil } - return source.hasPrefix("file://") ? URL(fileURLWithPath: source) : baseURL.appendingPathComponent(source) - } - - private var destURL: URL? { - guard let dest = operationType.destination else { return nil } - return dest.hasPrefix("file://") ? URL(fileURLWithPath: dest) : baseURL.appendingPathComponent(dest) - } - - /// Caution: may put pressure on CPU, may have latency - open var bytesSoFar: Int64 { - assert(!Thread.isMainThread, "Don't run \(#function) method on main thread") - switch operationType { - case .modify: - guard let url = sourceURL, url.isFileURL else { return 0 } - if url.fileIsDirectory { - return iterateDirectory(url, deep: true).totalsize - } else { - return url.fileSize - } - case .copy, .move: - guard let url = destURL, url.isFileURL else { return 0 } - if url.fileIsDirectory { - return iterateDirectory(url, deep: true).totalsize - } else { - return url.fileSize - } - default: - return 0 - } - - } - - /// Caution: may put pressure on CPU, may have latency - open var totalBytes: Int64 { - assert(!Thread.isMainThread, "Don't run \(#function) method on main thread") - switch operationType { - case .copy, .move: - guard let url = sourceURL, url.isFileURL else { return 0 } - if url.fileIsDirectory { - return iterateDirectory(url, deep: true).totalsize - } else { - return url.fileSize - } - default: - return 0 - } - } - - /// Not usable in local provider - open var inProgress: Bool - - - /// Not usable in local provider - open func cancel() -> Bool{ - return false - } - - func iterateDirectory(_ pathURL: URL, deep: Bool) -> (folders: Int, files: Int, totalsize: Int64) { - var folders = 0 - var files = 0 - var totalsize: Int64 = 0 - let keys: [URLResourceKey] = [.isDirectoryKey, .fileSizeKey] - let enumOpt: FileManager.DirectoryEnumerationOptions = !deep ? [.skipsSubdirectoryDescendants, .skipsPackageDescendants] : [] - - let fp = FileManager() - let filesList = fp.enumerator(at: pathURL, includingPropertiesForKeys: keys, options: enumOpt, errorHandler: nil) - while let fileURL = filesList?.nextObject() as? URL { - guard let values = try? fileURL.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey]) else { continue } - let isdir = values.isDirectory ?? false - let size = Int64(values.fileSize ?? 0) - if isdir { - folders += 1 - } else { - files += 1 - } - totalsize += size - } - - return (folders, files, totalsize) - - } -} - class UndoBox: NSObject { weak var provider: FileProvideUndoable? let operation: FileOperationType diff --git a/Sources/OneDriveFileProvider.swift b/Sources/OneDriveFileProvider.swift index e9a0c7d..7a386cc 100644 --- a/Sources/OneDriveFileProvider.swift +++ b/Sources/OneDriveFileProvider.swift @@ -43,7 +43,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote { public var validatingCache: Bool fileprivate var _session: URLSession? - fileprivate var sessionDelegate: SessionDelegate? + internal fileprivate(set) var sessionDelegate: SessionDelegate? public var session: URLSession { get { if _session == nil { @@ -190,12 +190,13 @@ open class OneDriveFileProvider: FileProviderBasicRemote { task.resume() } - open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { + open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? { var foundFiles = [OneDriveFileObject]() 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 } - search(path, query: finalQueryStr, foundItem: { (file) in + guard let finalQueryStr = queryStr else { return nil } + let progress = Progress(parent: nil, userInfo: nil) + search(path, query: finalQueryStr, progress: progress, foundItem: { (file) in if query.evaluate(with: file.mapPredicate()) { foundFiles.append(file) foundItemHandler?(file) @@ -203,6 +204,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote { }, completionHandler: { (error) in completionHandler(foundFiles, error) }) + return progress } open func url(of path: String? = nil, modifier: String? = nil) -> URL { @@ -249,27 +251,33 @@ open class OneDriveFileProvider: FileProviderBasicRemote { extension OneDriveFileProvider: FileProviderOperations { - open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? { let path = (atPath as NSString).appendingPathComponent(folderName) + "/" return doOperation(.create(path: path), completionHandler: completionHandler) } - open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler) } - open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler) } - open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? { return doOperation(.remove(path: path), completionHandler: completionHandler) } - fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else { return nil } + + let progress = Progress(totalUnitCount: 1) + progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + guard let sourcePath = operation.source else { return nil } let destPath = operation.destination var request = URLRequest(url: url(of: sourcePath)) @@ -299,15 +307,24 @@ extension OneDriveFileProvider: FileProviderOperations { if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) { serverError = FileProviderOneDriveError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) } + if serverError == nil && error == nil { + progress.completedUnitCount = 1 + } else { + progress.cancel() + } completionHandler?(serverError ?? error) self.delegateNotify(operation, error: serverError ?? error) }) task.taskDescription = operation.json + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() - return RemoteOperationHandle(operationType: operation, tasks: [task]) + return progress } - open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { // check file is not a folder guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else { dispatch_queue.async { @@ -323,20 +340,34 @@ extension OneDriveFileProvider: FileProviderOperations { return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler) } - open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } + + var progress = Progress(parent: nil, userInfo: nil) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + var request = URLRequest(url: self.url(of: path, modifier: "content")) request.set(httpAuthentication: credential, with: .oAuth2) let task = session.downloadTask(with: request) - completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = completionHandler + completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in + if error != nil { + progress.cancel() + } + completionHandler?(error) + } downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else { let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1) let errorData : Data? = nil //Data(contentsOf: cacheURL) // TODO: Figure out how to get error response data for the error description let serverError : FileProviderOneDriveError? = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil + if serverError != nil { + progress.cancel() + } completionHandler?(serverError) return } @@ -348,13 +379,19 @@ extension OneDriveFileProvider: FileProviderOperations { } } task.taskDescription = opType.json + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress) + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() - return RemoteOperationHandle(operationType: opType, tasks: [task]) + return progress } } extension OneDriveFileProvider: FileProviderReadWrite { - open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { + open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { if length == 0 || offset < 0 { dispatch_queue.async { completionHandler(Data(), nil) @@ -363,19 +400,31 @@ extension OneDriveFileProvider: FileProviderReadWrite { } let opType = FileOperationType.fetch(path: path) + + var progress = Progress(parent: nil, userInfo: nil) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + var request = URLRequest(url: self.url(of: path, modifier: "content")) request.httpMethod = "GET" request.set(httpAuthentication: credential, with: .oAuth2) request.set(rangeWithOffset: offset, length: length) let task = session.downloadTask(with: request) completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in + if error != nil { + progress.cancel() + } completionHandler(nil, error) } - downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in + downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else { let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1) let errorData : Data? = nil //Data(contentsOf: cacheURL) // TODO: Figure out how to get error response data for the error description let serverError : FileProviderOneDriveError? = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil + if serverError != nil { + progress.cancel() + } completionHandler(nil, serverError) return } @@ -387,11 +436,17 @@ extension OneDriveFileProvider: FileProviderReadWrite { } } task.taskDescription = opType.json + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress) + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() - return RemoteOperationHandle(operationType: opType, tasks: [task]) + return progress } - open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.modify(path: path) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil diff --git a/Sources/OneDriveHelper.swift b/Sources/OneDriveHelper.swift index 25313bd..fececc3 100644 --- a/Sources/OneDriveHelper.swift +++ b/Sources/OneDriveHelper.swift @@ -114,7 +114,7 @@ internal extension OneDriveFileProvider { task.resume() } - func upload_simple(_ targetPath: String, data: Data? = nil , localFile: URL? = nil, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + func upload_simple(_ targetPath: String, data: Data? = nil, localFile: URL? = nil, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? { let size = data?.count ?? (try? localFile?.resourceValues(forKeys: [.fileSizeKey]))??.fileSize ?? -1 if size > 100 * 1024 * 1024 { let error = FileProviderOneDriveError(code: .payloadTooLarge, path: targetPath, errorDescription: nil) @@ -122,6 +122,13 @@ internal extension OneDriveFileProvider { self.delegateNotify(.create(path: targetPath), error: error) return nil } + + var progress = Progress(parent: nil, userInfo: nil) + progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + progress.totalUnitCount = Int64(size) + let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail" let url = self.url(of: targetPath, modifier: "content\(queryStr)") var request = URLRequest(url: url) @@ -143,15 +150,27 @@ internal extension OneDriveFileProvider { // We can't fetch server result from delegate! responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: nil) } + if !(responseError == nil && error == nil) { + progress.cancel() + } completionHandler?(responseError ?? error) self?.delegateNotify(.create(path: targetPath), error: responseError ?? error) } task.taskDescription = operation.json + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() - return RemoteOperationHandle(operationType: operation, tasks: [task]) + return progress } - func search(_ startPath: String = "", query: String, next: URL? = nil, foundItem:@escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) { + func search(_ startPath: String = "", query: String, next: URL? = nil, progress: Progress, foundItem: @escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) { + if progress.isCancelled { + return + } + let url: URL let q = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! url = next ?? self.url(of: startPath, modifier: "view.search?q=\(q)") @@ -172,8 +191,8 @@ internal extension OneDriveFileProvider { } } let next: URL? = (json["@odata.nextLink"] as? String).flatMap { URL(string: $0) } - if let next = next { - self.search(startPath, query: query, next: next, foundItem: foundItem, completionHandler: completionHandler) + if !progress.isCancelled, let next = next { + self.search(startPath, query: query, next: next, progress: progress, foundItem: foundItem, completionHandler: completionHandler) } else { completionHandler(responseError ?? error) } @@ -181,7 +200,11 @@ internal extension OneDriveFileProvider { } } completionHandler(responseError ?? error) - }) + }) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() } } diff --git a/Sources/RemoteSession.swift b/Sources/RemoteSession.swift index a66209d..209fdc0 100644 --- a/Sources/RemoteSession.swift +++ b/Sources/RemoteSession.swift @@ -8,67 +8,6 @@ import Foundation -/// Allows to get progress or cancel an in-progress operation, for remote, `URLSession` based providers. -/// This class keeps strong reference to tasks. -open class RemoteOperationHandle: OperationHandle { - - internal var tasks: [URLSessionTask] - - open private(set) var operationType: FileOperationType - - init(operationType: FileOperationType, tasks: [URLSessionTask]) { - self.operationType = operationType - self.tasks = tasks - } - - internal func add(task: URLSessionTask) { - tasks.append(task) - } - - internal func reape() { - self.tasks = tasks.filter { $0.state != .completed } - } - - open var bytesSoFar: Int64 { - return tasks.reduce(0) { - switch $1 { - case let task as URLSessionUploadTask: - return $0 + task.countOfBytesSent - case let task as FileProviderStreamTask: - return $0 + task.countOfBytesSent + task.countOfBytesReceived - default: - return $0 + $1.countOfBytesReceived - } - } - } - - open var totalBytes: Int64 { - return tasks.reduce(0) { - switch $1 { - case let task as URLSessionUploadTask: - return $0 + task.countOfBytesExpectedToSend - case let task as FileProviderStreamTask: - return $0 + task.countOfBytesExpectedToSend + task.countOfBytesExpectedToReceive - default: - return $0 + $1.countOfBytesExpectedToReceive - } - } - } - - open func cancel() -> Bool { - var canceled = false - for taskbox in tasks { - taskbox.cancel() - canceled = true - } - return canceled - } - - open var inProgress: Bool { - return tasks.reduce(false) { $0 || $1.state == .running } - } -} - /// A protocol defines properties for errors returned by HTTP/S based providers. /// Including Dropbox, OneDrive and WebDAV. public protocol FileProviderHTTPError: Error, CustomStringConvertible { @@ -140,8 +79,49 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession self.credential = fileProvider.credential } + open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if let progress = context?.load(as: Progress.self), let newVal = change?[.newKey] as? Int64 { + switch keyPath ?? "" { + case #keyPath(URLSessionTask.countOfBytesReceived): + progress.completedUnitCount = newVal + if let startTime = progress.userInfo[ProgressUserInfoKey.startingTimeKey] as? Date, let task = object as? URLSessionTask { + let elapsed = Date().timeIntervalSince(startTime) + let throughput = Double(newVal) / elapsed + progress.setUserInfoObject(NSNumber(value: throughput), forKey: .throughputKey) + if task.countOfBytesExpectedToReceive > 0 { + let remain = task.countOfBytesExpectedToReceive - task.countOfBytesReceived + let estimatedTimeRemaining = Double(remain) / elapsed + progress.setUserInfoObject(NSNumber(value: estimatedTimeRemaining), forKey: .estimatedTimeRemainingKey) + } + } + case #keyPath(URLSessionTask.countOfBytesSent): + progress.completedUnitCount = newVal + if let startTime = progress.userInfo[ProgressUserInfoKey.startingTimeKey] as? Date, let task = object as? URLSessionTask { + let elapsed = Date().timeIntervalSince(startTime) + let throughput = Double(newVal) / elapsed + progress.setUserInfoObject(NSNumber(value: throughput), forKey: .throughputKey) + if task.countOfBytesExpectedToSend > 0 { + let remain = task.countOfBytesExpectedToSend - task.countOfBytesSent + let estimatedTimeRemaining = Double(remain) / elapsed + progress.setUserInfoObject(NSNumber(value: estimatedTimeRemaining), forKey: .estimatedTimeRemainingKey) + } + } + case #keyPath(URLSessionTask.countOfBytesExpectedToReceive), #keyPath(URLSessionTask.countOfBytesExpectedToSend): + progress.totalUnitCount = newVal + default: + break + } + } + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + } + // codebeat:disable[ARITY] public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived)) + task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive)) + task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent)) + task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived)) + if !(error == nil && task is URLSessionDownloadTask) { let completionHandler = completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] ?? nil completionHandler?(error) diff --git a/Sources/SMBFileProvider.swift b/Sources/SMBFileProvider.swift index bc229fb..ee445d7 100644 --- a/Sources/SMBFileProvider.swift +++ b/Sources/SMBFileProvider.swift @@ -74,53 +74,54 @@ class SMBFileProvider: FileProvider, FileProviderMonitor { open weak var fileOperationDelegate: FileOperationDelegate? - open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? { NotImplemented() return nil } - open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { NotImplemented() return nil } - open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { NotImplemented() return nil } - open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? { NotImplemented() return nil } - open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { NotImplemented() return nil } - open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? { NotImplemented() return nil } - open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { + open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { NotImplemented() return nil } - open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { + open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { NotImplemented() return nil } - open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { NotImplemented() return nil } - open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) { + open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) -> Progress? { NotImplemented() + return nil } open func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) { diff --git a/Sources/WebDAVFileProvider.swift b/Sources/WebDAVFileProvider.swift index 0f24eef..69329d7 100644 --- a/Sources/WebDAVFileProvider.swift +++ b/Sources/WebDAVFileProvider.swift @@ -171,7 +171,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote { request.set(contentType: .xml) request.httpBody = "\n\n\(WebDavFileObject.propString(including))\n".data(using: .utf8) request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length") - runDataTask(with: request, operationHandle: RemoteOperationHandle(operationType: opType, tasks: []), completionHandler: { (data, response, error) in + runDataTask(with: request, operation: opType, completionHandler: { (data, response, error) in var responseError: FileProviderWebDavError? if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url) @@ -258,7 +258,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote { }) } - open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { + open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? { let url = self.url(of: path) var request = URLRequest(url: url) request.httpMethod = "PROPFIND" @@ -266,7 +266,8 @@ open class WebDAVFileProvider: FileProviderBasicRemote { request.set(httpAuthentication: credential, with: credentialType) request.set(contentType: .xml) request.httpBody = "\n\n".data(using: .utf8) - runDataTask(with: request, completionHandler: { (data, response, error) in + let progress = Progress(parent: nil, userInfo: nil) + let task = session.dataTask(with: request) { (data, response, error) in // FIXME: paginating results var responseError: FileProviderWebDavError? if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { @@ -282,13 +283,20 @@ open class WebDAVFileProvider: FileProviderBasicRemote { } fileObjects.append(fileObject) + progress.completedUnitCount = Int64(fileObjects.count) foundItemHandler?(fileObject) } completionHandler(fileObjects, responseError ?? error) return } completionHandler([], responseError ?? error) - }) + } + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) + task.resume() + return progress } open func isReachable(completionHandler: @escaping (Bool) -> Void) { @@ -310,30 +318,16 @@ open class WebDAVFileProvider: FileProviderBasicRemote { extension WebDAVFileProvider: FileProviderOperations { @discardableResult - open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/") guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } - let url = self.url(of: atPath).appendingPathComponent(folderName, isDirectory: true) - var request = URLRequest(url: url) - request.httpMethod = "MKCOL" - request.set(httpAuthentication: credential, with: credentialType) - let task = session.dataTask(with: request, completionHandler: { (data, response, error) in - var responseError: FileProviderWebDavError? - if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { - responseError = FileProviderWebDavError(code: rCode, path: url.relativePath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url) - } - completionHandler?(responseError ?? error) - self.delegateNotify(opType, error: responseError ?? error) - }) - task.taskDescription = opType.json - task.resume() - return RemoteOperationHandle(operationType: opType, tasks: [task]) + return self.doOperation(operation: opType, overwrite: false, completionHandler: completionHandler) } @discardableResult - open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.move(source: path, destination: toPath) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil @@ -342,7 +336,7 @@ extension WebDAVFileProvider: FileProviderOperations { } @discardableResult - open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.copy(source: path, destination: toPath) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil @@ -351,7 +345,7 @@ extension WebDAVFileProvider: FileProviderOperations { } @discardableResult - open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.remove(path: path) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil @@ -359,7 +353,7 @@ extension WebDAVFileProvider: FileProviderOperations { return self.doOperation(operation: opType, completionHandler: completionHandler) } - fileprivate func doOperation(operation opType: FileOperationType, overwrite: Bool? = nil, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + fileprivate func doOperation(operation opType: FileOperationType, overwrite: Bool? = nil, completionHandler: SimpleCompletionHandler) -> Progress? { let source = opType.source! let sourceURL = self.url(of: source) var request = URLRequest(url: sourceURL) @@ -367,6 +361,8 @@ extension WebDAVFileProvider: FileProviderOperations { request.setValue(url(of:dest).absoluteString, forHTTPHeaderField: "Destination") } switch opType { + case .create: + request.httpMethod = "MKCOL" case .copy: request.httpMethod = "COPY" case .move: @@ -377,6 +373,11 @@ extension WebDAVFileProvider: FileProviderOperations { return nil } + let progress = Progress(totalUnitCount: 1) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + request.set(httpAuthentication: credential, with: credentialType) if let overwrite = overwrite, !overwrite { request.setValue("F", forHTTPHeaderField: "Overwrite") @@ -392,9 +393,17 @@ extension WebDAVFileProvider: FileProviderOperations { for xresponse in xresponses where (xresponse.status ?? 0) >= 300 { let error = FileProviderWebDavError(code: code, path: source, errorDescription: String(data: data, encoding: .utf8), url: sourceURL) completionHandler?(error) + progress.cancel() } } } + + if responseError == nil && error == nil { + progress.completedUnitCount = 1 + } else { + progress.cancel() + } + if (response as? HTTPURLResponse)?.statusCode ?? 0 != FileProviderHTTPErrorCode.multiStatus.rawValue { completionHandler?(responseError ?? error) } @@ -402,12 +411,16 @@ extension WebDAVFileProvider: FileProviderOperations { self.delegateNotify(opType, error: responseError ?? error) }) task.taskDescription = opType.json + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() - return RemoteOperationHandle(operationType: opType, tasks: [task]) + return progress } @discardableResult - open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { // check file is not a folder guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else { dispatch_queue.async { @@ -420,6 +433,13 @@ extension WebDAVFileProvider: FileProviderOperations { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } + + var progress = Progress(parent: nil, userInfo: nil) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + progress.totalUnitCount = localFile.fileSize + let url = self.url(of:toPath) var request = URLRequest(url: url) if !overwrite { @@ -434,25 +454,44 @@ extension WebDAVFileProvider: FileProviderOperations { // We can't fetch server result from delegate! responseError = FileProviderWebDavError(code: rCode, path: toPath, errorDescription: nil, url: url) } + if !(responseError == nil && error == nil) { + progress.cancel() + } completionHandler?(responseError ?? error) self?.delegateNotify(.create(path: toPath), error: responseError ?? error) } task.taskDescription = opType.json + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() - return RemoteOperationHandle(operationType: opType, tasks: [task]) + return progress } @discardableResult - open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } + + var progress = Progress(parent: nil, userInfo: nil) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + let url = self.url(of:path) var request = URLRequest(url: url) request.set(httpAuthentication: credential, with: credentialType) let task = session.downloadTask(with: request) - completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = completionHandler + completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in + if error != nil { + progress.cancel() + } + completionHandler?(error) + } downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else { let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1) @@ -468,14 +507,20 @@ extension WebDAVFileProvider: FileProviderOperations { } } task.taskDescription = opType.json + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress) + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() - return RemoteOperationHandle(operationType: opType, tasks: [task]) + return progress } } extension WebDAVFileProvider: FileProviderReadWrite { @discardableResult - open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { + open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { if length == 0 || offset < 0 { dispatch_queue.async { completionHandler(Data(), nil) @@ -484,17 +529,26 @@ extension WebDAVFileProvider: FileProviderReadWrite { } let opType = FileOperationType.fetch(path: path) + + var progress = Progress(parent: nil, userInfo: nil) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + let url = self.url(of: path) var request = URLRequest(url: url) request.set(httpAuthentication: credential, with: credentialType) request.set(rangeWithOffset: offset, length: length) let task = session.downloadTask(with: request) - let handle = RemoteOperationHandle(operationType: opType, tasks: [task]) completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in + if error != nil { + progress.cancel() + } + completionHandler(nil, error) } - downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in + downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] tempURL in guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else { let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1) let serverError : FileProviderWebDavError? = code != nil ? FileProviderWebDavError(code: code!, path: path, errorDescription: code?.description, url: url) : nil @@ -503,7 +557,7 @@ extension WebDAVFileProvider: FileProviderReadWrite { } do { let data = try Data(contentsOf: tempURL) - self.dispatch_queue.async { + (self?.dispatch_queue ?? DispatchQueue.global()).async { completionHandler(data, nil) } } catch let e { @@ -511,16 +565,29 @@ extension WebDAVFileProvider: FileProviderReadWrite { } } task.taskDescription = opType.json + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress) + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() - return handle + return progress } @discardableResult - open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { let opType = FileOperationType.modify(path: path) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } + + var progress = Progress(parent: nil, userInfo: nil) + progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + progress.totalUnitCount = Int64(data?.count ?? 0) + // FIXME: lock destination before writing process let url = atomically ? self.url(of: path).appendingPathExtension("tmp") : self.url(of: path) var request = URLRequest(url: url) @@ -536,12 +603,20 @@ extension WebDAVFileProvider: FileProviderReadWrite { // We can't fetch server result from delegate! responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: nil, url: url) } + if !(responseError == nil && error == nil) { + progress.cancel() + } completionHandler?(responseError ?? error) self?.delegateNotify(opType, error: responseError ?? error) } task.taskDescription = opType.json + task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) task.resume() - return RemoteOperationHandle(operationType: opType, tasks: [task]) + return progress } /*