diff --git a/FileProvider.podspec b/FileProvider.podspec index 1f697fd..92ca9bc 100644 --- a/FileProvider.podspec +++ b/FileProvider.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = "FileProvider" - s.version = "0.12.9" + s.version = "0.14.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 ec95be0..7b022f8 100644 --- a/FileProvider.xcodeproj/project.pbxproj +++ b/FileProvider.xcodeproj/project.pbxproj @@ -597,7 +597,7 @@ 799396601D48B7BF00086753 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_VERSION_STRING = 0.12.9; + BUNDLE_VERSION_STRING = 0.14.0; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_EMPTY_BODY = YES; @@ -627,7 +627,7 @@ 799396611D48B7BF00086753 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_VERSION_STRING = 0.12.9; + BUNDLE_VERSION_STRING = 0.14.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 index db30f5a..42bb0aa 100644 --- a/Sources/CloudFileProvider.swift +++ b/Sources/CloudFileProvider.swift @@ -9,7 +9,7 @@ import Foundation /** - Allows accessing to iCloud Drive stored files. Determine scope when initializing,to either access + Allows accessing to iCloud Drive stored files. Determine scope when initializing, to either access to public documents folder or files stored as data. To setup a functional iCloud container, please @@ -87,7 +87,7 @@ open class CloudFileProvider: LocalFileProvider { If the directory contains no entries or an error is occured, this method will return the empty array. - Parameter path: path to target directory. If empty, `currentPath` value will be used. - - Parameter completionHandler: a block with result of directory entries or error. + - Parameter completionHandler: a closure with result of directory entries or error. `contents`: An array of `FileObject` identifying the the directory entries. `error`: Error returned by system. */ @@ -156,7 +156,7 @@ open class CloudFileProvider: LocalFileProvider { If the directory contains no entries or an error is occured, this method will return the empty `FileObject`. - Parameter path: path to target directory. If empty, `currentPath` value will be used. - - Parameter completionHandler: a block with result of directory entries or error. + - Parameter completionHandler: a closure with result of directory entries or error. `attributes`: A `FileObject` containing the attributes of the item. `error`: Error returned by system. */ @@ -205,6 +205,130 @@ open class CloudFileProvider: LocalFileProvider { } } + /** + Search files inside directory using query asynchronously. + + - Note: For now only it's limited to file names. `query` parameter may take `NSPredicate` format in near future. + + - Parameters: + - path: location of directory to start search + - recursive: Searching subdirectories of path + - query: Simple string of file name to be search (for now). + - foundItemHandler: Closure which is called when a file is found + - completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured. + */ + open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { + + let mapDict: [String: String] = ["url": NSMetadataItemURLKey, "name": NSMetadataItemFSNameKey, "path": NSMetadataItemPathKey, "filesize": NSMetadataItemFSSizeKey, "modifiedDate": NSMetadataItemFSContentChangeDateKey, "creationDate": NSMetadataItemFSCreationDateKey, "contentType": NSMetadataItemContentTypeKey] + + func updateQueryKeys(_ queryComponent: NSPredicate) -> NSPredicate { + if let cQuery = queryComponent as? NSCompoundPredicate { + let newSub = cQuery.subpredicates.map { updateQueryKeys($0 as! NSPredicate) } + switch cQuery.compoundPredicateType { + case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub) + case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub[0]) + case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub) + } + } else if let cQuery = queryComponent as? NSComparisonPredicate { + var newLeft = cQuery.leftExpression + var newRight = cQuery.rightExpression + if newLeft.expressionType == .keyPath, let newKey = mapDict[newLeft.keyPath] { + newLeft = NSExpression(forKeyPath: newKey) + } + if newRight.expressionType == .keyPath, let newKey = mapDict[newRight.keyPath] { + newRight = NSExpression(forKeyPath: newKey) + } + if newLeft.expressionType == .keyPath, newLeft.keyPath == "type" { + newRight = NSExpression(forConstantValue: newRight.constantValue as? String == "directory" ? "public.directory": "public.data") + } + if newRight.expressionType == .keyPath, newRight.keyPath == "type" { + newLeft = NSExpression(forConstantValue: newLeft.constantValue as? String == "directory" ? "public.directory": "public.data") + } + return NSComparisonPredicate(leftExpression: newLeft, rightExpression: newRight, modifier: cQuery.comparisonPredicateModifier, type: cQuery.predicateOperatorType, options: cQuery.options) + } else { + return queryComponent + } + } + + dispatch_queue.async { + let pathURL = self.url(of: path) + let mdquery = NSMetadataQuery() + mdquery.predicate = NSPredicate(format: "(%K BEGINSWITH %@) && (\(updateQueryKeys(query).predicateFormat))", NSMetadataItemPathKey, pathURL.path) + mdquery.searchScopes = [self.scope.rawValue] + + var lastReportedCount = 0 + + if let foundItemHandler = foundItemHandler { + var updateObserver: NSObjectProtocol? + + // FIXME: Remove this section as it won't work as expected on iCloud + updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryGatheringProgress, object: mdquery, queue: nil, using: { (notification) in + + mdquery.disableUpdates() + + guard mdquery.resultCount > lastReportedCount else { return } + + for index in lastReportedCount.. Void) { dispatch_queue.async { completionHandler(self.fileManager.ubiquityIdentityToken != nil) @@ -378,7 +502,7 @@ open class CloudFileProvider: LocalFileProvider { - Parameters: - path: Path of file. - - completionHandler: a block with result of file contents or error. + - completionHandler: a closure with result of file contents or error. `contents`: contents of file in a `Data` object. `error`: Error returned by system. - Returns: An `OperationHandle` to get progress or cancel progress. @@ -397,7 +521,7 @@ open class CloudFileProvider: LocalFileProvider { - path: Path of file. - offset: First byte index which should be read. **Starts from 0.** - length: Bytes count of data. Pass `-1` to read until the end of file. - - completionHandler: a block with result of file contents or error. + - completionHandler: a closure with result of file contents or error. `contents`: contents of file in a `Data` object. `error`: Error returned by system. - Returns: An `OperationHandle` to get progress or cancel progress. @@ -425,98 +549,6 @@ open class CloudFileProvider: LocalFileProvider { return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL) } - /** - Search files inside directory using query asynchronously. - - - Note: For now only it's limited to file names. `query` parameter may take `NSPredicate` format in near future. - - - Parameters: - - path: location of directory to start search - - recursive: Searching subdirectories of path - - query: Simple string of file name to be search (for now). - - foundItemHandler: Block which is called when a file is found - - completionHandler: Block which will be called after finishing search. Returns an arry of `FileObject` or error if occured. - */ - open override func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { - dispatch_queue.async { - let pathURL = self.url(of: path) - let query = NSMetadataQuery() - query.predicate = NSPredicate(format: "(%K BEGINSWITH %@) && (%K LIKE %@)", NSMetadataItemPathKey, pathURL.path, NSMetadataItemFSNameKey, query) - query.searchScopes = [self.scope.rawValue] - - var lastReportedCount = 0 - - if let foundItemHandler = foundItemHandler { - var updateObserver: NSObjectProtocol? - - // FIXME: Remove this section as it won't work as expected on iCloud - 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) @@ -622,7 +654,19 @@ open class CloudFileProvider: LocalFileProvider { } } - /// Returns a pulic url with expiration date, can be shared with other people. + /** + Genrates a public url to a file to be shared with other users and can be downloaded without authentication. + + - Important: URL will be available for a limitied time, determined in `expiration` argument. + + - Parameters: + - to: path of file, including file/directory name. + - completionHandler: a closure with result of directory entries or error. + `link`: a url returned by Dropbox to share. + `attribute`: a `FileObject` containing the attributes of the item. + `expiration`: a `Date` object, determines when the public url will expires. + `error`: Error returned by Dropbox. + */ open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) { operation_queue.addOperation { do { diff --git a/Sources/DropboxFileProvider.swift b/Sources/DropboxFileProvider.swift index 9015150..6a56f03 100644 --- a/Sources/DropboxFileProvider.swift +++ b/Sources/DropboxFileProvider.swift @@ -61,7 +61,7 @@ open class DropboxFileProvider: FileProviderBasicRemote { The latter is easier to use and prefered. Also you can use [auth0/Lock](https://github.com/auth0/Lock.iOS-OSX) which provides graphical user interface. - Parameter credential: a `URLCredential` object with Client ID set as `user` and Token set as `password`. - - Parameter cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used. + - Parameter cache: A URLCache to cache downloaded files and contents. */ public init(credential: URLCredential?, cache: URLCache? = nil) { self.baseURL = nil @@ -135,6 +135,19 @@ open class DropboxFileProvider: FileProviderBasicRemote { task.resume() } + open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { + var foundFiles = [DropboxFileObject]() + guard let queryStr = self.findNameQuery(query, key: "name") as? String else { return } + search(path, query: queryStr, foundItem: { (file) in + if query.evaluate(with: file.mapPredicate()) { + foundFiles.append(file) + foundItemHandler?(file) + } + }, completionHandler: { (error) in + completionHandler(foundFiles, error) + }) + } + open func isReachable(completionHandler: @escaping (Bool) -> Void) { self.storageProperties { total, _ in completionHandler(total > 0) @@ -145,25 +158,25 @@ open class DropboxFileProvider: FileProviderBasicRemote { } extension DropboxFileProvider: FileProviderOperations { - public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open 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) } - public func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let filePath = (path as NSString).appendingPathComponent(fileName) return self.writeContents(path: filePath, contents: data ?? Data(), completionHandler: completionHandler) } - public func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler) } - public func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler) } - public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { return doOperation(.remove(path: path), completionHandler: completionHandler) } @@ -211,7 +224,7 @@ extension DropboxFileProvider: FileProviderOperations { return RemoteOperationHandle(operationType: operation, tasks: [task]) } - public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil @@ -219,7 +232,7 @@ extension DropboxFileProvider: FileProviderOperations { return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler) } - public func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil @@ -253,7 +266,7 @@ extension DropboxFileProvider: FileProviderOperations { } extension DropboxFileProvider: FileProviderReadWrite { - public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { + open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { if length == 0 || offset < 0 { dispatch_queue.async { completionHandler(Data(), nil) @@ -295,16 +308,6 @@ extension DropboxFileProvider: FileProviderReadWrite { return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler) } - public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { - var foundFiles = [DropboxFileObject]() - search(path, query: query, foundItem: { (file) in - foundFiles.append(file) - foundItemHandler?(file) - }, completionHandler: { (error) in - completionHandler(foundFiles, error) - }) - } - /* fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) { /* There is two ways to monitor folders changing in Dropbox. Either using webooks @@ -346,7 +349,7 @@ extension DropboxFileProvider { - Parameters: - to: path of file, including file/directory name. - - completionHandler: a block with result of directory entries or error. + - completionHandler: a closure with result of directory entries or error. `link`: a url returned by Dropbox to share. `attribute`: a `FileObject` containing the attributes of the item. `expiration`: a `Date` object, determines when the public url will expires. @@ -389,7 +392,7 @@ extension DropboxFileProvider { - Parameters: - remoteURL: a valid remote url to file. - to: Destination path of file, including file/directory name. - - completionHandler: a block with result of directory entries or error. + - completionHandler: a closure with result of directory entries or error. `jobId`: Job ID returned by Dropbox to monitor the copy/download progress. `attribute`: A `FileObject` containing the attributes of the item. `error`: Error returned by Dropbox. @@ -454,7 +457,7 @@ extension DropboxFileProvider { } extension DropboxFileProvider: ExtendedFileProvider { - public func thumbnailOfFileSupported(path: String) -> Bool { + open func thumbnailOfFileSupported(path: String) -> Bool { switch (path as NSString).pathExtension.lowercased() { case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff": return true @@ -469,7 +472,7 @@ extension DropboxFileProvider: ExtendedFileProvider { } } - public func propertiesOfFileSupported(path: String) -> Bool { + open func propertiesOfFileSupported(path: String) -> Bool { let fileExt = (path as NSString).pathExtension.lowercased() switch fileExt { case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff": @@ -484,7 +487,7 @@ extension DropboxFileProvider: ExtendedFileProvider { } /// Default value for dimension is 64x64, according to Dropbox documentation - public func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) { + open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) { let url: URL switch (path as NSString).pathExtension.lowercased() { case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff": @@ -527,7 +530,7 @@ extension DropboxFileProvider: ExtendedFileProvider { task.resume() } - public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) { + open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) { let url = URL(string: "files/get_metadata", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" diff --git a/Sources/ExtendedLocalFileProvider.swift b/Sources/ExtendedLocalFileProvider.swift index 810a434..d00a93f 100644 --- a/Sources/ExtendedLocalFileProvider.swift +++ b/Sources/ExtendedLocalFileProvider.swift @@ -10,11 +10,6 @@ import Foundation import ImageIO import CoreGraphics import AVFoundation -#if os(iOS) || os(tvOS) -import UIKit -#elseif os(macOS) -import Cocoa -#endif extension LocalFileProvider: ExtendedFileProvider { public func thumbnailOfFileSupported(path: String) -> Bool { diff --git a/Sources/FPSStreamTask.swift b/Sources/FPSStreamTask.swift index c8cfb61..9fcc2c8 100644 --- a/Sources/FPSStreamTask.swift +++ b/Sources/FPSStreamTask.swift @@ -13,7 +13,6 @@ private var lasttaskIdAssociated = 1_000_000_000 /// This class is a replica of NSURLSessionStreamTask with same api for iOS 7/8 /// while it will fallback to NSURLSessionStreamTask in iOS 9. -@objc internal class FPSStreamTask: URLSessionTask, StreamDelegate { fileprivate var inputStream: InputStream? fileprivate var outputStream: OutputStream? diff --git a/Sources/FileObject.swift b/Sources/FileObject.swift index a715359..68d0901 100644 --- a/Sources/FileObject.swift +++ b/Sources/FileObject.swift @@ -156,6 +156,25 @@ open class FileObject: Equatable { } return rhs.path == lhs.path && rhs.size == lhs.size && rhs.modifiedDate == lhs.modifiedDate } + + internal func mapPredicate() -> [String: Any] { + let mapDict: [URLResourceKey: String] = [.fileURL: "url", .nameKey: "name", .pathKey: "path", .fileSizeKey: "filesize", .creationDateKey: "creationDate", + .contentModificationDateKey: "modifiedDate", .isHiddenKey: "isHidden", .isWritableKey: "isWritable", .serverDate: "serverDate", .entryTag: "entryTag", .mimeType: "mimeType"] + let typeDict: [URLFileResourceType: String] = [.directory: "directory", .regular: "regular", .symbolicLink: "symbolicLink", .unknown: "unknown"] + var result = [String: Any]() + for (key, value) in allValues { + if let convertkey = mapDict[key] { + result[convertkey] = value + } + } + result["eTag"] = result["entryTag"] + result["isReadOnly"] = self.isReadOnly + result["isDirectory"] = self.isDirectory + result["isRegularFile"] = self.isRegularFile + result["isSymLink"] = self.isSymLink + result["type"] = typeDict[self.type ?? .unknown] ?? "unknown" + return result + } } internal func resolve(dateString: String) -> Date? { diff --git a/Sources/FileProvider.swift b/Sources/FileProvider.swift index 3bfd635..0bcec41 100644 --- a/Sources/FileProvider.swift +++ b/Sources/FileProvider.swift @@ -68,7 +68,7 @@ public protocol FileProviderBasic: class { If the directory contains no entries or an error is occured, this method will return the empty array. - Parameter path: path to target directory. If empty, `currentPath` value will be used. - - Parameter completionHandler: a block with result of directory entries or error. + - Parameter completionHandler: a closure with result of directory entries or error. `contents`: An array of `FileObject` identifying the the directory entries. `error`: Error returned by system. */ @@ -80,7 +80,7 @@ public protocol FileProviderBasic: class { If the directory contains no entries or an error is occured, this method will return the empty `FileObject`. - Parameter path: path to target directory. If empty, `currentPath` value will be used. - - Parameter completionHandler: a block with result of directory entries or error. + - Parameter completionHandler: a closure with result of directory entries or error. `attributes`: A `FileObject` containing the attributes of the item. `error`: Error returned by system. */ @@ -90,6 +90,41 @@ public protocol FileProviderBasic: class { /// Returns total and used capacity in provider container asynchronously. func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) + /** + Search files inside directory using query asynchronously. + + - Note: Query string is limited to file name, to search based on other file properties, use NSPredicate version. + + - Parameters: + - path: location of directory to start search + - recursive: Searching subdirectories of path + - query: Simple string that file name contains to be search, case-insensitive. + - foundItemHandler: Closure which is called when a file is found + - completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured. + */ + func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) + + /** + Search files inside directory using query asynchronously. + + Sample predicates: + ``` + NSPredicate(format: "(name CONTAINS[c] 'hello') && (filesize >= 10000)") + NSPredicate(format: "(modifiedDate >= %@)", Date()) + NSPredicate(format: "(path BEGINSWITH %@)", "folder/child folder") + ``` + + - Note: Don't pass Spotlight predicates to this method directly, use `FileProvider.convertSpotlightPredicateTo()` method to get usable predicate. + + - Parameters: + - path: location of directory to start search + - recursive: Searching subdirectories of path + - query: An `NSPredicate` object with keys like `FileObject` members, except `size` which becomes `filesize`. + - foundItemHandler: Closure which is called when a file is found + - completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured. + */ + func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) + /** Returns an independent url to access the file. Some providers like `Dropbox` due to their nature. don't return an absolute url to be used to access file directly. @@ -103,6 +138,39 @@ public protocol FileProviderBasic: class { } extension FileProviderBasic { + public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { + let predicate = NSPredicate(format: "name CONTAINS[c] %@", query) + self.searchFiles(path: path, recursive: recursive, query: predicate, foundItemHandler: foundItemHandler, completionHandler: completionHandler) + } + + /// Converts Spotlight search predicate to `FileProvider.searchFiles()` method usable predicate. + public func convertSpotlightPredicateTo(_ query: NSPredicate) -> NSPredicate { + let mapDict: [String: URLResourceKey] = [NSMetadataItemURLKey: .fileURL, NSMetadataItemFSNameKey: .nameKey, NSMetadataItemPathKey: .pathKey, + NSMetadataItemFSSizeKey: .fileSizeKey, NSMetadataItemFSCreationDateKey: .creationDateKey, + NSMetadataItemFSContentChangeDateKey: .contentModificationDateKey, "kMDItemFSInvisible": .isHiddenKey, "kMDItemFSIsWriteable": .isWritableKey, "kMDItemKind": .mimeType] + + if let cQuery = query as? NSCompoundPredicate { + let newSub = cQuery.subpredicates.map { convertSpotlightPredicateTo($0 as! NSPredicate) } + switch cQuery.compoundPredicateType { + case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub) + case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub[0]) + case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub) + } + } else if let cQuery = query as? NSComparisonPredicate { + var newLeft = cQuery.leftExpression + var newRight = cQuery.rightExpression + if newLeft.expressionType == .keyPath, let newKey = mapDict[newLeft.keyPath] { + newLeft = NSExpression(forKeyPath: newKey.rawValue) + } + if newRight.expressionType == .keyPath, let newKey = mapDict[newRight.keyPath] { + newRight = NSExpression(forKeyPath: newKey.rawValue) + } + return NSComparisonPredicate(leftExpression: newLeft, rightExpression: newRight, modifier: cQuery.comparisonPredicateModifier, type: cQuery.predicateOperatorType, options: cQuery.options) + } else { + return query + } + } + /// The maximum number of queued operations that can execute at the same time. /// /// The default value of this property is `OperationQueue.defaultMaxConcurrentOperationCount`. @@ -114,6 +182,26 @@ extension FileProviderBasic { operation_queue.maxConcurrentOperationCount = newValue } } + + internal func findNameQuery(_ query: NSPredicate, key: String?) -> Any? { + if let cQuery = query as? NSCompoundPredicate { + let find = cQuery.subpredicates.flatMap { findNameQuery($0 as! NSPredicate, key: key) } + if find.count > 0 { + return find[0] + } + return nil + } else if let cQuery = query as? NSComparisonPredicate { + if cQuery.leftExpression.expressionType == .keyPath, key == nil || cQuery.leftExpression.keyPath == key! { + return cQuery.rightExpression.constantValue + } + if cQuery.rightExpression.expressionType == .keyPath, key == nil || cQuery.rightExpression.keyPath == key! { + return cQuery.leftExpression.constantValue + } + return nil + } else { + return nil + } + } } /// Checking equality of two file provider, regardless of current path queues and delegates. @@ -133,10 +221,18 @@ public protocol FileProviderBasicRemote: FileProviderBasic { /// Underlying URLSession instance used for HTTP/S requests var session: URLSession { get } - /// A `URLCache` to cache downloaded files and contents. - /// - /// If set to nil, `URLCache.shared` object will be used. - /// - Note: It has no effect unless setting `useCache` property to `true`. + /** + A `URLCache` to cache downloaded files and contents. + + - Note: It has no effect unless setting `useCache` property to `true`. + + - Warning: FileProvider doesn't manage/free `URLCache` object in a memory pressure scenario. It's upon you to clear + cache memory when receiving `didReceiveMemoryWarning` or via observing `.UIApplicationDidReceiveMemoryWarning` notification. + To clear memory usage use this code: + ``` + provider.cache?.removeAllCachedResponses() + ``` + */ var cache: URLCache? { get } /// Determine to use `cache` property to cache downloaded file objects. Doesn't have effect on query type methods. @@ -224,7 +320,6 @@ public protocol FileProviderOperations: FileProviderBasic { - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. */ @discardableResult - func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? /** @@ -363,7 +458,7 @@ public protocol FileProviderReadWrite: FileProviderBasic { - Parameters: - path: Path of file. - - completionHandler: a block with result of file contents or error. + - completionHandler: a closure with result of file contents or error. `contents`: contents of file in a `Data` object. `error`: Error returned by system. - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. @@ -379,7 +474,7 @@ public protocol FileProviderReadWrite: FileProviderBasic { - path: Path of file. - offset: First byte index which should be read. **Starts from 0.** - length: Bytes count of data. Pass `-1` to read until the end of file. - - completionHandler: a block with result of file contents or error. + - completionHandler: a closure with result of file contents or error. `contents`: contents of file in a `Data` object. `error`: Error returned by system. - Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. @@ -442,20 +537,6 @@ public protocol FileProviderReadWrite: FileProviderBasic { */ @discardableResult func writeContents(path: String, contents: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? - - /** - Search files inside directory using query asynchronously. - - - Note: For now only it's limited to file names. `query` parameter may take `NSPredicate` format in near future. - - - Parameters: - - path: location of directory to start search - - recursive: Searching subdirectories of path - - query: Simple string of file name to be search (for now). - - foundItemHandler: Block which is called when a file is found - - completionHandler: Block which will be called after finishing search. Returns an arry of `FileObject` or error if occured. - */ - func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) } extension FileProviderReadWrite { @@ -496,7 +577,7 @@ public protocol FileProviderMonitor: FileProviderBasic { - Parameters: - path: path of directory. - - eventHandler: Block executed after change, on a secondary thread. + - eventHandler: Closure executed after change, on a secondary thread. */ func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) @@ -614,7 +695,7 @@ extension FileProviderBasic { /// - Returns: A `String` contains relative path of url against base url. public func relativePathOf(url: URL) -> String { // check if url derieved from current base url - if url.relativeString.isEmpty, url.baseURL == self.baseURL { + if !url.relativePath.isEmpty, url.baseURL == self.baseURL { return url.relativePath.removingPercentEncoding! } @@ -710,7 +791,7 @@ public protocol ExtendedFileProvider: FileProviderBasic { - Parameters: - path: path of file. - - completionHandler: a block with result of preview image or error. + - completionHandler: a closure with result of preview image or error. `image`: `NSImage`/`UIImage` object contains preview. `error`: Error returned by system. */ @@ -726,7 +807,7 @@ public protocol ExtendedFileProvider: FileProviderBasic { - Parameters: - path: path of file. - dimension: width and height of result preview image. - - completionHandler: a block with result of preview image or error. + - completionHandler: a closure with result of preview image or error. `image`: `NSImage`/`UIImage` object contains preview. `error`: Error returned by system. */ @@ -741,7 +822,7 @@ public protocol ExtendedFileProvider: FileProviderBasic { - Parameters: - path: path of file. - - completionHandler: a block with result of preview image or error. + - completionHandler: a closure with result of preview image or error. `propertiesDictionary`: A `Dictionary` of proprty keys and values. `keys`: An `Array` contains ordering of keys. `error`: Error returned by system. diff --git a/Sources/LocalFileProvider.swift b/Sources/LocalFileProvider.swift index 39c0d30..845bb15 100644 --- a/Sources/LocalFileProvider.swift +++ b/Sources/LocalFileProvider.swift @@ -23,8 +23,10 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo open var operation_queue: OperationQueue open weak var delegate: FileProviderDelegate? open internal(set) var credential: URLCredential? - + + /// Underlying `FileManager` object for listing and metadata fetching. open private(set) var fileManager = FileManager() + /// Underlying `FileManager` object for operationa like copying, moving, etc. open private(set) var opFileManager = FileManager() fileprivate var fileProviderManagerDelegate: LocalFileProviderManagerDelegate? = nil @@ -61,7 +63,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo default values are `directory: .documentDirectory`. - Parameters: - - sharedContainerId: Same with `App Group` identifier defined in project settings. + - sharedContainerId: Same with `App Group` identifier defined in project settings. - directory: The search path directory. The supported values are described in `FileManager.SearchPathDirectory`. */ public convenience init? (sharedContainerId: String, directory: FileManager.SearchPathDirectory = .documentDirectory) { @@ -85,6 +87,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } self.init(baseURL: finalBaseURL) + self.isCoorinating = true try? fileManager.createDirectory(at: finalBaseURL, withIntermediateDirectories: true) } @@ -132,6 +135,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } } + open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) { + dispatch_queue.async { + completionHandler(LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), nil) + } + } + open func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) { let values = try? baseURL?.resourceValues(forKeys: [.volumeTotalCapacityKey, .volumeAvailableCapacityKey]) let totalSize = Int64(values??.volumeTotalCapacity ?? -1) @@ -139,9 +148,21 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo completionHandler(totalSize, totalSize - freeSize) } - open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) { + open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { dispatch_queue.async { - completionHandler(LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), nil) + let iterator = self.fileManager.enumerator(at: self.url(of: path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in + completionHandler([], e) + return true + } + var result = [LocalFileObject]() + while let fileURL = iterator?.nextObject() as? URL { + let path = self.relativePathOf(url: fileURL) + if let fileObject = LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), query.evaluate(with: fileObject.mapPredicate()) { + result.append(fileObject) + foundItemHandler?(fileObject) + } + } + completionHandler(result, nil) } } @@ -227,26 +248,24 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo @discardableResult fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + + func urlofpath(path: String) -> URL { + if path.hasPrefix("file://") { + let removedSchemePath = path.replacingOccurrences(of: "file://", with: "", options: .anchored) + let pDecodedPath = removedSchemePath.removingPercentEncoding ?? removedSchemePath + return URL(fileURLWithPath: pDecodedPath) + } else { + return self.url(of: path) + } + } + guard let sourcePath = opType.source else { return nil } let destPath = opType.destination - let source: URL - if sourcePath.hasPrefix("file://") { - let removedSchemePath = sourcePath.replacingOccurrences(of: "file://", with: "", options: .anchored) - let pDecodedPath = removedSchemePath.removingPercentEncoding ?? removedSchemePath - source = URL(fileURLWithPath: pDecodedPath) - } else { - source = self.url(of: sourcePath) - } + let source: URL = urlofpath(path: sourcePath) let dest: URL? if let destPath = destPath { - if destPath.hasPrefix("file://") { - let removedSchemePath = destPath.replacingOccurrences(of: "file://", with: "", options: .anchored) - let pDecodedPath = removedSchemePath.removingPercentEncoding ?? removedSchemePath - dest = URL(fileURLWithPath: pDecodedPath) - } else { - dest = self.url(of: destPath) - } + dest = urlofpath(path: destPath) } else { dest = nil } @@ -307,8 +326,8 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } if isCoorinating { - var intents = [NSFileAccessIntent]() successfulSecurityScopedResourceAccess = source.startAccessingSecurityScopedResource() + var intents = [NSFileAccessIntent]() switch opType { case .create, .modify: intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forReplacing)) @@ -452,26 +471,6 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo return self.doOperation(opType, data: data, atomically: atomically, completionHandler: completionHandler) } - open func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { - dispatch_queue.async { - let iterator = self.fileManager.enumerator(at: self.url(of: path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in - completionHandler([], e) - return true - } - var result = [LocalFileObject]() - while let fileURL = iterator?.nextObject() as? URL { - if fileURL.lastPathComponent.lowercased().contains(query.lowercased()) { - let path = self.relativePathOf(url: fileURL) - if let fileObject = LocalFileObject(fileWithPath: path, relativeTo: self.baseURL) { - result.append(fileObject) - foundItemHandler?(fileObject) - } - } - } - completionHandler(result, nil) - } - } - fileprivate var monitors = [LocalFolderMonitor]() open func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) { @@ -500,7 +499,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } open func isRegisteredForNotification(path: String) -> Bool { - return monitors.map( { self.relativePathOf(url: $0.url) } ).contains(path) + return monitors.map( { self.relativePathOf(url: $0.url) } ).contains(path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))) } open func copy(with zone: NSZone? = nil) -> Any { diff --git a/Sources/OneDriveFileProvide.swift b/Sources/OneDriveFileProvide.swift index 2e65c7e..e9522b8 100644 --- a/Sources/OneDriveFileProvide.swift +++ b/Sources/OneDriveFileProvide.swift @@ -71,7 +71,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote { - serverURL: server url, Set it if you are trying to connect OneDrive Business server, otherwise leave it `nil` to connect to OneDrive Personal uses. - drive: drive name for user on server, default value is `root`. - - cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used. + - cache: A URLCache to cache downloaded files and contents. */ public init(credential: URLCredential?, serverURL: URL? = nil, drive: String = "root", cache: URLCache? = nil) { let baseURL = serverURL ?? URL(string: "https://api.onedrive.com/")! @@ -139,6 +139,21 @@ open class OneDriveFileProvider: FileProviderBasicRemote { task.resume() } + open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { + var foundFiles = [OneDriveFileObject]() + var queryStr: String? + queryStr = self.findNameQuery(query, key: "name") as? String ?? self.findNameQuery(query, key: nil) as? String + guard let finalQueryStr = queryStr else { return } + search(path, query: finalQueryStr, foundItem: { (file) in + if query.evaluate(with: file.mapPredicate()) { + foundFiles.append(file) + foundItemHandler?(file) + } + }, completionHandler: { (error) in + completionHandler(foundFiles, error) + }) + } + open func isReachable(completionHandler: @escaping (Bool) -> Void) { let url = URL(string: "/drive/root", relativeTo: baseURL)! var request = URLRequest(url: url) @@ -157,25 +172,25 @@ open class OneDriveFileProvider: FileProviderBasicRemote { extension OneDriveFileProvider: FileProviderOperations { - public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open 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) } - public func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let filePath = (path as NSString).appendingPathComponent(fileName) return self.writeContents(path: filePath, contents: data ?? Data(), completionHandler: completionHandler) } - public func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler) } - public func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler) } - public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { return doOperation(.remove(path: path), completionHandler: completionHandler) } @@ -220,7 +235,7 @@ extension OneDriveFileProvider: FileProviderOperations { return RemoteOperationHandle(operationType: operation, tasks: [task]) } - public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil @@ -228,7 +243,7 @@ extension OneDriveFileProvider: FileProviderOperations { return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler) } - public func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil @@ -259,7 +274,7 @@ extension OneDriveFileProvider: FileProviderOperations { } extension OneDriveFileProvider: FileProviderReadWrite { - public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { + open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { if length == 0 || offset < 0 { dispatch_queue.async { completionHandler(Data(), nil) @@ -290,7 +305,7 @@ extension OneDriveFileProvider: FileProviderReadWrite { return RemoteOperationHandle(operationType: opType, tasks: [task]) } - public func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.modify(path: path) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil @@ -299,16 +314,6 @@ extension OneDriveFileProvider: FileProviderReadWrite { return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler) } - public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { - var foundFiles = [OneDriveFileObject]() - search(path, query: query, foundItem: { (file) in - foundFiles.append(file) - foundItemHandler?(file) - }, completionHandler: { (error) in - completionHandler(foundFiles, error) - }) - } - fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) { /* There is two ways to monitor folders changing in OneDrive. Either using webooks * which means you have to implement a server to translate it to push notifications @@ -327,7 +332,7 @@ extension OneDriveFileProvider: FileProviderReadWrite { - Parameters: - to: path of file, including file/directory name. - - completionHandler: a block with result of directory entries or error. + - completionHandler: a closure with result of directory entries or error. `link`: a url returned by OneDrive to share. `attribute`: `nil` for OneDrive. `expiration`: `nil` for OneDrive, as it doesn't expires. @@ -360,11 +365,11 @@ extension OneDriveFileProvider: FileProviderReadWrite { extension OneDriveFileProvider: ExtendedFileProvider { - public func thumbnailOfFileSupported(path: String) -> Bool { + open func thumbnailOfFileSupported(path: String) -> Bool { return true } - public func propertiesOfFileSupported(path: String) -> Bool { + open func propertiesOfFileSupported(path: String) -> Bool { let fileExt = (path as NSString).pathExtension.lowercased() switch fileExt { case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff": @@ -378,7 +383,7 @@ extension OneDriveFileProvider: ExtendedFileProvider { } } - public func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) { + open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) { let url: URL if let dimension = dimension { url = URL(string: escaped(path: path) + ":/thumbnails/0/=c\(dimension.width)x\(dimension.height)/content", relativeTo: driveURL)! @@ -403,7 +408,7 @@ extension OneDriveFileProvider: ExtendedFileProvider { task.resume() } - public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) { + open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) { let url = URL(string: escaped(path: path), relativeTo: driveURL)! var request = URLRequest(url: url) request.httpMethod = "GET" diff --git a/Sources/RemoteSession.swift b/Sources/RemoteSession.swift index cb50445..b8a03c3 100644 --- a/Sources/RemoteSession.swift +++ b/Sources/RemoteSession.swift @@ -8,7 +8,7 @@ import Foundation -/// Allows to get progress or cancel an in-progress operation, for remote -URLSession based- providers. +/// Allows to get progress or cancel an in-progress operation, for remote, `URLSession` based providers. open class RemoteOperationHandle: OperationHandle { internal var tasks: [Weak] @@ -43,7 +43,7 @@ open class RemoteOperationHandle: OperationHandle { if let task = $1.value as? URLSessionUploadTask { return $0 + task.countOfBytesExpectedToSend } else { - return $0 + ($1.value?.countOfBytesExpectedToSend ?? 0) + return $0 + ($1.value?.countOfBytesExpectedToReceive ?? 0) } } } @@ -71,8 +71,6 @@ public protocol FileProviderHTTPError: Error, CustomStringConvertible { var path: String { get } /// Contents returned by server as error description var errorDescription: String? { get } - - var description: String { get } } extension FileProviderHTTPError { @@ -309,7 +307,7 @@ public enum FileProviderHTTPErrorCode: Int, CustomStringConvertible { case 300...399: return "Redirection" case 400...499: return "Client Error" case 500...599: return "Server Error" - default: return "Server Error" + default: return "Unknown Error" } } } diff --git a/Sources/SMBFileProvider.swift b/Sources/SMBFileProvider.swift index 7444fa5..f67cc54 100644 --- a/Sources/SMBFileProvider.swift +++ b/Sources/SMBFileProvider.swift @@ -100,7 +100,7 @@ class SMBFileProvider: FileProvider, FileProviderMonitor { return nil } - open func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) { + open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) { NotImplemented() } diff --git a/Sources/WebDAVFileProvider.swift b/Sources/WebDAVFileProvider.swift index 628585d..8c377ae 100644 --- a/Sources/WebDAVFileProvider.swift +++ b/Sources/WebDAVFileProvider.swift @@ -13,7 +13,7 @@ import Foundation set `useCache` and `cache` properties to use Foundation `NSURLCache` system. WebDAV system supported by many cloud services including [Box.net](https://www.box.com/home) - and [Yandex disk](https://disk.yandex.com). + and [Yandex disk](https://disk.yandex.com) and [ownCloud](https://owncloud.org). - Important: Because this class uses `URLSession`, it's necessary to disable App Transport Security in case of using this class with unencrypted HTTP connection. @@ -59,7 +59,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote { - Parameters: - baseURL: Location of WebDAV server. - credential: An `URLCredential` object with `user` and `password`. - - cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used. + - cache: A URLCache to cache downloaded files and contents. */ public init? (baseURL: URL, credential: URLCredential?, cache: URLCache? = nil) { if !["http", "https"].contains(baseURL.uw_scheme.lowercased()) { @@ -164,6 +164,38 @@ open class WebDAVFileProvider: FileProviderBasicRemote { }) } + open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { + let url = self.url(of: path) + var request = URLRequest(url: url) + request.httpMethod = "PROPFIND" + //request.setValue("1", forHTTPHeaderField: "Depth") + request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type") + request.httpBody = "\n\n".data(using: .utf8) + runDataTask(with: request, completionHandler: { (data, response, error) in + // FIXME: paginating results + var responseError: FileProviderWebDavError? + if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { + responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url) + } + if let data = data { + let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL) + var fileObjects = [WebDavFileObject]() + for attr in xresponse { + let fileObject = WebDavFileObject(attr) + if !query.evaluate(with: fileObject.mapPredicate()) { + continue + } + + fileObjects.append(fileObject) + foundItemHandler?(fileObject) + } + completionHandler(fileObjects, responseError ?? error) + return + } + completionHandler([], responseError ?? error) + }) + } + open func isReachable(completionHandler: @escaping (Bool) -> Void) { var request = URLRequest(url: baseURL!) request.httpMethod = "PROPFIND" @@ -182,7 +214,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote { extension WebDAVFileProvider: FileProviderOperations { @discardableResult - public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/") guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil @@ -204,7 +236,7 @@ extension WebDAVFileProvider: FileProviderOperations { } @discardableResult - public func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.create(path: (path as NSString).appendingPathComponent(fileName)) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil @@ -226,7 +258,7 @@ extension WebDAVFileProvider: FileProviderOperations { } @discardableResult - public func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.move(source: path, destination: toPath) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil @@ -235,7 +267,7 @@ extension WebDAVFileProvider: FileProviderOperations { } @discardableResult - public func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.copy(source: path, destination: toPath) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil @@ -244,7 +276,7 @@ extension WebDAVFileProvider: FileProviderOperations { } @discardableResult - public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.remove(path: path) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil @@ -299,7 +331,7 @@ extension WebDAVFileProvider: FileProviderOperations { } @discardableResult - public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil @@ -324,7 +356,7 @@ extension WebDAVFileProvider: FileProviderOperations { } @discardableResult - public func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil @@ -355,7 +387,7 @@ extension WebDAVFileProvider: FileProviderOperations { extension WebDAVFileProvider: FileProviderReadWrite { @discardableResult - public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { + open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { if length == 0 || offset < 0 { dispatch_queue.async { completionHandler(Data(), nil) @@ -384,7 +416,7 @@ extension WebDAVFileProvider: FileProviderReadWrite { } @discardableResult - public func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.modify(path: path) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil @@ -417,38 +449,6 @@ extension WebDAVFileProvider: FileProviderReadWrite { return RemoteOperationHandle(operationType: opType, tasks: [task]) } - public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { - let url = self.url(of: path) - var request = URLRequest(url: url) - request.httpMethod = "PROPFIND" - //request.setValue("1", forHTTPHeaderField: "Depth") - request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type") - request.httpBody = "\n\n".data(using: .utf8) - runDataTask(with: request, completionHandler: { (data, response, error) in - // FIXME: paginating results - var responseError: FileProviderWebDavError? - if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { - responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url) - } - if let data = data { - let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL) - var fileObjects = [WebDavFileObject]() - for attr in xresponse { - let path = attr.href.path - if !((path as NSString).lastPathComponent.contains(query)) { - continue - } - let fileObject = WebDavFileObject(attr) - fileObjects.append(fileObject) - foundItemHandler?(fileObject) - } - completionHandler(fileObjects, responseError ?? error) - return - } - completionHandler([], responseError ?? error) - }) - } - /* fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) { /* There is no unified api for monitoring WebDAV server content change/update