diff --git a/FileProvider.podspec b/FileProvider.podspec index 4a0effc..473fe13 100644 --- a/FileProvider.podspec +++ b/FileProvider.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = "FileProvider" - s.version = "0.5.2" + s.version = "0.6.0" s.summary = "FileManager replacement for Local and Remote (WebDAV/Dropbox/SMB2) files on iOS and MacOS." # This description is used to generate tags and improve search results. diff --git a/FileProvider.xcodeproj/project.pbxproj b/FileProvider.xcodeproj/project.pbxproj index d32ce54..cf08dec 100644 --- a/FileProvider.xcodeproj/project.pbxproj +++ b/FileProvider.xcodeproj/project.pbxproj @@ -7,9 +7,9 @@ objects = { /* Begin PBXBuildFile section */ - 7902C0861D61B56D00564440 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7902C0851D61B56D00564440 /* SessionDelegate.swift */; }; - 7902C0871D61B67100564440 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7902C0851D61B56D00564440 /* SessionDelegate.swift */; }; - 7902C0881D61B67100564440 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7902C0851D61B56D00564440 /* SessionDelegate.swift */; }; + 7902C0861D61B56D00564440 /* RemoteSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7902C0851D61B56D00564440 /* RemoteSession.swift */; }; + 7902C0871D61B67100564440 /* RemoteSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7902C0851D61B56D00564440 /* RemoteSession.swift */; }; + 7902C0881D61B67100564440 /* RemoteSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7902C0851D61B56D00564440 /* RemoteSession.swift */; }; 7924B1931D89DAE000589DB7 /* AEXML.h in Headers */ = {isa = PBXBuildFile; fileRef = 7924B18C1D89DAE000589DB7 /* AEXML.h */; }; 7924B1941D89DAE000589DB7 /* AEXML.h in Headers */ = {isa = PBXBuildFile; fileRef = 7924B18C1D89DAE000589DB7 /* AEXML.h */; }; 7924B1951D89DAE000589DB7 /* AEXML.h in Headers */ = {isa = PBXBuildFile; fileRef = 7924B18C1D89DAE000589DB7 /* AEXML.h */; }; @@ -97,7 +97,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 7902C0851D61B56D00564440 /* SessionDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionDelegate.swift; sourceTree = ""; }; + 7902C0851D61B56D00564440 /* RemoteSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteSession.swift; sourceTree = ""; }; 7924B18C1D89DAE000589DB7 /* AEXML.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AEXML.h; sourceTree = ""; }; 7924B18D1D89DAE000589DB7 /* Document.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; 7924B18E1D89DAE000589DB7 /* Element.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Element.swift; sourceTree = ""; }; @@ -213,7 +213,7 @@ 799396941D48C02300086753 /* FileProvider.h */, 799396951D48C02300086753 /* FileProvider.swift */, 799396961D48C02300086753 /* LocalFileProvider.swift */, - 7902C0851D61B56D00564440 /* SessionDelegate.swift */, + 7902C0851D61B56D00564440 /* RemoteSession.swift */, 7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */, 799396971D48C02300086753 /* SMBClient.swift */, 799396981D48C02300086753 /* SMBFileProvider.swift */, @@ -413,7 +413,7 @@ 794C220A1D5893F800EC49B8 /* SMB2Notification.swift in Sources */, 799396D11D48C02300086753 /* SMB2SetInfo.swift in Sources */, 7924B1961D89DAE000589DB7 /* Document.swift in Sources */, - 7902C0861D61B56D00564440 /* SessionDelegate.swift in Sources */, + 7902C0861D61B56D00564440 /* RemoteSession.swift in Sources */, 799396CE1D48C02300086753 /* SMB2Session.swift in Sources */, 799396E01D48C02300086753 /* WebDAVFileProvider.swift in Sources */, 799396DA1D48C02300086753 /* SMBErrorType.swift in Sources */, @@ -447,7 +447,7 @@ 794C220B1D5893F800EC49B8 /* SMB2Notification.swift in Sources */, 799396D21D48C02300086753 /* SMB2SetInfo.swift in Sources */, 7924B1971D89DAE000589DB7 /* Document.swift in Sources */, - 7902C0871D61B67100564440 /* SessionDelegate.swift in Sources */, + 7902C0871D61B67100564440 /* RemoteSession.swift in Sources */, 799396CF1D48C02300086753 /* SMB2Session.swift in Sources */, 799396E11D48C02300086753 /* WebDAVFileProvider.swift in Sources */, 799396DB1D48C02300086753 /* SMBErrorType.swift in Sources */, @@ -481,7 +481,7 @@ 794C220C1D5893F800EC49B8 /* SMB2Notification.swift in Sources */, 799396D31D48C02300086753 /* SMB2SetInfo.swift in Sources */, 7924B1981D89DAE000589DB7 /* Document.swift in Sources */, - 7902C0881D61B67100564440 /* SessionDelegate.swift in Sources */, + 7902C0881D61B67100564440 /* RemoteSession.swift in Sources */, 799396D01D48C02300086753 /* SMB2Session.swift in Sources */, 799396E21D48C02300086753 /* WebDAVFileProvider.swift in Sources */, 799396DC1D48C02300086753 /* SMBErrorType.swift in Sources */, @@ -568,7 +568,6 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; @@ -628,7 +627,6 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; @@ -681,7 +679,6 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; @@ -742,7 +739,6 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; @@ -795,7 +791,6 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; @@ -830,7 +825,7 @@ SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 9.0; + TVOS_DEPLOYMENT_TARGET = 10.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -855,7 +850,6 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; @@ -882,7 +876,7 @@ SDKROOT = appletvos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 9.0; + TVOS_DEPLOYMENT_TARGET = 10.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; diff --git a/Sources/DropboxFileProvider.swift b/Sources/DropboxFileProvider.swift index ae25d12..1fb3c84 100644 --- a/Sources/DropboxFileProvider.swift +++ b/Sources/DropboxFileProvider.swift @@ -66,7 +66,7 @@ open class DropboxFileProvider: NSObject, FileProviderBasic { let task = session.dataTask(with: request, completionHandler: { (data, response, error) in if let response = response as? HTTPURLResponse { defer { - self.delegateNotify(FileOperation.create(path: path), error: error) + self.delegateNotify(.create(path: path), error: error) } let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) let dbError: FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: String.Encoding.utf8)) : nil @@ -104,31 +104,31 @@ open class DropboxFileProvider: NSObject, FileProviderBasic { extension DropboxFileProvider: FileProviderOperations { - - public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) { + + public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let path = (atPath as NSString).appendingPathComponent(folderName) + "/" - doOperation(.create(path: path), completionHandler: completionHandler) + return doOperation(.create(path: path), completionHandler: completionHandler) } - public func create(file fileAttribs: FileObject, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) { - self.writeContents(path: path, contents: data ?? Data(), completionHandler: completionHandler) + public func create(file fileAttribs: FileObject, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + return self.writeContents(path: path, contents: data ?? Data(), completionHandler: completionHandler) } - public func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) { - doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler) + public func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler) } - public func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) { - doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler) + public func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler) } - public func removeItem(path: String, completionHandler: SimpleCompletionHandler) { - doOperation(.remove(path: path), completionHandler: completionHandler) + public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + return doOperation(.remove(path: path), completionHandler: completionHandler) } - fileprivate func doOperation(_ operation: FileOperation, completionHandler: SimpleCompletionHandler) { + fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else { - return + return nil } let url: String var path: String?, fromPath: String?, toPath: String? @@ -145,12 +145,12 @@ extension DropboxFileProvider: FileProviderOperations { fromPath = fp toPath = tp case .modify(path: let p): - return + return nil case .remove(path: let p): url = "https://api.dropboxapi.com/2/files/delete" path = p case .link(link: _, target: _): - return + return nil } var request = URLRequest(url: URL(string: url)!) request.httpMethod = "POST" @@ -177,23 +177,24 @@ extension DropboxFileProvider: FileProviderOperations { completionHandler?(error) }) task.resume() + return RemoteOperationHandle(tasks: [task]) } - public func copyItem(localFile: URL, to toPath: String, completionHandler: SimpleCompletionHandler) { + public func copyItem(localFile: URL, to toPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .copy(source: localFile.absoluteString, destination: toPath)) ?? true == true else { - return + return nil } guard let data = try? Data(contentsOf: localFile) else { let error = throwError(localFile.absoluteString, code: URLError.fileDoesNotExist as FoundationErrorEnum) completionHandler?(error) - return + return nil } - upload_simple(toPath, data: data, overwrite: true, operation: .copy(source: localFile.absoluteString, destination: toPath), completionHandler: completionHandler) + return upload_simple(toPath, data: data, overwrite: true, operation: .copy(source: localFile.absoluteString, destination: toPath), completionHandler: completionHandler) } - public func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) { + public func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .copy(source: path, destination: destURL.absoluteString)) ?? true == true else { - return + return nil } let url = URL(string: "https://content.dropboxapi.com/2/files/download")! var request = URLRequest(url: url) @@ -218,15 +219,16 @@ extension DropboxFileProvider: FileProviderOperations { }) task.taskDescription = dictionaryToJSON(["type": "Copy" as NSString, "source": path as NSString, "dest": destURL.absoluteString as NSString]) task.resume() + return RemoteOperationHandle(tasks: [task]) } } extension DropboxFileProvider: FileProviderReadWrite { - public func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) { - self.contents(path: path, offset: 0, length: -1, completionHandler: completionHandler) + public func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { + return self.contents(path: path, offset: 0, length: -1, completionHandler: completionHandler) } - public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) { + public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { let url = URL(string: "https://content.dropboxapi.com/2/files/download")! var request = URLRequest(url: url) request.httpMethod = "GET" @@ -248,14 +250,15 @@ extension DropboxFileProvider: FileProviderReadWrite { completionHandler(data, error) }) task.resume() + return RemoteOperationHandle(tasks: [task]) } - public func writeContents(path: String, contents data: Data, atomically: Bool = false, completionHandler: SimpleCompletionHandler) { + public func writeContents(path: String, contents data: Data, atomically: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .modify(path: path)) ?? true == true else { - return + return nil } // FIXME: remove 150MB restriction - upload_simple(path, data: data, overwrite: true, operation: .modify(path: path), completionHandler: completionHandler) + return upload_simple(path, data: data, overwrite: true, operation: .modify(path: path), completionHandler: completionHandler) } public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { diff --git a/Sources/DropboxHelper.swift b/Sources/DropboxHelper.swift index 040e109..896c1ae 100644 --- a/Sources/DropboxHelper.swift +++ b/Sources/DropboxHelper.swift @@ -80,7 +80,7 @@ internal extension DropboxFileProvider { task.resume() } - func upload_simple(_ targetPath: String, data: Data, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperation, completionHandler: SimpleCompletionHandler) { + func upload_simple(_ targetPath: String, data: Data, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? { assert(data.count < 150*1024*1024, "Maximum size of allowed size to upload is 150MB") var requestDictionary = [String: AnyObject]() let url: URL @@ -123,6 +123,7 @@ internal extension DropboxFileProvider { } task.taskDescription = dictionaryToJSON(dic) task.resume() + return RemoteOperationHandle(tasks: [task]) } 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)) { @@ -185,7 +186,7 @@ internal extension DropboxFileProvider { return DropboxFileObject(name: name, path: path, size: size, serverTime: serverTime, modifiedDate: modifiedDate, fileType: isDirectory ? .directory : .regular, isReadOnly: isReadonly, id: id, rev: rev) } - func delegateNotify(_ operation: FileOperation, error: Error?) { + func delegateNotify(_ operation: FileOperationType, error: Error?) { DispatchQueue.main.async(execute: { if error == nil { self.delegate?.fileproviderSucceed(self, operation: operation) diff --git a/Sources/FileProvider.swift b/Sources/FileProvider.swift index 177ec2f..65b2b80 100644 --- a/Sources/FileProvider.swift +++ b/Sources/FileProvider.swift @@ -117,20 +117,30 @@ public protocol FileProviderBasic: class { public protocol FileProviderOperations: FileProviderBasic { var fileOperationDelegate : FileOperationDelegate? { get set } - func create(folder: String, at: String, completionHandler: SimpleCompletionHandler) - func create(file: FileObject, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) - func moveItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) - func copyItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) - func removeItem(path: String, completionHandler: SimpleCompletionHandler) + @discardableResult + func create(folder: String, at: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? + @discardableResult + func create(file: FileObject, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? + @discardableResult + func moveItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? + @discardableResult + func copyItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? + @discardableResult + func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? - func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) - func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) + @discardableResult + func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? + @discardableResult + func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? } public protocol FileProviderReadWrite: FileProviderBasic { - func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) - func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) - func writeContents(path: String, contents: Data, atomically: Bool, completionHandler: SimpleCompletionHandler) + @discardableResult + func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? + @discardableResult + func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? + @discardableResult + func writeContents(path: String, contents: Data, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) } @@ -236,7 +246,7 @@ extension FileProviderBasic { return NSError(domain: domain, code: code.rawValue, userInfo: [NSURLErrorFailingURLErrorKey: fileURL, NSURLErrorFailingURLStringErrorKey: fileURL.absoluteString]) } - internal func NotImplemented() { + internal func NotImplemented() -> Never { assert(false, "method not implemented") } @@ -272,7 +282,7 @@ public protocol ExtendedFileProvider: FileProvider { func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void)) } -public enum FileOperation: CustomStringConvertible { +public enum FileOperationType: CustomStringConvertible { case create (path: String) case copy (source: String, destination: String) case move (source: String, destination: String) @@ -301,22 +311,37 @@ public enum FileOperation: CustomStringConvertible { case .link(link: _, target: _): return "Linking" } } - +} + +@objc +public protocol OperationHandle { + var progress: Float { get } + var bytesSoFar: Int64 { get } + var totalBytes: Int64 { get } + var inProgress: Bool { get } + func cancel() +} + +internal class Weak { + weak var value : T? + init (_ value: T) { + self.value = value + } } public protocol FileProviderDelegate: class { - func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperation) - func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperation) - func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperation, progress: Float) + func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperationType) + func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperationType) + func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperationType, progress: Float) } public protocol FileOperationDelegate: class { /// fileProvider(_:shouldOperate:) gives the delegate an opportunity to filter the file operation. Returning true from this method will allow the copy to happen. Returning false from this method causes the item in question to be skipped. If the item skipped was a directory, no children of that directory will be subject of the operation, nor will the delegate be notified of those children. - func fileProvider(_ fileProvider: FileProviderOperations, shouldDoOperation operation: FileOperation) -> Bool + func fileProvider(_ fileProvider: FileProviderOperations, shouldDoOperation operation: FileOperationType) -> Bool /// fileProvider(_:shouldProceedAfterError:copyingItemAtPath:toPath:) gives the delegate an opportunity to recover from or continue copying after an error. If an error occurs, the error object will contain an ErrorType indicating the problem. The source path and destination paths are also provided. If this method returns true, the FileProvider instance will continue as if the error had not occurred. If this method returns false, the NSFileManager instance will stop copying, return false from copyItemAtPath:toPath:error: and the error will be provied there. - func fileProvider(_ fileProvider: FileProviderOperations, shouldProceedAfterError error: Error, operation: FileOperation) -> Bool + func fileProvider(_ fileProvider: FileProviderOperations, shouldProceedAfterError error: Error, operation: FileOperationType) -> Bool } // THESE ARE METHODS TO PROVIDE COMPATIBILITY WITH SWIFT 2.3 SIMOULTANIOUSLY! diff --git a/Sources/LocalFileProvider.swift b/Sources/LocalFileProvider.swift index fcd31f5..4413601 100644 --- a/Sources/LocalFileProvider.swift +++ b/Sources/LocalFileProvider.swift @@ -102,7 +102,9 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { open weak var fileOperationDelegate : FileOperationDelegate? - @objc(createWithFolder:at:completionHandler:) open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) { + @objc(createWithFolder:at:completionHandler:) + @discardableResult + open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { operation_queue.async { do { try self.opFileManager.createDirectory(at: self.absoluteURL(atPath).appendingPathComponent(folderName), withIntermediateDirectories: true, attributes: [:]) @@ -117,9 +119,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { }) } } + return nil } - open func create(file fileAttribs: FileObject, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) { + @discardableResult + open func create(file fileAttribs: FileObject, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? { operation_queue.async { let fileURL = self.absoluteURL(atPath).appendingPathComponent(fileAttribs.name) var attributes = [String : Any]() @@ -148,9 +152,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { }) } } + return nil } - open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) { + @discardableResult + open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { // FIXME: progress operation_queue.async { if !overwrite && self.fileManager.fileExists(atPath: self.absoluteURL(toPath).path) { @@ -170,9 +176,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { }) } } + return nil } - open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) { + @discardableResult + open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { // FIXME: progress, for files > 100mb, monitor file by another thread, for dirs check copied items count operation_queue.async { if !overwrite && self.fileManager.fileExists(atPath: self.absoluteURL(toPath).path) { @@ -192,9 +200,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { }) } } + return nil } - open func removeItem(path: String, completionHandler: SimpleCompletionHandler) { + @discardableResult + open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { operation_queue.async { do { try self.opFileManager.removeItem(at: self.absoluteURL(path)) @@ -209,9 +219,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { }) } } + return nil } - open func copyItem(localFile: URL, to toPath: String, completionHandler: SimpleCompletionHandler) { + @discardableResult + open func copyItem(localFile: URL, to toPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { operation_queue.async { do { try self.opFileManager.copyItem(at: localFile, to: self.absoluteURL(toPath)) @@ -226,9 +238,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { }) } } + return nil } - open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) { + @discardableResult + open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { operation_queue.async { do { try self.opFileManager.copyItem(at: self.absoluteURL(path), to: toLocalURL) @@ -243,16 +257,20 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { }) } } + return nil } - open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) { + @discardableResult + open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { dispatch_queue.async { let data = self.fileManager.contents(atPath: self.absoluteURL(path).path) completionHandler(data, nil) } + return nil } - open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) { + @discardableResult + open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { // Unfortunatlely there is no method provided in NSFileManager to read a segment of file. // So we have to fallback to POSIX provided methods dispatch_queue.async { @@ -279,15 +297,18 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { completionHandler(data, nil) } } + return nil } - open func writeContents(path: String, contents data: Data, atomically: Bool, completionHandler: SimpleCompletionHandler) { + @discardableResult + open func writeContents(path: String, contents data: Data, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { operation_queue.async { try? data.write(to: self.absoluteURL(path), options: atomically ? [.atomic] : []) DispatchQueue.main.async(execute: { self.delegate?.fileproviderSucceed(self, operation: .modify(path: path)) }) } + return nil } open func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { diff --git a/Sources/SessionDelegate.swift b/Sources/RemoteSession.swift similarity index 84% rename from Sources/SessionDelegate.swift rename to Sources/RemoteSession.swift index fc48de0..d7f0b34 100644 --- a/Sources/SessionDelegate.swift +++ b/Sources/RemoteSession.swift @@ -8,6 +8,62 @@ import Foundation +open class RemoteOperationHandle: OperationHandle { + + internal var tasks: [Weak] + + private var _inProgress = false + open var inProgress: Bool { + return _inProgress + } + + init(tasks: [URLSessionTask]) { + self.tasks = tasks.map { Weak($0) } + _inProgress = true + } + + internal func add(task: URLSessionTask) { + tasks.append(Weak(task)) + } + + private func reape() { + self.tasks = tasks.filter { $0.value != nil } + } + + open var progress: Float { + let bytesSoFar = self.bytesSoFar + let totalBytes = self.totalBytes + return totalBytes > 0 ? Float(Double(bytesSoFar) / Double(totalBytes)) : Float.nan + } + + open var bytesSoFar: Int64 { + return tasks.reduce(0) { + if let task = $1.value as? URLSessionUploadTask { + return $0 + task.countOfBytesSent + } else { + return $0 + ($1.value?.countOfBytesReceived ?? 0) + } + } + } + + open var totalBytes: Int64 { + return tasks.reduce(0) { + if let task = $1.value as? URLSessionUploadTask { + return $0 + task.countOfBytesExpectedToSend + } else { + return $0 + ($1.value?.countOfBytesExpectedToSend ?? 0) + } + } + } + + open func cancel() { + for taskbox in tasks { + taskbox.value?.cancel() + } + _inProgress = false + } +} + class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDelegate { weak var fileProvider: FileProvider? @@ -38,7 +94,7 @@ class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDeleg return } let dest = json["dest"] as? String - let op : FileOperation + let op : FileOperationType switch type { case "Create": op = .create(path: source) diff --git a/Sources/SMBFileProvider.swift b/Sources/SMBFileProvider.swift index 202e26e..e61ac21 100644 --- a/Sources/SMBFileProvider.swift +++ b/Sources/SMBFileProvider.swift @@ -31,9 +31,6 @@ open class SMBFileProvider: FileProvider, FileProviderMonitor { open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObjectClass], _ error: Error?) -> Void)) { NotImplemented() - dispatch_queue.async { - - } } open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObjectClass?, _ error: Error?) -> Void)) { @@ -46,43 +43,43 @@ open class SMBFileProvider: FileProvider, FileProviderMonitor { open weak var fileOperationDelegate: FileOperationDelegate? - open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) { + open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { NotImplemented() } - open func create(file fileAttribs: FileObject, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) { + open func create(file fileAttribs: FileObject, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? { NotImplemented() } - open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) { + open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { NotImplemented() } - open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) { + open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { NotImplemented() } - open func removeItem(path: String, completionHandler: SimpleCompletionHandler) { + open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { NotImplemented() } - open func copyItem(localFile: URL, to toPath: String, completionHandler: SimpleCompletionHandler) { + open func copyItem(localFile: URL, to toPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { NotImplemented() } - open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) { + open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { NotImplemented() } - open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) { + open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { NotImplemented() } - open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) { + open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { NotImplemented() } - open func writeContents(path: String, contents data: Data, atomically: Bool, completionHandler: SimpleCompletionHandler) { + open func writeContents(path: String, contents data: Data, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { NotImplemented() } diff --git a/Sources/WebDAVFileProvider.swift b/Sources/WebDAVFileProvider.swift index 407d8e9..57c742c 100644 --- a/Sources/WebDAVFileProvider.swift +++ b/Sources/WebDAVFileProvider.swift @@ -150,9 +150,10 @@ open class WebDAVFileProvider: NSObject, FileProviderBasic { } extension WebDAVFileProvider: FileProviderOperations { - public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) { + @discardableResult + public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")) ?? true == true else { - return + return nil } let url = absoluteURL((atPath as NSString).appendingPathComponent(folderName) + "/") var request = URLRequest(url: url) @@ -170,11 +171,13 @@ extension WebDAVFileProvider: FileProviderOperations { self.delegateNotify(.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/"), error: responseError ?? error) }) task.resume() + return RemoteOperationHandle(tasks: [task]) } - public func create(file fileAttribs: FileObject, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) { + @discardableResult + public func create(file fileAttribs: FileObject, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .create(path: path)) ?? true == true else { - return + return nil } let url = absoluteURL(path) var request = URLRequest(url: url) @@ -189,23 +192,26 @@ extension WebDAVFileProvider: FileProviderOperations { }) task.taskDescription = dictionaryToJSON(["type": "Create" as NSString, "source": (path as NSString).appendingPathComponent(fileAttribs.name) as NSString]) task.resume() + return RemoteOperationHandle(tasks: [task]) } - public func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) { + @discardableResult + public func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .move(source: path, destination: toPath)) ?? true == true else { - return + return nil } - self.copyMoveItem(move: true, path: path, toPath: toPath, overwrite: overwrite, completionHandler: completionHandler) + return self.copyMoveItem(move: true, path: path, toPath: toPath, overwrite: overwrite, completionHandler: completionHandler) } - public func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) { + @discardableResult + public func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .copy(source: path, destination: toPath)) ?? true == true else { - return + return nil } - self.copyMoveItem(move: false, path: path, toPath: toPath, overwrite: overwrite, completionHandler: completionHandler) + return self.copyMoveItem(move: false, path: path, toPath: toPath, overwrite: overwrite, completionHandler: completionHandler) } - fileprivate func copyMoveItem(move:Bool, path: String, toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) { + fileprivate func copyMoveItem(move:Bool, path: String, toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let url = absoluteURL(path) var request = URLRequest(url: url) if move { @@ -220,7 +226,7 @@ extension WebDAVFileProvider: FileProviderOperations { let task = session.dataTask(with: request, completionHandler: { (data, response, error) in if let response = response as? HTTPURLResponse, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) { defer { - let op = move ? FileOperation.move(source: path, destination: toPath) : .copy(source: path, destination: toPath) + let op = move ? FileOperationType.move(source: path, destination: toPath) : .copy(source: path, destination: toPath) self.delegateNotify(op, error: error) } if code == .multiStatus, let data = data { @@ -236,11 +242,13 @@ extension WebDAVFileProvider: FileProviderOperations { completionHandler?(error) }) task.resume() + return RemoteOperationHandle(tasks: [task]) } - public func removeItem(path: String, completionHandler: SimpleCompletionHandler) { + @discardableResult + public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .remove(path: path)) ?? true == true else { - return + return nil } let url = absoluteURL(path) var request = URLRequest(url: url) @@ -263,11 +271,13 @@ extension WebDAVFileProvider: FileProviderOperations { completionHandler?(error) }) task.resume() + return RemoteOperationHandle(tasks: [task]) } - public func copyItem(localFile: URL, to toPath: String, completionHandler: SimpleCompletionHandler) { + @discardableResult + public func copyItem(localFile: URL, to toPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .copy(source: localFile.absoluteString, destination: toPath)) ?? true == true else { - return + return nil } let url = absoluteURL(toPath) var request = URLRequest(url: url) @@ -282,11 +292,13 @@ extension WebDAVFileProvider: FileProviderOperations { }) task.taskDescription = dictionaryToJSON(["type": "Copy" as NSString, "source": localFile.absoluteString as NSString, "dest": toPath as NSString]) task.resume() + return RemoteOperationHandle(tasks: [task]) } - public func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) { + @discardableResult + public func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .copy(source: path, destination: toLocalURL.absoluteString)) ?? true == true else { - return + return nil } let url = absoluteURL(path) let request = URLRequest(url: url) @@ -307,15 +319,18 @@ extension WebDAVFileProvider: FileProviderOperations { }) task.taskDescription = dictionaryToJSON(["type": "Copy" as NSString, "source": path as NSString, "dest": toLocalURL.absoluteString as NSString]) task.resume() + return RemoteOperationHandle(tasks: [task]) } } extension WebDAVFileProvider: FileProviderReadWrite { - public func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) { - self.contents(path: path, offset: 0, length: -1, completionHandler: completionHandler) + @discardableResult + public func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { + return self.contents(path: path, offset: 0, length: -1, completionHandler: completionHandler) } - public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) { + @discardableResult + public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { let url = absoluteURL(path) var request = URLRequest(url: url) request.httpMethod = "GET" @@ -332,11 +347,13 @@ extension WebDAVFileProvider: FileProviderReadWrite { completionHandler(data, responseError ?? error) }) task.resume() + return RemoteOperationHandle(tasks: [task]) } - public func writeContents(path: String, contents data: Data, atomically: Bool = false, completionHandler: SimpleCompletionHandler) { + @discardableResult + public func writeContents(path: String, contents data: Data, atomically: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .modify(path: path)) ?? true == true else { - return + return nil } // FIXME: lock destination before writing process let url = atomically ? absoluteURL(path).appendingPathExtension("tmp") : absoluteURL(path) @@ -360,6 +377,7 @@ extension WebDAVFileProvider: FileProviderReadWrite { }) task.taskDescription = dictionaryToJSON(["type": "Modify" as NSString, "source": path as NSString]) task.resume() + return RemoteOperationHandle(tasks: [task]) } public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { @@ -508,7 +526,7 @@ internal extension WebDAVFileProvider { return WebDavFileObject(absoluteURL: href, name: name, path: href.path, size: size, contentType: contentType, createdDate: createdDate, modifiedDate: modifiedDate, fileType: isDirectory ? .directory : .regular, isHidden: false, isReadOnly: false, entryTag: entryTag) } - fileprivate func delegateNotify(_ operation: FileOperation, error: Error?) { + fileprivate func delegateNotify(_ operation: FileOperationType, error: Error?) { DispatchQueue.main.async(execute: { if error == nil { self.delegate?.fileproviderSucceed(self, operation: operation)