diff --git a/FileProvider.podspec b/FileProvider.podspec index 6bbeb8e..b9872d3 100644 --- a/FileProvider.podspec +++ b/FileProvider.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = "FileProvider" - s.version = "0.10.3" + s.version = "0.11.0" s.summary = "FileManager replacement for Local and Remote (WebDAV/Dropbox/OneDrive/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 b67acc4..8778a8d 100644 --- a/FileProvider.xcodeproj/project.pbxproj +++ b/FileProvider.xcodeproj/project.pbxproj @@ -35,6 +35,9 @@ 792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; }; 792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; }; 792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; }; + 79480FF61E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; }; + 79480FF71E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; }; + 79480FF81E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; }; 794C21FE1D58912A00EC49B8 /* DropboxHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */; }; 794C21FF1D58912A00EC49B8 /* DropboxHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */; }; 794C22001D58912A00EC49B8 /* DropboxHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */; }; @@ -127,6 +130,7 @@ 7924B1921D89DAE000589DB7 /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = ""; }; 7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FPSStreamTask.swift; sourceTree = ""; }; 792572401DF23BDA006A1526 /* LocalHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalHelper.swift; sourceTree = ""; }; + 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudFileProvider.swift; sourceTree = ""; }; 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropboxHelper.swift; sourceTree = ""; }; 794C22091D5893F800EC49B8 /* SMB2Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2Notification.swift; sourceTree = ""; }; 794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2QueryTypes.swift; sourceTree = ""; }; @@ -286,6 +290,7 @@ 79F5745A1DFDB10A00179ABF /* FileObject.swift */, 799396961D48C02300086753 /* LocalFileProvider.swift */, 792572401DF23BDA006A1526 /* LocalHelper.swift */, + 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */, 79BD638B1E2CC2300035128C /* ExtendedLocalFileProvider.swift */, 7902C0851D61B56D00564440 /* RemoteSession.swift */, 799396931D48C02300086753 /* DropboxFileProvider.swift */, @@ -480,6 +485,7 @@ 799396D41D48C02300086753 /* SMB2Tree.swift in Sources */, 7924B1A21D89DAE000589DB7 /* Options.swift in Sources */, 792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */, + 79480FF61E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */, 79F5745B1DFDB10B00179ABF /* FileObject.swift in Sources */, 79BD63C51E2D17880035128C /* OneDriveFileProvide.swift in Sources */, 7924B1991D89DAE000589DB7 /* Element.swift in Sources */, @@ -519,6 +525,7 @@ 799396D51D48C02300086753 /* SMB2Tree.swift in Sources */, 7924B1A31D89DAE000589DB7 /* Options.swift in Sources */, 792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */, + 79480FF71E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */, 79F5745C1DFDB10B00179ABF /* FileObject.swift in Sources */, 79BD63C61E2D17880035128C /* OneDriveFileProvide.swift in Sources */, 7924B1B01D89F7DE00589DB7 /* FPSStreamTask.swift in Sources */, @@ -558,6 +565,7 @@ 799396D61D48C02300086753 /* SMB2Tree.swift in Sources */, 7924B1A41D89DAE000589DB7 /* Options.swift in Sources */, 792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */, + 79480FF81E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */, 79F5745D1DFDB10B00179ABF /* FileObject.swift in Sources */, 79BD63C71E2D17880035128C /* OneDriveFileProvide.swift in Sources */, 7924B1B11D89F7DF00589DB7 /* FPSStreamTask.swift in Sources */, @@ -595,7 +603,7 @@ 799396601D48B7BF00086753 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_VERSION_STRING = 0.10.3; + BUNDLE_VERSION_STRING = 0.11.0; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_EMPTY_BODY = YES; @@ -625,7 +633,7 @@ 799396611D48B7BF00086753 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_VERSION_STRING = 0.10.3; + BUNDLE_VERSION_STRING = 0.11.0; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_EMPTY_BODY = YES; diff --git a/Sources/CloudFileProvider.swift b/Sources/CloudFileProvider.swift new file mode 100644 index 0000000..00f4704 --- /dev/null +++ b/Sources/CloudFileProvider.swift @@ -0,0 +1,405 @@ +// +// CloudFileProvider.swift +// FileProvider +// +// Created by Amir Abbas Mousavian. +// Copyright © 2017 Mousavian. Distributed under MIT license. +// + +import Foundation + +open class CloudFileProvider: LocalFileProvider { + + public var type: String { + return "iCloudDrive" + } + + /// Actually is readonly + override open var isCoorinating: Bool { + get { + return true + } + set { + return + } + } + + open var containerId: String? + + public init? (containerId: String?) { + assert(!Thread.isMainThread, "LocalFileProvider.init(containerId:) is not recommended to be executed on Main Thread.") + guard FileManager.default.ubiquityIdentityToken == nil else { + return nil + } + guard let ubiquityURL = FileManager.default.url(forUbiquityContainerIdentifier: containerId) else { + return nil + } + self.containerId = containerId + let baseURL = ubiquityURL.standardized.appendingPathComponent("Documents") + super.init(baseURL: baseURL) + self.isCoorinating = true + + dispatch_queue = DispatchQueue(label: "FileProvider.\(self.type)", attributes: DispatchQueue.Attributes.concurrent) + operation_queue = OperationQueue() + operation_queue.name = "FileProvider.\(self.type).Operation" + + fileManager.url(forUbiquityContainerIdentifier: containerId) + opFileManager.url(forUbiquityContainerIdentifier: containerId) + + try? fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true) + } + + open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) { + dispatch_queue.async { + let pathURL = self.absoluteURL(path) + let query = NSMetadataQuery() + query.predicate = NSPredicate(format: "%K BEGINSWITH %@", NSMetadataItemPathKey, pathURL.path) + query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] + var finishObserver: NSObjectProtocol? + finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in + defer { + query.stop() + NotificationCenter.default.removeObserver(finishObserver!) + } + + guard let results = query.results as? [NSMetadataItem] else { + return + } + + query.disableUpdates() + + var contents = [FileObject]() + for result in results { + guard let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else { + continue + } + + guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else { + continue + } + + if let file = self.mapFileObject(attributes: attribs) { + contents.append(file) + } + } + + completionHandler(contents, nil) + }) + query.start() + } + } + + /// iCloud Storage size and free space is unavailable, it returns local space + open override func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) { + super.storageProperties(completionHandler: completionHandler) + } + + open override func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) { + dispatch_queue.async { + let pathURL = self.absoluteURL(path) + let query = NSMetadataQuery() + query.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemPathKey, pathURL.path) + query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] + var finishObserver: NSObjectProtocol? + finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in + defer { + query.stop() + NotificationCenter.default.removeObserver(finishObserver!) + } + + query.disableUpdates() + + guard let result = (query.results as? [NSMetadataItem])?.first, let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else { + let error = self.throwError(path, code: CocoaError.fileNoSuchFile) + completionHandler(nil, error) + return + } + + if let file = self.mapFileObject(attributes: attribs) { + completionHandler(file, nil) + } else { + let noFileError = self.throwError(path, code: CocoaError.fileNoSuchFile) + completionHandler(nil, noFileError) + } + }) + query.start() + } + } + + @discardableResult + open override func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + let r = super.create(folder: folderName, at: atPath, completionHandler: completionHandler) + return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL) + } + + @discardableResult + open override func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + let r = super.create(file: fileName, at: atPath, contents: data, completionHandler: completionHandler) + return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL) + } + + @discardableResult + open override func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + let r = super.moveItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler) + return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL) + } + + @discardableResult + open override func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + let r = super.copyItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler) + return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL) + } + + @discardableResult + open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + let r = super.removeItem(path: path, completionHandler: completionHandler) + return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL) + } + + @discardableResult + open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + // TODO: Make use of overwrite parameter + let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath) + operation_queue.addOperation { + do { + let toUrl = self.absoluteURL(toPath) + try self.opFileManager.copyItem(at: localFile, to: toUrl) + try self.opFileManager.setUbiquitous(true, itemAt: toUrl, destinationURL: toUrl) + completionHandler?(nil) + DispatchQueue.main.async(execute: { + self.delegate?.fileproviderSucceed(self, operation: opType) + }) + } catch let e { + completionHandler?(e) + DispatchQueue.main.async(execute: { + self.delegate?.fileproviderFailed(self, operation: opType) + }) + } + } + return LocalOperationHandle(operationType: opType, baseURL: self.baseURL) + } + + @discardableResult + open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + // FIXME: Progress + // TODO: test + + let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString) + + do { + try self.opFileManager.startDownloadingUbiquitousItem(at: self.absoluteURL(path)) + } catch let e { + completionHandler?(e) + DispatchQueue.main.async(execute: { + self.delegate?.fileproviderFailed(self, operation: opType) + }) + return nil + } + + let r = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler) + return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL) + } + + @discardableResult + open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { + let r = super.contents(path: path, completionHandler: completionHandler) + return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL) + } + + @discardableResult + open override func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { + let r = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler) + return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL) + } + + @discardableResult + open override func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + let r = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler) + return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL) + } + + open override func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { + dispatch_queue.async { + let pathURL = self.absoluteURL(path) + let query = NSMetadataQuery() + query.predicate = NSPredicate(format: "(%K BEGINSWITH %@) && (%K LIKE %@)", NSMetadataItemPathKey, pathURL.path, NSMetadataItemFSNameKey, query) + query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] + + var lastReportedCount = 0 + + if let foundItemHandler = foundItemHandler { + var updateObserver: NSObjectProtocol? + + updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryGatheringProgress, object: query, queue: nil, using: { (notification) in + + query.disableUpdates() + + guard query.resultCount > lastReportedCount else { return } + + for index in lastReportedCount.. Void)) { + self.unregisterNotifcation(path: path) + self.NotImplemented() + } + // + open override func unregisterNotifcation(path: String) { + self.NotImplemented() + } + + open override func isRegisteredForNotification(path: String) -> Bool { + return monitors[absoluteURL(path)] != nil + } + + /// may return nil + open override func copy(with zone: NSZone? = nil) -> Any { + let copy = CloudFileProvider(containerId: self.containerId) + copy?.currentPath = self.currentPath + copy?.delegate = self.delegate + copy?.fileOperationDelegate = self.fileOperationDelegate + copy?.isPathRelative = self.isPathRelative + return copy as Any + } + + fileprivate func mapFileObject(attributes attribs: [String: Any]) -> FileObject? { + guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, let name = attribs[NSMetadataItemFSNameKey] as? String else { + return nil + } + + let path = self.relativePathOf(url: url) + + let file = FileObject(absoluteURL: url, name: name, path: path) + + file.size = (attribs[NSMetadataItemFSSizeKey] as? NSNumber)?.int64Value ?? -1 + file.creationDate = attribs[NSMetadataItemFSCreationDateKey] as? Date + file.modifiedDate = attribs[NSMetadataItemFSContentChangeDateKey] as? Date + let isFolder = (attribs[NSMetadataItemContentTypeTreeKey] as? [String])?.contains("public.folder") ?? false + let isSymbolic = (attribs[NSMetadataItemContentTypeTreeKey] as? [String])?.contains("public.symlink") ?? false + file.type = isFolder ? .directory : (isSymbolic ? .symbolicLink : .regular) + + return file + } + + /// Removes local copy of file, but spares cloud copy + open func evictItem(path: String, completionHandler: SimpleCompletionHandler) { + operation_queue.addOperation { + do { + try self.opFileManager.evictUbiquitousItem(at: self.absoluteURL(path)) + completionHandler?(nil) + } catch let e { + completionHandler?(e) + } + } + } + + open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) { + operation_queue.addOperation { + do { + var expiration: NSDate? + let url = try self.opFileManager.url(forPublishingUbiquitousItemAt: self.absoluteURL(path), expiration: &expiration) + completionHandler(url, nil, expiration as Date?, nil) + } catch let e { + completionHandler(nil, nil, nil, e) + } + } + } +} + +open class CloudOperationHandle: OperationHandle { + public let baseURL: URL? + public let operationType: FileOperationType + + init (operationType: FileOperationType, baseURL: URL?) { + self.baseURL = baseURL + self.operationType = operationType + } + + private var sourceURL: URL? { + guard let source = operationType.source, let baseURL = baseURL else { return nil } + return source.hasPrefix("file://") ? URL(fileURLWithPath: source) : baseURL.appendingPathComponent(source) + } + + private var destURL: URL? { + guard let dest = operationType.destination, let baseURL = baseURL 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") + + return -1 + } + + /// Caution: may put pressure on CPU, may have latency + open var totalBytes: Int64 { + assert(!Thread.isMainThread, "Don't run \(#function) method on main thread") + + return -1 + } + + /// Not usable in local provider + open var inProgress: Bool { + + return false + } + + /// Not usable in local provider + open func cancel() -> Bool{ + return false + } +} diff --git a/Sources/LocalFileProvider.swift b/Sources/LocalFileProvider.swift index ed7b065..3e71990 100644 --- a/Sources/LocalFileProvider.swift +++ b/Sources/LocalFileProvider.swift @@ -14,31 +14,66 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { open fileprivate(set) var baseURL: URL? open var currentPath: String open var dispatch_queue: DispatchQueue - open var operation_queue: DispatchQueue + open var operation_queue: OperationQueue open weak var delegate: FileProviderDelegate? - open fileprivate(set) var credential: URLCredential? + open internal(set) var credential: URLCredential? open private(set) var fileManager = FileManager() open private(set) var opFileManager = FileManager() fileprivate var fileProviderManagerDelegate: LocalFileProviderManagerDelegate? = nil + /// Forces file operations to use NSFileCoordinating, should be set true if: + /// - Files are on ubiquity(iCloud) container + /// - Multiple processes are accessing same file, recommended always in macOS and when using app extensions in iOS/tvOS (shared container) + open var isCoorinating: Bool + /// default values are `directory: .documentDirectory, domainMask: .userDomainMask` public convenience init (directory: FileManager.SearchPathDirectory = .documentDirectory, domainMask: FileManager.SearchPathDomainMask = .userDomainMask) { self.init(baseURL: FileManager.default.urls(for: directory, in: domainMask).first!) } + public convenience init ? (sharedContainerId: String, directory: FileManager.SearchPathDirectory = .userDirectory) { + guard let baseURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: sharedContainerId) else { + return nil + } + + var finalBaseURL = baseURL + + switch directory { + case .documentDirectory: + finalBaseURL = baseURL.appendingPathComponent("Documents") + case .libraryDirectory: + finalBaseURL = baseURL.appendingPathComponent("Library") + case .cachesDirectory: + finalBaseURL = baseURL.appendingPathComponent("Library/Caches") + case .applicationSupportDirectory: + finalBaseURL = baseURL.appendingPathComponent("Library/Application%20support") + default: + break + } + + self.init(baseURL: finalBaseURL) + + try? fileManager.createDirectory(at: finalBaseURL, withIntermediateDirectories: true) + } + public init (baseURL: URL) { self.baseURL = baseURL self.isPathRelative = true self.currentPath = "" self.credential = nil + self.isCoorinating = false dispatch_queue = DispatchQueue(label: "FileProvider.\(LocalFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent) - operation_queue = DispatchQueue(label: "FileProvider.\(LocalFileProvider.type).Operation", attributes: []) + operation_queue = OperationQueue() + operation_queue.name = "FileProvider.\(LocalFileProvider.type).Operation" + fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self) opFileManager.delegate = fileProviderManagerDelegate + } + @available(*, deprecated, message: "Overriding this method has no effect anymore.") open class func defaultBaseURL() -> URL { return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! } @@ -52,7 +87,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { return LocalFileObject(fileWithPath: path, relativeTo: self.baseURL) }) completionHandler(filesAttributes, nil) - } catch let e as NSError { + } catch let e { completionHandler([], e) } } @@ -76,109 +111,204 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { @discardableResult open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/") - operation_queue.async { + let url = self.absoluteURL(atPath).appendingPathComponent(folderName) + + let operationHandler: (URL) -> Void = { url in do { - try self.opFileManager.createDirectory(at: self.absoluteURL(atPath).appendingPathComponent(folderName), withIntermediateDirectories: true, attributes: [:]) + try self.opFileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: [:]) completionHandler?(nil) - DispatchQueue.main.async(execute: { + DispatchQueue.main.async { self.delegate?.fileproviderSucceed(self, operation: opType) - }) - } catch let e as NSError { + } + } catch let e { completionHandler?(e) - DispatchQueue.main.async(execute: { + DispatchQueue.main.async { self.delegate?.fileproviderFailed(self, operation: opType) - }) + } } } + + if isCoorinating { + let intent = NSFileAccessIntent.writingIntent(with: url, options: .forReplacing) + self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in + completionHandler?(error) + DispatchQueue.main.async { + self.delegate?.fileproviderFailed(self, operation: opType) + } + }) + } else { + operation_queue.addOperation { + operationHandler(url) + } + } + return LocalOperationHandle(operationType: opType, baseURL: self.baseURL) } @discardableResult open func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(fileName)) - operation_queue.async { - let fileURL = self.absoluteURL(atPath).appendingPathComponent(fileName) - let success = self.opFileManager.createFile(atPath: fileURL.path, contents: data, attributes: nil) + let url = self.absoluteURL(atPath).appendingPathComponent(fileName, isDirectory: false) + + let operationHandler: (URL) -> Void = { url in + let success = self.opFileManager.createFile(atPath: url.path, contents: data, attributes: nil) if success { completionHandler?(nil) - DispatchQueue.main.async(execute: { + DispatchQueue.main.async { self.delegate?.fileproviderSucceed(self, operation: opType) - }) + } } else { completionHandler?(self.throwError(atPath, code: URLError.cannotCreateFile as FoundationErrorEnum)) - DispatchQueue.main.async(execute: { + DispatchQueue.main.async { self.delegate?.fileproviderFailed(self, operation: opType) - }) + } } } + + if isCoorinating { + let intent = NSFileAccessIntent.writingIntent(with:url, options: .forReplacing) + self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in + completionHandler?(error) + DispatchQueue.main.async { + self.delegate?.fileproviderFailed(self, operation: opType) + } + }) + } else { + operation_queue.addOperation { + operationHandler(url) + } + } + return LocalOperationHandle(operationType: opType, baseURL: self.baseURL) } @discardableResult open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.move(source: path, destination: toPath) - operation_queue.async { - if !overwrite && self.fileManager.fileExists(atPath: self.absoluteURL(toPath).path) { + let sourceUrl = self.absoluteURL(path) + let destUrl = self.absoluteURL(toPath) + + let sourceIntent = NSFileAccessIntent.writingIntent(with: sourceUrl, options: .forDeleting) + let destIntent = NSFileAccessIntent.writingIntent(with: destUrl, options: .forReplacing) + + let operationHandler: (URL, URL) -> Void = { sourceUrl, destUrl in + if !overwrite && self.fileManager.fileExists(atPath: destUrl.path) { completionHandler?(self.throwError(toPath, code: URLError.cannotMoveFile as FoundationErrorEnum)) return } do { - try self.opFileManager.moveItem(at: self.absoluteURL(path), to: self.absoluteURL(toPath)) + try self.opFileManager.moveItem(at: sourceUrl, to: destUrl) completionHandler?(nil) - DispatchQueue.main.async(execute: { + DispatchQueue.main.async { self.delegate?.fileproviderSucceed(self, operation: opType) - }) - } catch let e as NSError { + } + } catch let e { completionHandler?(e) - DispatchQueue.main.async(execute: { + DispatchQueue.main.async { self.delegate?.fileproviderFailed(self, operation: opType) - }) + } } } + + if isCoorinating { + coordinated(intents: [sourceIntent, destIntent], completionHandler: operationHandler) { (error) in + completionHandler?(error) + DispatchQueue.main.async { + self.delegate?.fileproviderFailed(self, operation: opType) + } + } + } else { + operation_queue.addOperation { + operationHandler(sourceUrl, destUrl) + } + } + + return LocalOperationHandle(operationType: opType, baseURL: self.baseURL) } @discardableResult open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.copy(source: path, destination: toPath) - operation_queue.async { - if !overwrite && self.fileManager.fileExists(atPath: self.absoluteURL(toPath).path) { - completionHandler?(self.throwError(toPath, code: URLError.cannotWriteToFile as FoundationErrorEnum)) + let sourceUrl = self.absoluteURL(path) + let destUrl = self.absoluteURL(toPath) + + let sourceIntent = NSFileAccessIntent.readingIntent(with: sourceUrl, options: .withoutChanges) + let destIntent = NSFileAccessIntent.writingIntent(with: destUrl, options: .forDeleting) + + let operationHandler: (URL, URL) -> Void = { sourceUrl, destUrl in + if !overwrite && self.fileManager.fileExists(atPath: destUrl.path) { + completionHandler?(self.throwError(toPath, code: URLError.cannotMoveFile as FoundationErrorEnum)) return } do { - try self.opFileManager.copyItem(at: self.absoluteURL(path), to: self.absoluteURL(toPath)) + try self.opFileManager.copyItem(at: sourceUrl, to: destUrl) completionHandler?(nil) - DispatchQueue.main.async(execute: { + DispatchQueue.main.async { self.delegate?.fileproviderSucceed(self, operation: opType) - }) - } catch let e as NSError { + } + } catch let e { completionHandler?(e) - DispatchQueue.main.async(execute: { + DispatchQueue.main.async { self.delegate?.fileproviderFailed(self, operation: opType) - }) + } } } + + if isCoorinating { + coordinated(intents: [sourceIntent, destIntent], moving: true, completionHandler: operationHandler) { (error) in + completionHandler?(error) + DispatchQueue.main.async { + self.delegate?.fileproviderFailed(self, operation: opType) + } + } + } else { + operation_queue.addOperation { + operationHandler(sourceUrl, destUrl) + } + } + return LocalOperationHandle(operationType: opType, baseURL: self.baseURL) } @discardableResult open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.remove(path: path) - operation_queue.async { + let url = self.absoluteURL(path) + + let operationHandler: (URL) -> Void = { url in do { - try self.opFileManager.removeItem(at: self.absoluteURL(path)) + let successfulSecurityScopedResourceAccess = url.startAccessingSecurityScopedResource() + try self.opFileManager.removeItem(at: url) + if successfulSecurityScopedResourceAccess { + url.stopAccessingSecurityScopedResource() + } completionHandler?(nil) - DispatchQueue.main.async(execute: { + DispatchQueue.main.async { self.delegate?.fileproviderSucceed(self, operation: opType) - }) - } catch let e as NSError { + } + } catch let e { completionHandler?(e) - DispatchQueue.main.async(execute: { + DispatchQueue.main.async { self.delegate?.fileproviderFailed(self, operation: opType) - }) + } } } + + if isCoorinating { + let intent = NSFileAccessIntent.writingIntent(with:url, options: .forReplacing) + self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in + completionHandler?(error) + DispatchQueue.main.async { + self.delegate?.fileproviderFailed(self, operation: opType) + } + }) + } else { + operation_queue.addOperation { + operationHandler(url) + } + } + return LocalOperationHandle(operationType: opType, baseURL: self.baseURL) } @@ -186,18 +316,18 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { // TODO: Make use of overwrite parameter let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath) - operation_queue.async { + operation_queue.addOperation { do { try self.opFileManager.copyItem(at: localFile, to: self.absoluteURL(toPath)) completionHandler?(nil) - DispatchQueue.main.async(execute: { + DispatchQueue.main.async { self.delegate?.fileproviderSucceed(self, operation: opType) - }) - } catch let e as NSError { + } + } catch let e { completionHandler?(e) - DispatchQueue.main.async(execute: { + DispatchQueue.main.async { self.delegate?.fileproviderFailed(self, operation: opType) - }) + } } } return LocalOperationHandle(operationType: opType, baseURL: self.baseURL) @@ -206,18 +336,18 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { @discardableResult open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString) - operation_queue.async { + operation_queue.addOperation { do { try self.opFileManager.copyItem(at: self.absoluteURL(path), to: toLocalURL) completionHandler?(nil) - DispatchQueue.main.async(execute: { + DispatchQueue.main.async { self.delegate?.fileproviderSucceed(self, operation: opType) - }) - } catch let e as NSError { + } + } catch let e { completionHandler?(e) - DispatchQueue.main.async(execute: { + DispatchQueue.main.async { self.delegate?.fileproviderFailed(self, operation: opType) - }) + } } } return LocalOperationHandle(operationType: opType, baseURL: self.baseURL) @@ -225,11 +355,29 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { @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) + let opType = FileOperationType.fetch(path: path) + let url = self.absoluteURL(path) + + let operationHandler: (URL) -> Void = { url in + let data = self.fileManager.contents(atPath: url.path) completionHandler(data, nil) } - return nil + + if isCoorinating { + let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges) + coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in + completionHandler(nil, error) + DispatchQueue.main.async { + self.delegate?.fileproviderFailed(self, operation: opType) + } + }) + } else { + dispatch_queue.async { + operationHandler(url) + } + } + + return LocalOperationHandle(operationType: opType, baseURL: self.baseURL) } @discardableResult @@ -237,14 +385,16 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { if length < 0 { return self.contents(path: path, completionHandler: completionHandler) } + let opType = FileOperationType.fetch(path: path) - dispatch_queue.async { - let aPath = self.absoluteURL(path).path - guard self.fileManager.fileExists(atPath: aPath) && !self.absoluteURL(path).fileIsDirectory else { + let url = self.absoluteURL(path) + + let operationHandler: (URL) -> Void = { url in + guard self.fileManager.fileExists(atPath: url.path) && !url.fileIsDirectory else { completionHandler(nil, self.throwError(path, code: URLError.cannotOpenFile as FoundationErrorEnum)) return } - guard let handle = FileHandle(forReadingAtPath: aPath) else { + guard let handle = FileHandle(forReadingAtPath: url.path) else { completionHandler(nil, self.throwError(path, code: URLError.cannotOpenFile as FoundationErrorEnum)) return } @@ -254,14 +404,29 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { handle.seek(toFileOffset: UInt64(offset)) let data = handle.readData(ofLength: length) completionHandler(data, nil) - } + + if isCoorinating { + let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges) + coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in + completionHandler(nil, error) + DispatchQueue.main.async { + self.delegate?.fileproviderFailed(self, operation: opType) + } + }) + } else { + dispatch_queue.async { + operationHandler(url) + } + } + return LocalOperationHandle(operationType: opType, baseURL: self.baseURL) } @discardableResult open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.modify(path: path) + let url = self.absoluteURL(path) var options: Data.WritingOptions = [] if atomically { options.insert(.atomic) @@ -269,12 +434,38 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { if overwrite { options.insert(.withoutOverwriting) } - operation_queue.async { - try? data.write(to: self.absoluteURL(path), options: atomically ? [.atomic] : []) - DispatchQueue.main.async(execute: { - self.delegate?.fileproviderSucceed(self, operation: opType) - }) + + let operationHandler: (URL) -> Void = { url in + do { + try data.write(to: url, options: atomically ? [.atomic] : []) + completionHandler?(nil) + DispatchQueue.main.async{ + self.delegate?.fileproviderSucceed(self, operation: opType) + } + } catch let e { + completionHandler?(e) + DispatchQueue.main.async { + self.delegate?.fileproviderFailed(self, operation: opType) + } + } + + } + + if isCoorinating { + let intent = NSFileAccessIntent.writingIntent(with: url, options: .forReplacing) + coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in + completionHandler?(error) + DispatchQueue.main.async { + self.delegate?.fileproviderFailed(self, operation: opType) + } + }) + } else { + operation_queue.addOperation { + operationHandler(url) + } + } + return LocalOperationHandle(operationType: opType, baseURL: self.baseURL) } @@ -341,49 +532,61 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { public extension LocalFileProvider { public func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) { - operation_queue.async { + operation_queue.addOperation { do { try self.opFileManager.createSymbolicLink(at: self.absoluteURL(path), withDestinationURL: self.absoluteURL(destPath)) completionHandler?(nil) - DispatchQueue.main.async(execute: { + DispatchQueue.main.async { self.delegate?.fileproviderSucceed(self, operation: .link(link: path, target: destPath)) - }) - } catch let e as NSError { + } + } catch let e { completionHandler?(e) - DispatchQueue.main.async(execute: { + DispatchQueue.main.async { self.delegate?.fileproviderFailed(self, operation: .link(link: path, target: destPath)) - }) + } + } + } + } + + public func destination(ofSymbolicLink path: String, completionHandler: @escaping (_ url: URL?, _ error: Error?) -> Void) { + dispatch_queue.async { + do { + let destPath = try self.opFileManager.destinationOfSymbolicLink(atPath: self.absoluteURL(path).path) + let destUrl = URL(fileURLWithPath: destPath) + completionHandler(destUrl, nil) + } catch let e{ + completionHandler(nil, e) } } } } -class CloudFileProvider: LocalFileProvider { - // FIXME: convert static var type to class var in next Swift version - - open var type: String { return "iCloudDrive" } - - public init? (containerId: String?) { - assert(!Thread.isMainThread, "LocalFileProvider.init(containerId:) is not recommended to be executed on Main Thread.") - guard FileManager.default.ubiquityIdentityToken == nil else { - return nil +internal extension LocalFileProvider { + func coordinated(intents: [NSFileAccessIntent], completionHandler: @escaping (_ url: URL) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) { + let coordinator = NSFileCoordinator(filePresenter: nil) + coordinator.coordinate(with: intents, queue: operation_queue) { (error) in + if let error = error { + errorHandler?(error) + return + } + completionHandler(intents[0].url) } - guard let ubiquityURL = FileManager.default.url(forUbiquityContainerIdentifier: containerId) else { - return nil - } - super.init(baseURL: ubiquityURL.appendingPathComponent("Documents")) - - dispatch_queue = DispatchQueue(label: "FileProvider.\(self.type)", attributes: DispatchQueue.Attributes.concurrent) - operation_queue = DispatchQueue(label: "FileProvider.\(self.type).Operation", attributes: []) - - fileManager.url(forUbiquityContainerIdentifier: containerId) - opFileManager.url(forUbiquityContainerIdentifier: containerId) - fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self) - opFileManager.delegate = fileProviderManagerDelegate } - open override static func defaultBaseURL() -> URL { - return FileManager.default.url(forUbiquityContainerIdentifier: nil) ?? super.defaultBaseURL() + func coordinated(intents: [NSFileAccessIntent], moving: Bool = false, completionHandler: @escaping (_ sourceUrl: URL, _ destURL: URL) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) { + let coordinator = NSFileCoordinator(filePresenter: nil) + coordinator.coordinate(with: intents, queue: operation_queue) { (error) in + if let error = error { + errorHandler?(error) + return + } + if moving { + coordinator.item(at: intents[0].url, willMoveTo: intents[1].url) + } + completionHandler(intents[0].url, intents[1].url) + if moving { + coordinator.item(at: intents[0].url, didMoveTo: intents[1].url) + } + } } - } diff --git a/Sources/LocalHelper.swift b/Sources/LocalHelper.swift index 2759214..f6b8912 100644 --- a/Sources/LocalHelper.swift +++ b/Sources/LocalHelper.swift @@ -190,7 +190,7 @@ open class LocalOperationHandle: OperationHandle { public let operationType: FileOperationType init (operationType: FileOperationType, baseURL: URL?) { - self.baseURL = baseURL ?? LocalFileProvider.defaultBaseURL() + self.baseURL = baseURL ?? URL(fileURLWithPath: "/") self.operationType = operationType }