diff --git a/FileProvider.podspec b/FileProvider.podspec index d4609ba..92765e1 100644 --- a/FileProvider.podspec +++ b/FileProvider.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = "FileProvider" - s.version = "0.12.1" + s.version = "0.12.2" 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 83d83cc..0b2ace8 100644 --- a/FileProvider.xcodeproj/project.pbxproj +++ b/FileProvider.xcodeproj/project.pbxproj @@ -603,7 +603,7 @@ 799396601D48B7BF00086753 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_VERSION_STRING = 0.12.1; + BUNDLE_VERSION_STRING = 0.12.2; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_EMPTY_BODY = YES; @@ -633,7 +633,7 @@ 799396611D48B7BF00086753 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_VERSION_STRING = 0.12.1; + BUNDLE_VERSION_STRING = 0.12.2; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_EMPTY_BODY = YES; diff --git a/README.md b/README.md index d150377..ac483ef 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ Local and WebDAV providers are fully tested and can be used in production enviro - [x] **LocalFileProvider** a wrapper around `FileManager` with some additions like searching and reading a portion of file. - [x] **CloudFileProvider** A wrapper around app's ubiquitous container to iCloud Drive in iOS 8+ API. - [x] **WebDAVFileProvider** WebDAV protocol is defacto file transmission standard, replaced FTP. -- [x] **DropboxFileProvider** A wrapper around Dropbox REST API. +- [x] **DropboxFileProvider** A wrapper around Dropbox Web API. * For now it has limitation in uploading files up to 150MB. - [x] **OneDriveFileProvider** A wrapper around OneDrive REST API, works with `onedrive.com` and compatible (business) servers. * For now it has limitation in uploading files up to 100MB. - [ ] **GoogleFileProvider** A wrapper around Goodle Drive REST API. - [ ] **AmazonS3FileProvider** Amazon storage backend. Used by many sites. - [ ] **SMBFileProvider** SMB2/3 introduced in 2006, which is a file and printer sharing protocol originated from Microsoft Windows and now is replacing AFP protocol on macOS. - * Data types and some basic functions are implemented but *main interface is not implemented yet!* - * SMB1/CIFS is depericated and very tricky to be implemented + * Data types and some basic functions are implemented but *main interface is not implemented yet!*. + * SMB1/CIFS is deprecated and very tricky to be implemented. - [ ] **FTPFileProvider** while deprecated in 1990s, it's still in use on some Web hosts. ## Requirements @@ -69,7 +69,7 @@ github "amosavian/FileProvider" Or to use in Swift Package Manager add this line in `Dependencies`: ```swift -.Package(url: "https://github.com/amosavian/FileProvider.git", majorVersion: 0, minorVersion: 8) +.Package(url: "https://github.com/amosavian/FileProvider.git", majorVersion: 0, minorVersion: 12) ``` ### Manually diff --git a/Sources/CloudFileProvider.swift b/Sources/CloudFileProvider.swift index 42965a6..c9204a4 100644 --- a/Sources/CloudFileProvider.swift +++ b/Sources/CloudFileProvider.swift @@ -11,7 +11,8 @@ import Foundation open class CloudFileProvider: LocalFileProvider { open override class var type: String { return "iCloudDrive" } - /// Actually is readonly, value is true + /// Forces file operations to use `NSFileCoordinating`, + /// Actually this is readonly, and value is always true. override open var isCoorinating: Bool { get { return true @@ -24,6 +25,9 @@ open class CloudFileProvider: LocalFileProvider { /// The fully-qualified container identifier for an iCloud container directory. open fileprivate(set) var containerId: String? + /// Scope of container, indicates user can manipulate data/files or not. + open fileprivate(set) var scope: UbiquitousScope + /** Initializes the provider for the iCloud container associated with the specified identifier and establishes access to that container. @@ -31,10 +35,11 @@ open class CloudFileProvider: LocalFileProvider { - Important: Do not call this method from your app’s main thread. Because this method might take a nontrivial amount of time to set up iCloud and return the requested URL, you should always call it from a secondary thread. - Parameter containerId: The fully-qualified container identifier for an iCloud container directory. The string you specify must not contain wildcards and must be of the form `.`, where `` is your development team ID and `` is the bundle identifier of the container you want to access.\ - The container identifiers for your app must be declared in the `com.apple.developer.ubiquity-container-identifiers` array of the `.entitlements` property list file in your Xcode project.\ - If you specify nil for this parameter, this method uses the first container listed in the `com.apple.developer.ubiquity-container-identifiers` entitlement array. + The container identifiers for your app must be declared in the `com.apple.developer.ubiquity-container-identifiers` array of the `.entitlements` property list file in your Xcode project.\ + If you specify nil for this parameter, this method uses the first container listed in the `com.apple.developer.ubiquity-container-identifiers` entitlement array. + - Parameter scope: Use `.documents` (default) to put documents that the user is allowed to access inside a Documents subdirectory. Otherwise use `.data` to store user-related data files that your app needs to share but that are not files you want the user to manipulate directly. */ - public init? (containerId: String?) { + public init? (containerId: String?, scope: UbiquitousScope = .documents) { assert(!Thread.isMainThread, "LocalFileProvider.init(containerId:) is not recommended to be executed on Main Thread.") guard FileManager.default.ubiquityIdentityToken == nil else { return nil @@ -43,7 +48,14 @@ open class CloudFileProvider: LocalFileProvider { return nil } self.containerId = containerId - let baseURL = ubiquityURL.standardized.appendingPathComponent("Documents/") + self.scope = scope + let baseURL: URL + if scope == .documents { + baseURL = ubiquityURL.standardized.appendingPathComponent("Documents/") + } else { + baseURL = ubiquityURL.standardized + } + super.init(baseURL: baseURL) self.isCoorinating = true @@ -57,12 +69,14 @@ open class CloudFileProvider: LocalFileProvider { try? fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true) } + // FIXME: create runloop for dispatch_queue, start query on it open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) { dispatch_queue.async { let pathURL = self.url(of: path) + let query = NSMetadataQuery() query.predicate = NSPredicate(format: "%K BEGINSWITH %@", NSMetadataItemPathKey, pathURL.path) - query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] + query.searchScopes = [self.scope.rawValue] var finishObserver: NSObjectProtocol? finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in defer { @@ -91,9 +105,14 @@ open class CloudFileProvider: LocalFileProvider { } } + query.stop() completionHandler(contents, nil) }) - query.start() + DispatchQueue.main.async { + if !query.start() { + completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission)) + } + } } } @@ -107,7 +126,7 @@ open class CloudFileProvider: LocalFileProvider { let pathURL = self.url(of: path) let query = NSMetadataQuery() query.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemPathKey, pathURL.path) - query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] + query.searchScopes = [self.scope.rawValue] var finishObserver: NSObjectProtocol? finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in defer { @@ -130,7 +149,11 @@ open class CloudFileProvider: LocalFileProvider { completionHandler(nil, noFileError) } }) - query.start() + DispatchQueue.main.async { + if !query.start() { + completionHandler(nil, self.throwError(path, code: CocoaError.fileReadNoPermission)) + } + } } } @@ -239,7 +262,7 @@ open class CloudFileProvider: LocalFileProvider { let pathURL = self.url(of: path) let query = NSMetadataQuery() query.predicate = NSPredicate(format: "(%K BEGINSWITH %@) && (%K LIKE %@)", NSMetadataItemPathKey, pathURL.path, NSMetadataItemFSNameKey, query) - query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] + query.searchScopes = [self.scope.rawValue] var lastReportedCount = 0 @@ -302,18 +325,22 @@ open class CloudFileProvider: LocalFileProvider { completionHandler(contents, nil) }) - query.start() + DispatchQueue.main.async { + if !query.start() { + completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission)) + } + } } } - fileprivate var monitors = [URL: (NSMetadataQuery, NSObjectProtocol)]() - // + fileprivate var monitors = [String: (NSMetadataQuery, NSObjectProtocol)]() + open override func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) { self.unregisterNotifcation(path: path) let pathURL = self.url(of: path) let query = NSMetadataQuery() query.predicate = NSPredicate(format: "(%K BEGINSWITH %@)", NSMetadataItemPathKey, pathURL.path) - query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] + query.searchScopes = [self.scope.rawValue] let updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: nil, using: { (notification) in @@ -324,24 +351,25 @@ open class CloudFileProvider: LocalFileProvider { query.enableUpdates() }) - query.start() - - monitors[pathURL] = (query, updateObserver) + DispatchQueue.main.async { + if query.start() { + self.monitors[path] = (query, updateObserver) + } + } } open override func unregisterNotifcation(path: String) { - let key = url(of: path) - guard let (query, observer) = monitors[key] else { + guard let (query, observer) = monitors[path] else { return } query.disableUpdates() query.stop() - monitors.removeValue(forKey: key) NotificationCenter.default.removeObserver(observer) + monitors.removeValue(forKey: path) } open override func isRegisteredForNotification(path: String) -> Bool { - return monitors[url(of: path)] != nil + return monitors[path] != nil } open override func copy(with zone: NSZone? = nil) -> Any { @@ -401,6 +429,38 @@ open class CloudFileProvider: LocalFileProvider { } } +public enum UbiquitousScope: RawRepresentable { + /// Search all files not in the Documents directories of the app’s iCloud container directories. + /// Use this scope to store user-related data files that your app needs to share + /// but that are not files you want the user to manipulate directly. + case data + /// Search all files in the Documents directories of the app’s iCloud container directories. + /// Put documents that the user is allowed to access inside a Documents subdirectory. + case documents + + public typealias RawValue = String + + public init? (rawValue: String) { + switch rawValue { + case NSMetadataQueryUbiquitousDataScope: + self = .data + case NSMetadataQueryUbiquitousDocumentsScope: + self = .documents + default: + return nil + } + } + + public var rawValue: String { + switch self { + case .data: + return NSMetadataQueryUbiquitousDataScope + case .documents: + return NSMetadataQueryUbiquitousDocumentsScope + } + } +} + open class CloudOperationHandle: OperationHandle { public let baseURL: URL? public let operationType: FileOperationType @@ -458,11 +518,12 @@ open class CloudOperationHandle: OperationHandle { fileprivate static func getMetadataItem(url: URL) -> NSMetadataItem? { let query = NSMetadataQuery() query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path) - query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] + query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope] var item: NSMetadataItem? let group = DispatchGroup() + group.enter() var finishObserver: NSObjectProtocol? finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in defer { @@ -479,8 +540,9 @@ open class CloudOperationHandle: OperationHandle { }) - group.enter() - query.start() + DispatchQueue.main.async { + query.start() + } _ = group.wait(timeout: DispatchTime.now() + 30) return item } diff --git a/Sources/DropboxFileProvider.swift b/Sources/DropboxFileProvider.swift index 5c1331e..3bc01b4 100644 --- a/Sources/DropboxFileProvider.swift +++ b/Sources/DropboxFileProvider.swift @@ -133,8 +133,6 @@ open class DropboxFileProvider: FileProviderBasicRemote { } extension DropboxFileProvider: FileProviderOperations { - - public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let path = (atPath as NSString).appendingPathComponent(folderName) + "/" return doOperation(.create(path: path), completionHandler: completionHandler) @@ -244,7 +242,10 @@ extension DropboxFileProvider: FileProviderOperations { extension DropboxFileProvider: FileProviderReadWrite { public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { - if length == 0 { + if length == 0 || offset < 0 { + dispatch_queue.async { + completionHandler(Data(), nil) + } return nil } @@ -305,12 +306,12 @@ extension DropboxFileProvider: FileProviderReadWrite { NotImplemented() } - // TODO: Implement /copy_reference, /get_account & /get_current_account + // TODO: Implement /get_account & /get_current_account } extension DropboxFileProvider { /// *DEPRECATED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead. - @available(*, deprecated, message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.") + @available(*, deprecated, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.") open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) { self.publicLink(to: path) { (url, file, _, error) in completionHandler(url, file, error) @@ -318,7 +319,7 @@ extension DropboxFileProvider { } /// *DEPRECATED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead. - @available(*, deprecated, message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.") + @available(*, deprecated, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.") open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) { self.publicLink(to: path) { (url, file, expiration, error) in completionHandler(url, file, expiration, error) diff --git a/Sources/FileObject.swift b/Sources/FileObject.swift index aca297b..c6b0fa3 100644 --- a/Sources/FileObject.swift +++ b/Sources/FileObject.swift @@ -25,7 +25,7 @@ open class FileObject { } /// url to access the resource, not supported by Dropbox provider - @available(*, deprecated, message: "Use url.absoluteURL instead.") + @available(*, deprecated, renamed: "url", message: "Use url.absoluteURL instead.") open var absoluteURL: URL? { return url?.absoluteURL } @@ -101,8 +101,8 @@ open class FileObject { } } - /// **DEPRECATED:** Use `type` property instead. - @available(*, deprecated, message: "Use type property instead.") + /// **OBSOLETED:** Use `type` property instead. + @available(*, obsoleted: 1.0, renamed: "type", message: "Use type property instead.") open var fileType: URLFileResourceType? { return self.type } diff --git a/Sources/FileProvider.swift b/Sources/FileProvider.swift index cd11f42..bd9750b 100644 --- a/Sources/FileProvider.swift +++ b/Sources/FileProvider.swift @@ -510,8 +510,8 @@ extension FileProviderBasic { return path.trimmingCharacters(in: pathTrimSet).addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)! } - /// **DEPRECATED:** Use `url(of:).absoluteURL` instead. - @available(*, deprecated, message: "Use url(of:).absoluteURL instead.") + /// **OBSOLETED:** Use `url(of:).absoluteURL` instead. + @available(*, obsoleted: 1.0, renamed: "url(of:)", message: "Use url(of:).absoluteURL instead.") public func absoluteURL(_ path: String? = nil) -> URL { return url(of: path).absoluteURL } @@ -534,11 +534,20 @@ extension FileProviderBasic { } } + + /// Returns the relative path of url, wothout percent encoding. Even if url is absolute or + /// retrieved from another provider, it will try to resolve the url against `baseURL` of + /// current provider. It's highly recomended to use this method for displaying purposes. + /// + /// - Parameter url: Absolute url to file or directory. + /// - Returns: A `String` contains relative path of url against base url. public func relativePathOf(url: URL) -> String { - if url.baseURL == self.baseURL { + // check if url derieved from current base url + if url.relativeString.isEmpty, url.baseURL == self.baseURL { return url.relativePath.removingPercentEncoding! } + // resolve url string against baseurl guard let baseURL = self.baseURL?.standardizedFileURL else { return url.absoluteString } return url.standardizedFileURL.absoluteString.replacingOccurrences(of: baseURL.absoluteString, with: "/").removingPercentEncoding! } diff --git a/Sources/LocalFileProvider.swift b/Sources/LocalFileProvider.swift index 4364629..eb3bbe3 100644 --- a/Sources/LocalFileProvider.swift +++ b/Sources/LocalFileProvider.swift @@ -417,7 +417,10 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor { @discardableResult open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { - if length == 0 { + if length == 0 || offset < 0 { + dispatch_queue.async { + completionHandler(Data(), nil) + } return nil } diff --git a/Sources/LocalHelper.swift b/Sources/LocalHelper.swift index e6fb140..70997c3 100644 --- a/Sources/LocalHelper.swift +++ b/Sources/LocalHelper.swift @@ -67,7 +67,7 @@ public final class LocalFileObject: FileObject { } -internal class LocalFolderMonitor { +internal final class LocalFolderMonitor { fileprivate let source: DispatchSourceFileSystemObject fileprivate let descriptor: CInt fileprivate let qq: DispatchQueue = DispatchQueue.global(qos: .default) diff --git a/Sources/OneDriveFileProvide.swift b/Sources/OneDriveFileProvide.swift index f381ff2..8b84a71 100644 --- a/Sources/OneDriveFileProvide.swift +++ b/Sources/OneDriveFileProvide.swift @@ -240,7 +240,10 @@ extension OneDriveFileProvider: FileProviderOperations { extension OneDriveFileProvider: FileProviderReadWrite { public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { - if length == 0 { + if length == 0 || offset < 0 { + dispatch_queue.async { + completionHandler(Data(), nil) + } return nil } diff --git a/Sources/WebDAVFileProvider.swift b/Sources/WebDAVFileProvider.swift index 7a61475..e83d3f8 100644 --- a/Sources/WebDAVFileProvider.swift +++ b/Sources/WebDAVFileProvider.swift @@ -331,7 +331,10 @@ extension WebDAVFileProvider: FileProviderOperations { extension WebDAVFileProvider: FileProviderReadWrite { @discardableResult public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { - if length == 0 { + if length == 0 || offset < 0 { + dispatch_queue.async { + completionHandler(Data(), nil) + } return nil }