diff --git a/FileProvider.podspec b/FileProvider.podspec index 92ca9bc..2385cd2 100644 --- a/FileProvider.podspec +++ b/FileProvider.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = "FileProvider" - s.version = "0.14.0" + s.version = "0.14.1" 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 7b022f8..435f2c7 100644 --- a/FileProvider.xcodeproj/project.pbxproj +++ b/FileProvider.xcodeproj/project.pbxproj @@ -597,7 +597,7 @@ 799396601D48B7BF00086753 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_VERSION_STRING = 0.14.0; + BUNDLE_VERSION_STRING = 0.14.1; 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.14.0; + BUNDLE_VERSION_STRING = 0.14.1; 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 f3c9b36..6d5303e 100644 --- a/Sources/CloudFileProvider.swift +++ b/Sources/CloudFileProvider.swift @@ -226,7 +226,7 @@ open class CloudFileProvider: LocalFileProvider { 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 .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub.first!) case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub) } } else if let cQuery = queryComponent as? NSComparisonPredicate { diff --git a/Sources/DropboxFileProvider.swift b/Sources/DropboxFileProvider.swift index 5401ca1..f04facd 100644 --- a/Sources/DropboxFileProvider.swift +++ b/Sources/DropboxFileProvider.swift @@ -52,6 +52,16 @@ open class DropboxFileProvider: FileProviderBasicRemote { return _session! } + fileprivate var _longpollSession: URLSession? + internal var longpollSession: URLSession { + if _longpollSession == nil { + let config = URLSessionConfiguration.default + config.timeoutIntervalForRequest = 600 + _longpollSession = URLSession(configuration: config, delegate: nil, delegateQueue: nil) + } + return _longpollSession! + } + /** Initializer for Dropbox provider with given client ID and Token. These parameters must be retrieved via [OAuth2 API of Dropbox](https://www.dropbox.com/developers/reference/oauth-guide). @@ -135,15 +145,29 @@ open class DropboxFileProvider: FileProviderBasicRemote { 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) - }) + if let queryStr = query.findValue(forKey: "name", operator: .beginsWith) as? String { + // Dropbox only support searching for file names begin with query in non-enterprise accounts. + // We will use it if there is a `name BEGINSWITH[c] "query"` in predicate, then filter to form final result. + search(path, query: queryStr, foundItem: { (file) in + if query.evaluate(with: file.mapPredicate()) { + foundFiles.append(file) + foundItemHandler?(file) + } + }, completionHandler: { (error) in + completionHandler(foundFiles, error) + }) + } else { + // Dropbox doesn't support searching attributes natively. The workaround is to fallback to listing all files + // and filter it locally. It may have a network burden in case there is many files in Dropbox, so please use it concisely. + list(path, recursive: true, progressHandler: { (files, _, error) in + for file in files where query.evaluate(with: file.mapPredicate()) { + foundItemHandler?(file) + } + }, completionHandler: { (files, _, error) in + let predicatedFiles = files.filter { query.evaluate(with: $0.mapPredicate()) } + completionHandler(predicatedFiles, error) + }) + } } open func isReachable(completionHandler: @escaping (Bool) -> Void) { diff --git a/Sources/DropboxHelper.swift b/Sources/DropboxHelper.swift index c14117a..74daba9 100644 --- a/Sources/DropboxHelper.swift +++ b/Sources/DropboxHelper.swift @@ -75,7 +75,7 @@ public final class DropboxFileObject: FileObject { // codebeat:disable[ARITY] internal extension DropboxFileProvider { - func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) { + func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, session: URLSession? = nil, progressHandler: ((_ contents: [FileObject], _ nextCursor: String?, _ error: Error?) -> Void)? = nil, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) { var requestDictionary = [String: AnyObject]() let url: URL if let cursor = cursor { @@ -91,15 +91,16 @@ internal extension DropboxFileProvider { request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8) - let task = session.dataTask(with: request, completionHandler: { (data, response, error) in + let task = (session ?? self.session).dataTask(with: request, completionHandler: { (data, response, error) in var responseError: FileProviderDropboxError? - var files = prevContents + var files = [DropboxFileObject]() if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { responseError = FileProviderDropboxError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) } if let data = data, let jsonStr = String(data: data, encoding: .utf8) { let json = jsonToDictionary(jsonStr) if let entries = json?["entries"] as? [AnyObject] , entries.count > 0 { + files.reserveCapacity(entries.count) for entry in entries { if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) { files.append(file) @@ -108,12 +109,14 @@ internal extension DropboxFileProvider { let ncursor = json?["cursor"] as? String let hasmore = (json?["has_more"] as? NSNumber)?.boolValue ?? false if hasmore { - self.list(path, cursor: ncursor, prevContents: files, completionHandler: completionHandler) + progressHandler?(files, ncursor, responseError ?? error) + self.list(path, cursor: ncursor, prevContents: prevContents + files, completionHandler: completionHandler) return } } } - completionHandler(files, nil, responseError ?? error) + progressHandler?(files, nil, responseError ?? error) + completionHandler(prevContents + files, nil, responseError ?? error) }) task.taskDescription = FileOperationType.fetch(path: path).json task.resume() diff --git a/Sources/ExtendedLocalFileProvider.swift b/Sources/ExtendedLocalFileProvider.swift index d00a93f..a5cc38f 100644 --- a/Sources/ExtendedLocalFileProvider.swift +++ b/Sources/ExtendedLocalFileProvider.swift @@ -301,9 +301,7 @@ public struct LocalFileInformationGenerator { let expfrac = simplify(Int64(exp.doubleValue * 10_000_000_000_000), 10_000_000_000_000) add(key: "Exposure time", value: "\(expfrac.newTop)/\(expfrac.newBottom)") } - if let iso = exifDict[kCGImagePropertyExifISOSpeedRatings as String] as? NSArray , iso.count > 0 { - add(key: "ISO speed", value: iso[0]) - } + add(key: "ISO speed", value: (exifDict[kCGImagePropertyExifISOSpeedRatings as String] as? NSArray)?.first) return (dic, keys) } @@ -371,10 +369,10 @@ public struct LocalFileInformationGenerator { } let asset = AVURLAsset(url: fileURL, options: nil) let videoTracks = asset.tracks(withMediaType: AVMediaTypeVideo) - if videoTracks.count > 0 { + if let videoTrack = videoTracks.first { var bitrate: Float = 0 - let width = Int(videoTracks[0].naturalSize.width) - let height = Int(videoTracks[0].naturalSize.height) + let width = Int(videoTrack.naturalSize.width) + let height = Int(videoTrack.naturalSize.height) add(key: "Dimensions", value: "\(width)x\(height)") var duration: Int64 = 0 for track in videoTracks { diff --git a/Sources/FileObject.swift b/Sources/FileObject.swift index 68d0901..badc62a 100644 --- a/Sources/FileObject.swift +++ b/Sources/FileObject.swift @@ -101,12 +101,6 @@ open class FileObject: Equatable { } } - /// **OBSOLETED:** Use `type` property instead. - @available(*, obsoleted: 1.0, renamed: "type", message: "Use type property instead.") - open var fileType: URLFileResourceType? { - return self.type - } - /// File is hidden either because begining with dot or filesystem flags /// Setting this value on a file begining with dot has no effect open internal(set) var isHidden: Bool { @@ -157,7 +151,7 @@ open class FileObject: Equatable { return rhs.path == lhs.path && rhs.size == lhs.size && rhs.modifiedDate == lhs.modifiedDate } - internal func mapPredicate() -> [String: Any] { + 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"] @@ -175,6 +169,33 @@ open class FileObject: Equatable { result["type"] = typeDict[self.type ?? .unknown] ?? "unknown" return result } + + static public func convertPredicate(fromSpotlight 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 { convertPredicate(fromSpotlight: $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 + } + } } internal func resolve(dateString: String) -> Date? { @@ -264,7 +285,7 @@ public struct FileObjectSorting { self.isDirectoriesFirst = isDirectoriesFirst } - /// Sorts array of `FileObject`s by criterias set in properties + /// Sorts array of `FileObject`s by criterias set in attributes. public func sort(_ files: [FileObject]) -> [FileObject] { return files.sorted { if isDirectoriesFirst { @@ -302,13 +323,13 @@ public struct FileObjectSorting { } extension Array where Element: FileObject { - /// Returns a sorted array of `FileObject`s by criterias set in properties. + /// Returns a sorted array of `FileObject`s by criterias set in attributes. public func sort(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) -> [Element] { let sorting = FileObjectSorting(type: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst) return sorting.sort(self) as! [Element] } - /// Sorts array of `FileObject`s by criterias set in properties + /// Sorts array of `FileObject`s by criterias set in attributes. public mutating func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) { self = self.sort(by: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst) } diff --git a/Sources/FileProvider.swift b/Sources/FileProvider.swift index 2659d56..2cd57b6 100644 --- a/Sources/FileProvider.swift +++ b/Sources/FileProvider.swift @@ -90,7 +90,7 @@ public protocol FileProviderBasic: class { /** 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. + - Note: Query string is limited to file name, to search based on other file attributes, use NSPredicate version. - Parameters: - path: location of directory to start search @@ -137,42 +137,15 @@ public protocol FileProviderBasic: class { } extension FileProviderBasic { - /// **DEPRECATED** This property never worked as expected and is redundant as only supported by `LocalFileProvider`. - /// To simulate `false` value, assign `URL(fileURLWithPath: "/") to `baseURL`. - @available(*, deprecated, message: "Redundant property, now is always true.") - var isPathRelative: Bool { return true } - 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. + @available(*, obsoleted: 1.0, renamed: "FileObject.convertProdicate(fromSpotlight:)", message: "Use FileObject.convertProdicate(fromSpotlight:) instead.") 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 - } + return FileObject.convertPredicate(fromSpotlight: query) } /// The maximum number of queued operations that can execute at the same time. @@ -186,26 +159,6 @@ 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. @@ -657,28 +610,13 @@ extension FileProviderBasic { return type(of: self).type } - /// path without heading and trailing slash - public var bareCurrentPath: String { - return currentPath.trimmingCharacters(in: pathTrimSet) - } - - func escaped(path: String) -> String { - return path.trimmingCharacters(in: pathTrimSet).addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)! - } - - /// **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 - } + /// **DEPRECATED** This property never worked as expected and is redundant as only supported by `LocalFileProvider`. + /// To simulate `false` value, assign `URL(fileURLWithPath: "/")` to `baseURL`. + @available(*, deprecated, message: "Redundant property, now is always true.") + var isPathRelative: Bool { return true } public func url(of path: String? = nil) -> URL { - var rpath: String - if let path = path { - rpath = path - } else { - rpath = self.currentPath - } + var rpath: String = path ?? self.currentPath rpath = rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath if let baseURL = baseURL { if rpath.hasPrefix("/") { @@ -1086,5 +1024,29 @@ public protocol FoundationErrorEnum { var rawValue: Int { get } } +extension NSPredicate { + func findValue(forKey key: String?, operator op: NSComparisonPredicate.Operator? = nil) -> Any? { + let val = findAllValues(forKey: key).lazy.filter { op == nil || $0.operator == op! } + return val.first?.value + } + + func findAllValues(forKey key: String?) -> [(value: Any, operator: NSComparisonPredicate.Operator)] { + if let cQuery = self as? NSCompoundPredicate { + let find = cQuery.subpredicates.flatMap { ($0 as! NSPredicate).findAllValues(forKey: key) } + return find + } else if let cQuery = self as? NSComparisonPredicate { + if cQuery.leftExpression.expressionType == .keyPath, key == nil || cQuery.leftExpression.keyPath == key!, let const = cQuery.rightExpression.constantValue { + return [(value: const, operator: cQuery.predicateOperatorType)] + } + if cQuery.rightExpression.expressionType == .keyPath, key == nil || cQuery.rightExpression.keyPath == key!, let const = cQuery.leftExpression.constantValue { + return [(value: const, operator: cQuery.predicateOperatorType)] + } + return [] + } else { + return [] + } + } +} + extension URLError.Code: FoundationErrorEnum {} extension CocoaError.Code: FoundationErrorEnum {} diff --git a/Sources/LocalFileProvider.swift b/Sources/LocalFileProvider.swift index 7c63bed..d54b461 100644 --- a/Sources/LocalFileProvider.swift +++ b/Sources/LocalFileProvider.swift @@ -566,7 +566,7 @@ internal extension LocalFileProvider { errorHandler?(error) return } - completionHandler(intents[0].url) + completionHandler(intents.first!.url) } } @@ -577,8 +577,8 @@ internal extension LocalFileProvider { errorHandler?(error) return } - let newSource: URL = intents[0].url - let newDest: URL? = intents.count > 1 ? intents[1].url : nil + guard let newSource: URL = intents.first?.url else { return } + let newDest: URL? = intents.dropFirst().first?.url if moving, let newDest = newDest { coordinator.item(at: newSource, willMoveTo: newDest) } diff --git a/Sources/OneDriveFileProvide.swift b/Sources/OneDriveFileProvide.swift index 72bd3fa..e7b6327 100644 --- a/Sources/OneDriveFileProvide.swift +++ b/Sources/OneDriveFileProvide.swift @@ -20,14 +20,9 @@ import CoreGraphics open class OneDriveFileProvider: FileProviderBasicRemote { open class var type: String { return "OneDrive" } open let baseURL: URL? - /// OneDrive server url, equals with unwrapped `baseURL` - open var serverURL: URL { return baseURL! } /// Drive name for user, default is `root`. Changing its value will effect on new operations. open var drive: String /// Generated storage url from server url and drive name - open var driveURL: URL { - return URL(string: "/drive/\(drive):/", relativeTo: baseURL)! - } open var currentPath: String open var dispatch_queue: DispatchQueue @@ -101,8 +96,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote { } open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) { - let url = URL(string: escaped(path: path), relativeTo: driveURL)! - var request = URLRequest(url: url) + var request = URLRequest(url: url(of: path)) request.httpMethod = "GET" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") let task = session.dataTask(with: request, completionHandler: { (data, response, error) in @@ -121,8 +115,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote { } open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) { - let url = URL(string: "/drive/root", relativeTo: baseURL)! - var request = URLRequest(url: url) + var request = URLRequest(url: url()) request.httpMethod = "GET" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") let task = session.dataTask(with: request, completionHandler: { (data, response, error) in @@ -140,7 +133,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote { 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 + queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first guard let finalQueryStr = queryStr else { return } search(path, query: finalQueryStr, foundItem: { (file) in if query.evaluate(with: file.mapPredicate()) { @@ -152,9 +145,34 @@ open class OneDriveFileProvider: FileProviderBasicRemote { }) } + open func url(of path: String? = nil, modifier: String? = nil) -> URL { + var rpath: String + if let path = path { + rpath = path + } else { + rpath = self.currentPath + } + + if rpath.hasPrefix("/") { + rpath.remove(at: rpath.startIndex) + } + if rpath.isEmpty { + if let modifier = modifier { + return baseURL!.appendingPathComponent("drive/\(drive)/\(modifier)") + } + return baseURL!.appendingPathComponent("drive/\(drive)") + } + let driveURL = baseURL!.appendingPathComponent("drive/\(drive):/") + rpath = (rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath) + rpath = rpath.trimmingCharacters(in: pathTrimSet) + if let modifier = modifier { + rpath = rpath + ":/" + modifier + } + return URL(string: rpath, relativeTo: driveURL) ?? driveURL + } + open func isReachable(completionHandler: @escaping (Bool) -> Void) { - let url = URL(string: "/drive/root", relativeTo: baseURL)! - var request = URLRequest(url: url) + var request = URLRequest(url: url()) request.httpMethod = "HEAD" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") let task = session.dataTask(with: request, completionHandler: { (data, response, error) in @@ -198,7 +216,7 @@ extension OneDriveFileProvider: FileProviderOperations { } guard let sourcePath = operation.source else { return nil } let destPath = operation.destination - var request = URLRequest(url: URL(string: sourcePath, relativeTo: driveURL)!) + var request = URLRequest(url: url(of: sourcePath)) switch operation { case .create: request.httpMethod = "CREATE" @@ -246,8 +264,7 @@ extension OneDriveFileProvider: FileProviderOperations { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } - let url = URL(string: escaped(path: path) + ":/content", relativeTo: driveURL)! - var request = URLRequest(url: url) + var request = URLRequest(url: self.url(of: path, modifier: "content")) request.httpMethod = "GET" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") let task = session.downloadTask(with: request, completionHandler: { (cacheURL, response, error) in @@ -281,8 +298,7 @@ extension OneDriveFileProvider: FileProviderReadWrite { } let opType = FileOperationType.fetch(path: path) - let url = URL(string: escaped(path: path) + ":/content", relativeTo: driveURL)! - var request = URLRequest(url: url) + var request = URLRequest(url: self.url(of: path, modifier: "content")) request.httpMethod = "GET" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") if length > 0 { @@ -337,8 +353,7 @@ extension OneDriveFileProvider: FileProviderReadWrite { `error`: Error returned by OneDrive. */ open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: OneDriveFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) { - let url = URL(string: escaped(path: path) + ":/action.createLink", relativeTo: driveURL)! - var request = URLRequest(url: url) + var request = URLRequest(url: self.url(of: path, modifier: "action.createLink")) request.httpMethod = "POST" let requestDictionary: [String: AnyObject] = ["type": "view" as NSString] request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8) @@ -384,9 +399,9 @@ extension OneDriveFileProvider: ExtendedFileProvider { 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)! + url = self.url(of: path, modifier: "thumbnails/0/=c\(dimension.width)x\(dimension.height)/content") } else { - url = URL(string: escaped(path: path) + ":/thumbnails/0/small/content", relativeTo: driveURL)! + url = self.url(of: path, modifier: "thumbnails/0/small/content") } var request = URLRequest(url: url) @@ -407,8 +422,7 @@ extension OneDriveFileProvider: ExtendedFileProvider { } 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) + var request = URLRequest(url: url(of: path)) request.httpMethod = "GET" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") let task = session.dataTask(with: request, completionHandler: { (data, response, error) in diff --git a/Sources/OneDriveHelper.swift b/Sources/OneDriveHelper.swift index 1f05196..b174cd9 100644 --- a/Sources/OneDriveHelper.swift +++ b/Sources/OneDriveHelper.swift @@ -34,7 +34,9 @@ public final class OneDriveFileObject: FileObject { internal convenience init? (baseURL: URL?, drive: String, json: [String: AnyObject]) { guard let name = json["name"] as? String else { return nil } guard let path = (json["parentReference"] as? NSDictionary)?["path"] as? String else { return nil } - let lPath = path.replacingOccurrences(of: "/drive/\(drive):", with: "/", options: .anchored, range: nil) + var lPath = path.replacingOccurrences(of: "/drive/\(drive)", with: "/", options: .anchored, range: nil) + lPath = lPath.replacingOccurrences(of: "/:", with: "", options: .anchored) + lPath = lPath.replacingOccurrences(of: "//", with: "", options: .anchored) self.init(baseURL: baseURL, name: name, path: lPath) self.size = (json["size"] as? NSNumber)?.int64Value ?? -1 self.modifiedDate = resolve(dateString: json["lastModifiedDateTime"] as? String ?? "") @@ -79,13 +81,8 @@ public final class OneDriveFileObject: FileObject { // codebeat:disable[ARITY] internal extension OneDriveFileProvider { - func list(_ path: String, cursor: String? = nil, prevContents: [OneDriveFileObject] = [], completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) { - let url: URL - if let cursor = cursor { - url = URL(string: cursor)! - } else { - url = URL(string: escaped(path: path), relativeTo: driveURL)! - } + func list(_ path: String, cursor: URL? = nil, prevContents: [OneDriveFileObject] = [], completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) { + let url = cursor ?? self.url(of: path, modifier: "children") var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") @@ -103,7 +100,7 @@ internal extension OneDriveFileProvider { files.append(file) } } - let ncursor = json?["@odata.nextLink"] as? String + let ncursor: URL? = (json?["@odata.nextLink"] as? String).flatMap { URL(string: $0) } let hasmore = ncursor != nil if hasmore { self.list(path, cursor: ncursor, prevContents: files, completionHandler: completionHandler) @@ -125,7 +122,7 @@ internal extension OneDriveFileProvider { return nil } let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail" - let url = URL(string: escaped(path: targetPath) + ":/content" + queryStr, relativeTo: driveURL)! + let url = self.url(of: targetPath, modifier: "content\(queryStr)") var request = URLRequest(url: url) request.httpMethod = "PUT" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") @@ -153,7 +150,7 @@ internal extension OneDriveFileProvider { return nil } let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail" - let url = URL(string: escaped(path: targetPath) + ":/content" + queryStr, relativeTo: driveURL)! + let url = self.url(of: targetPath, modifier: "content\(queryStr)") var request = URLRequest(url: url) request.httpMethod = "PUT" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") @@ -171,15 +168,10 @@ internal extension OneDriveFileProvider { return RemoteOperationHandle(operationType: operation, tasks: [task]) } - func search(_ startPath: String = "", query: String, next: String? = nil, foundItem:@escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) { + func search(_ startPath: String = "", query: String, next: URL? = nil, foundItem:@escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) { let url: URL - if let next = next { - url = URL(string: next)! - } else if self.escaped(path: startPath) == "" { - url = URL(string: "/drive/\(drive)/view.search?q=\(query)", relativeTo: baseURL)! - } else { - url = URL(string: "\(escaped(path: startPath))/view.search?q=\(query)", relativeTo: driveURL)! - } + let q = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! + url = next ?? self.url(of: startPath, modifier: "view.search?q=\(q)") var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") @@ -197,9 +189,8 @@ internal extension OneDriveFileProvider { foundItem(file) } } - let next = json?["@odata.nextLink"] as? String - let hasmore = next != nil - if hasmore, let next = next { + let next: URL? = (json?["@odata.nextLink"] as? String).flatMap { URL(string: $0) } + if let next = next { self.search(startPath, query: query, next: next, foundItem: foundItem, completionHandler: completionHandler) } else { completionHandler(responseError ?? error) diff --git a/Sources/SMBClient.swift b/Sources/SMBClient.swift index c657025..0ad0cf5 100644 --- a/Sources/SMBClient.swift +++ b/Sources/SMBClient.swift @@ -68,11 +68,8 @@ class SMB2ProtocolClient: FPSStreamTask { } let mId = messageId() let smbHeader = SMB2.Header(command: .TREE_CONNECT, creditRequestResponse: 123, messageId: mId, treeId: 0, sessionId: sessionId) - var share = "" let cmp = url.pathComponents - if cmp.count > 0 { - share = cmp[0] - } + let share = cmp.first ?? "" let tcHeader = SMB2.TreeConnectRequest.Header(flags: []) let msg = SMB2.TreeConnectRequest(header: tcHeader, host: host, share: share) let data = createSMB2Message(header: smbHeader, message: msg!)