diff --git a/Sources/CloudFileProvider.swift b/Sources/CloudFileProvider.swift index e3dba56..e85a12b 100644 --- a/Sources/CloudFileProvider.swift +++ b/Sources/CloudFileProvider.swift @@ -94,20 +94,17 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { return nil } self.init(containerId: containerId, scope: scope) - self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? "" self.isCoorinating = aDecoder.decodeBool(forKey: "isCoorinating") } open override func encode(with aCoder: NSCoder) { aCoder.encode(self.containerId, forKey: "containerId") aCoder.encode(self.scope.rawValue, forKey: "scope") - aCoder.encode(self.currentPath, forKey: "currentPath") aCoder.encode(self.isCoorinating, forKey: "isCoorinating") } open override func copy(with zone: NSZone? = nil) -> Any { let copy = CloudFileProvider(containerId: self.containerId, scope: self.scope) - copy?.currentPath = self.currentPath copy?.delegate = self.delegate copy?.fileOperationDelegate = self.fileOperationDelegate return copy as Any diff --git a/Sources/DropboxFileProvider.swift b/Sources/DropboxFileProvider.swift index 643d6c4..d354f43 100644 --- a/Sources/DropboxFileProvider.swift +++ b/Sources/DropboxFileProvider.swift @@ -43,14 +43,12 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { public required convenience init?(coder aDecoder: NSCoder) { self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential) - self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? "" self.useCache = aDecoder.decodeBool(forKey: "useCache") self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache") } override open func copy(with zone: NSZone? = nil) -> Any { let copy = DropboxFileProvider(credential: self.credential, cache: self.cache) - copy.currentPath = self.currentPath copy.delegate = self.delegate copy.fileOperationDelegate = self.fileOperationDelegate copy.useCache = self.useCache @@ -60,10 +58,28 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { open override func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) { // We don't want this progress became in another progress' hierarchy - let progress = Progress(parent: nil, userInfo: nil) - list(path, progress: progress) { (contents, cursor, error) in - completionHandler(contents, error) - } + _ = paginated(path, requestHandler: self.listRequest(path: path), + pageHandler: { [weak self] (data, progress) -> (files: [FileObject], error: Error?, newToken: String?) in + guard let json = data?.deserializeJSON(), let entries = json["entries"] as? [AnyObject] else { + let err = self?.urlError(path, code: .badServerResponse) + return ([], err, nil) + } + + var files = [FileObject]() + for entry in entries { + if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) { + files.append(file) + progress.completedUnitCount += 1 + } + } + let ncursor: String? + if let hasmore = (json["has_more"] as? NSNumber)?.boolValue, hasmore { + ncursor = json["cursor"] as? String + } else { + ncursor = nil + } + return (files, nil, ncursor) + }, completionHandler: completionHandler) } open override func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) { @@ -109,32 +125,33 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { } open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { - let progress = Progress(totalUnitCount: -1) - var foundFiles = [DropboxFileObject]() - 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, progress: progress, foundItem: { (file) in - if query.evaluate(with: file.mapPredicate()) { - foundFiles.append(file) + let queryStr = query.findValue(forKey: "name", operator: .beginsWith) as? String + let requestHandler = self.listRequest(path: path, queryStr: queryStr, recursive: true) + return paginated(path, requestHandler: requestHandler, + pageHandler: { [weak self] (data, progress) -> (files: [FileObject], error: Error?, newToken: String?) in + guard let json = data?.deserializeJSON(), let entries = (json["entries"] ?? json["matches"]) as? [AnyObject] else { + let err = self?.urlError(path, code: .badServerResponse) + return ([], err, nil) + } + + var files = [FileObject]() + for entry in entries { + if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry), query.evaluate(with: file.mapPredicate()) { + files.append(file) + progress.completedUnitCount += 1 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, progress: progress, 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) - }) - } - return progress + } + let ncursor: String? + if let hasmore = (json["has_more"] as? NSNumber)?.boolValue, hasmore { + ncursor = json["cursor"] as? String + } else if let hasmore = (json["more"] as? NSNumber)?.boolValue, hasmore { + ncursor = (json["start"] as? Int).flatMap(String.init) + } else { + ncursor = nil + } + return (files, nil, ncursor) + }, completionHandler: completionHandler) } override func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey : Any] = [:]) -> URLRequest { diff --git a/Sources/DropboxHelper.swift b/Sources/DropboxHelper.swift index 17e50ef..afb9cf3 100644 --- a/Sources/DropboxHelper.swift +++ b/Sources/DropboxHelper.swift @@ -72,7 +72,6 @@ public final class DropboxFileObject: FileObject { } } -// codebeat:disable[ARITY] internal extension DropboxFileProvider { internal func correctPath(_ path: String?) -> String? { guard let path = path else { return nil } @@ -86,105 +85,44 @@ internal extension DropboxFileProvider { return p } - func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, session: URLSession? = nil, progress: Progress, progressHandler: ((_ contents: [FileObject], _ nextCursor: String?, _ error: Error?) -> Void)? = nil, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) { - if progress.isCancelled { return } - - var requestDictionary = [String: AnyObject]() - let url: URL - if let cursor = cursor { - url = URL(string: "files/list_folder/continue", relativeTo: apiURL)! - requestDictionary["cursor"] = cursor as NSString? + internal func listRequest(path: String, queryStr: String? = nil, recursive: Bool = false) -> ((_ token: String?) -> URLRequest?) { + if let queryStr = queryStr { + return { [weak self] (token) -> URLRequest? in + guard let `self` = self else { return nil } + let url = URL(string: "files/search", relativeTo: self.apiURL)! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.set(httpAuthentication: self.credential, with: .oAuth2) + request.set(httpContentType: .json) + var requestDictionary: [String: AnyObject] = ["path": self.correctPath(path) as NSString!] + requestDictionary["query"] = queryStr as NSString + requestDictionary["start"] = NSNumber(value: (token.flatMap(Int.init) ?? 0)) + request.httpBody = Data(jsonDictionary: requestDictionary) + return request + } } else { - url = URL(string: "files/list_folder", relativeTo: apiURL)! - requestDictionary["path"] = correctPath(path) as NSString? - requestDictionary["recursive"] = recursive as NSNumber? - } - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.set(httpAuthentication: credential, with: .oAuth2) - request.set(httpContentType: .json) - request.httpBody = Data(jsonDictionary: requestDictionary) - let task = (session ?? self.session).dataTask(with: request, completionHandler: { (data, response, error) in - var responseError: FileProviderHTTPError? - var files = [DropboxFileObject]() - if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { - responseError = self.serverError(with: rCode, path: path, data: data) - } - if let json = data?.deserializeJSON() { - 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) - progress.totalUnitCount = Int64(files.count) - } - } - let ncursor = json["cursor"] as? String - let hasmore = (json["has_more"] as? NSNumber)?.boolValue ?? false - if hasmore && !progress.isCancelled { - progressHandler?(files, ncursor, responseError ?? error) - self.list(path, cursor: ncursor, prevContents: prevContents + files, progress: progress, completionHandler: completionHandler) - return - } + return { [weak self] (token) -> URLRequest? in + guard let `self` = self else { return nil } + var requestDictionary = [String: AnyObject]() + let url: URL + if let token = token { + url = URL(string: "files/list_folder/continue", relativeTo: self.apiURL)! + requestDictionary["cursor"] = token as NSString? + } else { + url = URL(string: "files/list_folder", relativeTo: self.apiURL)! + requestDictionary["path"] = self.correctPath(path) as NSString? + requestDictionary["recursive"] = NSNumber(value: recursive) } + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.set(httpAuthentication: self.credential, with: .oAuth2) + request.set(httpContentType: .json) + request.httpBody = Data(jsonDictionary: requestDictionary) + return request } - progressHandler?(files, nil, responseError ?? error) - completionHandler(prevContents + files, nil, responseError ?? error) - }) - progress.cancellationHandler = { [weak task] in - task?.cancel() } - progress.setUserInfoObject(Date(), forKey: .startingTimeKey) - task.taskDescription = FileOperationType.fetch(path: path).json - task.resume() - } - - func search(_ startPath: String = "", query: String, start: Int = 0, maxResultPerPage: Int = 25, maxResults: Int = -1, progress: Progress, foundItem:@escaping ((_ file: DropboxFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) { - if progress.isCancelled { return } - - let url = URL(string: "files/search", relativeTo: apiURL)! - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.set(httpAuthentication: credential, with: .oAuth2) - request.set(httpContentType: .json) - var requestDictionary: [String: AnyObject] = ["path": startPath as NSString] - requestDictionary["query"] = query as NSString - requestDictionary["start"] = start as NSNumber - requestDictionary["max_results"] = maxResultPerPage as NSNumber - request.httpBody = Data(jsonDictionary: requestDictionary) - let task = session.dataTask(with: request, completionHandler: { (data, response, error) in - var responseError: FileProviderHTTPError? - if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { - responseError = self.serverError(with: rCode, path: startPath, data: data) - } - if let json = data?.deserializeJSON() { - if let entries = json["matches"] as? [AnyObject] , entries.count > 0 { - for entry in entries { - if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) { - foundItem(file) - progress.completedUnitCount += 1 - } - } - let rstart = json["start"] as? Int - let hasmore = (json["more"] as? NSNumber)?.boolValue ?? false - if hasmore && !progress.isCancelled, let rstart = rstart { - self.search(startPath, query: query, start: rstart + entries.count, maxResultPerPage: maxResultPerPage, progress: progress, foundItem: foundItem, completionHandler: completionHandler) - } else { - completionHandler(responseError ?? error) - } - return - } - } - completionHandler(responseError ?? error) - }) - progress.cancellationHandler = { [weak task] in - task?.cancel() - } - progress.setUserInfoObject(Date(), forKey: .startingTimeKey) - task.resume() } } -// codebeat:enable[ARITY] internal extension DropboxFileProvider { static let dateFormatter = DateFormatter() diff --git a/Sources/ExtendedLocalFileProvider.swift b/Sources/ExtendedLocalFileProvider.swift index b882906..67d46b4 100644 --- a/Sources/ExtendedLocalFileProvider.swift +++ b/Sources/ExtendedLocalFileProvider.swift @@ -129,14 +129,14 @@ public struct LocalFileInformationGenerator { /// Audio and music extensions supportes for thumbnail. /// - /// Default: `["mp3", "aac", "m4a"]` - static public var audioThumbnailExtensions: [String] = ["mp3", "aac", "m4a"] + /// Default: `["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]` + static public var audioThumbnailExtensions: [String] = ["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"] /// Video extensions supportes for thumbnail. /// - /// Default: `["mov", "mp4", "m4v", "mpg", "mpeg"]` - static public var videoThumbnailExtensions: [String] = ["mov", "mp4", "m4v", "mpg", "mpeg"] - + /// Default: `["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]` + static public var videoThumbnailExtensions: [String] = ["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"] + /// Portable document file extensions supportes for thumbnail. /// /// Default: `["pdf"]` @@ -160,13 +160,13 @@ public struct LocalFileInformationGenerator { /// Audio and music extensions supportes for properties. /// - /// Default: `["mp3", "aac", "m4a", "caf"]` - static public var audioPropertiesExtensions: [String] = ["mp3", "aac", "m4a", "caf"] + /// Default: `["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]` + static public var audioPropertiesExtensions: [String] = ["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"] /// Video extensions supportes for properties. /// - /// Default: `["mp4", "mpg", "3gp", "mov", "avi"]` - static public var videoPropertiesExtensions: [String] = ["mp4", "mpg", "3gp", "mov", "avi"] + /// Default: `["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]` + static public var videoPropertiesExtensions: [String] = ["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"] /// Portable document file extensions supportes for properties. /// diff --git a/Sources/FTPFileProvider.swift b/Sources/FTPFileProvider.swift index 323b58a..7a88955 100644 --- a/Sources/FTPFileProvider.swift +++ b/Sources/FTPFileProvider.swift @@ -15,7 +15,9 @@ import Foundation open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite { open class var type: String { return "FTP" } open let baseURL: URL? - open var currentPath: String + /// **OBSOLETED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method. + @available(*, obsoleted: 0.22, message: "This property is redundant with almost no use internally.") + open var currentPath: String = "" open var dispatch_queue: DispatchQueue open var operation_queue: OperationQueue { @@ -88,7 +90,6 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil self.baseURL = (urlComponents.url!.path.hasSuffix("/") ? urlComponents.url! : urlComponents.url!.appendingPathComponent("")).absoluteURL self.passiveMode = passive - self.currentPath = "" self.useCache = false self.validatingCache = true self.cache = cache @@ -107,7 +108,6 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil public required convenience init?(coder aDecoder: NSCoder) { guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else { return nil } self.init(baseURL: baseURL, passive: aDecoder.decodeBool(forKey: "passiveMode"), credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential) - self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? "" self.useCache = aDecoder.decodeBool(forKey: "useCache") self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache") self.useAppleImplementation = aDecoder.decodeBool(forKey: "useAppleImplementation") @@ -116,7 +116,6 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil public func encode(with aCoder: NSCoder) { aCoder.encode(self.baseURL, forKey: "baseURL") aCoder.encode(self.credential, forKey: "credential") - aCoder.encode(self.currentPath, forKey: "currentPath") aCoder.encode(self.useCache, forKey: "useCache") aCoder.encode(self.validatingCache, forKey: "validatingCache") aCoder.encode(self.useAppleImplementation, forKey: "useAppleImplementation") @@ -129,7 +128,6 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil open func copy(with zone: NSZone? = nil) -> Any { let copy = FTPFileProvider(baseURL: self.baseURL!, credential: self.credential, cache: self.cache)! - copy.currentPath = self.currentPath copy.delegate = self.delegate copy.fileOperationDelegate = self.fileOperationDelegate copy.useCache = self.useCache @@ -315,7 +313,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil } open func url(of path: String?) -> URL { - let path = (path ?? self.currentPath).trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? (path ?? self.currentPath) + let path = path?.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? (path ?? "") var baseUrlComponent = URLComponents(url: self.baseURL!, resolvingAgainstBaseURL: true) baseUrlComponent?.user = credential?.user diff --git a/Sources/FTPHelper.swift b/Sources/FTPHelper.swift index 3fd697a..0d3ad2f 100644 --- a/Sources/FTPHelper.swift +++ b/Sources/FTPHelper.swift @@ -789,20 +789,14 @@ internal extension FTPFileProvider { } func ftpPath(_ apath: String) -> String { - var path = apath.isEmpty ? self.currentPath : apath - // path of base url should be concreted into file path! And remove final slash - path = baseURL!.appendingPathComponent(path).path.replacingOccurrences(of: "/", with: "", options: [.anchored, .backwards]) + var path = baseURL!.appendingPathComponent(apath).path.replacingOccurrences(of: "/", with: "", options: [.anchored, .backwards]) // Fixing slashes if !path.hasPrefix("/") { path = "/" + path } - if path.isEmpty { - path = "/" - } - return path } diff --git a/Sources/FileProvider.swift b/Sources/FileProvider.swift index 38cec78..9c2f95a 100644 --- a/Sources/FileProvider.swift +++ b/Sources/FileProvider.swift @@ -29,10 +29,6 @@ public protocol FileProviderBasic: class, NSSecureCoding { /// The url of which paths should resolve against. var baseURL: URL? { get } - /// **DEPRECATED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method. - @available(*, deprecated, message: "This property is redundant with almost no use internally.") - var currentPath: String { get set } - /** Dispatch queue usually used in query methods. Set it to a new object to switch between cuncurrent and serial queues. diff --git a/Sources/HTTPFileProvider.swift b/Sources/HTTPFileProvider.swift index f8b03c4..f503219 100644 --- a/Sources/HTTPFileProvider.swift +++ b/Sources/HTTPFileProvider.swift @@ -17,7 +17,10 @@ import Foundation open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite { open class var type: String { fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") } open let baseURL: URL? - open var currentPath: String + + /// **OBSOLETED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method. + @available(*, obsoleted: 0.22, message: "This property is redundant with almost no use internally.") + open var currentPath: String = "" open var dispatch_queue: DispatchQueue open var operation_queue: OperationQueue { @@ -84,7 +87,6 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi */ public init(baseURL: URL?, credential: URLCredential?, cache: URLCache?) { self.baseURL = baseURL - self.currentPath = "" self.useCache = false self.validatingCache = true self.cache = cache @@ -107,7 +109,6 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi public func encode(with aCoder: NSCoder) { aCoder.encode(self.baseURL, forKey: "baseURL") aCoder.encode(self.credential, forKey: "credential") - aCoder.encode(self.currentPath, forKey: "currentPath") aCoder.encode(self.useCache, forKey: "useCache") aCoder.encode(self.validatingCache, forKey: "validatingCache") } @@ -317,6 +318,46 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi return progress } + internal func paginated(_ path: String, startToken: String? = nil, currentProgress: Progress? = nil, previousResult: [FileObject] = [], requestHandler: @escaping (_ token: String?) -> URLRequest?, pageHandler: @escaping (_ data: Data?, _ progress: Progress) -> (files: [FileObject], error: Error?, newToken: String?), completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress { + let progress = currentProgress ?? Progress(totalUnitCount: -1) + if progress.isCancelled { return progress } + + guard let request = requestHandler(startToken) else { + return progress + } + + let task = session.dataTask(with: request, completionHandler: { (data, response, error) in + if let error = error { + completionHandler(previousResult, error) + return + } + if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { + let responseError = self.serverError(with: rCode, path: path, data: data) + completionHandler(previousResult, responseError) + return + } + + let (newFiles, err, newToken) = pageHandler(data, progress) + if let error = err { + completionHandler(previousResult, error) + return + } + let files = previousResult + newFiles + if let newToken = newToken, !progress.isCancelled { + _ = self.paginated(path, startToken: newToken, currentProgress: progress, previousResult: files, requestHandler: requestHandler, pageHandler: pageHandler, completionHandler: completionHandler) + } else { + completionHandler(files, nil) + } + + }) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) + task.resume() + return progress + } + internal func upload_simple(_ targetPath: String, request: URLRequest, data: Data? = nil, localFile: URL? = nil, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? { let size = data?.count ?? Int((try? localFile?.resourceValues(forKeys: [.fileSizeKey]))??.fileSize ?? -1) diff --git a/Sources/LocalFileProvider.swift b/Sources/LocalFileProvider.swift index e316032..a96c24d 100644 --- a/Sources/LocalFileProvider.swift +++ b/Sources/LocalFileProvider.swift @@ -17,7 +17,9 @@ import Foundation open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndoable { open class var type: String { return "Local" } open fileprivate(set) var baseURL: URL? - open var currentPath: String + /// **OBSOLETED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method. + @available(*, obsoleted: 0.22, message: "This property is redundant with almost no use internally.") + open var currentPath: String = "" open var dispatch_queue: DispatchQueue open var operation_queue: OperationQueue open weak var delegate: FileProviderDelegate? @@ -99,7 +101,6 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo fatalError("Cannot initialize a Local provider from remote URL.") } self.baseURL = (baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")).absoluteURL - self.currentPath = "" self.credential = nil self.isCoorinating = false @@ -121,13 +122,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo return nil } self.init(baseURL: baseURL) - self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? "" self.isCoorinating = aDecoder.decodeBool(forKey: "isCoorinating") } open func encode(with aCoder: NSCoder) { aCoder.encode(self.baseURL, forKey: "currentPath") - aCoder.encode(self.currentPath, forKey: "currentPath") aCoder.encode(self.isCoorinating, forKey: "isCoorinating") } @@ -137,7 +136,6 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo public func copy(with zone: NSZone? = nil) -> Any { let copy = LocalFileProvider(baseURL: self.baseURL!) - copy.currentPath = self.currentPath copy.undoManager = self.undoManager copy.isCoorinating = self.isCoorinating copy.delegate = self.delegate diff --git a/Sources/OneDriveFileProvider.swift b/Sources/OneDriveFileProvider.swift index ac1553b..b11d958 100644 --- a/Sources/OneDriveFileProvider.swift +++ b/Sources/OneDriveFileProvider.swift @@ -139,7 +139,6 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential, serverURL: aDecoder.decodeObject(forKey: "baseURL") as? URL, subAddress: subAddress) - self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? "" self.useCache = aDecoder.decodeBool(forKey: "useCache") self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache") } @@ -151,7 +150,6 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { open override func copy(with zone: NSZone? = nil) -> Any { let copy = OneDriveFileProvider(credential: self.credential, serverURL: self.baseURL, subAddress: self.subAddress, cache: self.cache) - copy.currentPath = self.currentPath copy.delegate = self.delegate copy.fileOperationDelegate = self.fileOperationDelegate copy.useCache = self.useCache @@ -160,9 +158,29 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { } open override func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) { - list(path) { (contents, cursor, error) in - completionHandler(contents, error) - } + _ = paginated(path, requestHandler: { [weak self] (token) -> URLRequest? in + guard let `self` = self else { return nil } + let url = token.flatMap(URL.init(string:)) ?? self.url(of: path, modifier: "children") + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.set(httpAuthentication: self.credential, with: .oAuth2) + return request + }, pageHandler: { [weak self] (data, _) -> (files: [FileObject], error: Error?, newToken: String?) in + guard let `self` = self else { return ([], nil, nil) } + + guard let json = data?.deserializeJSON(), let entries = json["value"] as? [AnyObject] else { + let err = self.urlError(path, code: .badServerResponse) + return ([], err, nil) + } + + var files = [FileObject]() + for entry in entries { + if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, subAddress: self.subAddress, json: entry) { + files.append(file) + } + } + return (files, nil, json["@odata.nextLink"] as? String) + }, completionHandler: completionHandler) } open override func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) { @@ -206,21 +224,45 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { } open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { - var foundFiles = [OneDriveFileObject]() - var queryStr: String? - queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first - guard let finalQueryStr = queryStr else { return nil } - let progress = Progress(totalUnitCount: -1) - progress.setUserInfoObject(url(of: path), forKey: .fileURLKey) - search(path, query: finalQueryStr, recursive: recursive, progress: progress, foundItem: { (file) in - if query.evaluate(with: file.mapPredicate()) { - foundFiles.append(file) - foundItemHandler?(file) + let queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first + + return paginated(path, requestHandler: { [weak self] (token) -> URLRequest? in + guard let `self` = self else { return nil } + + let url: URL + if let next = token.flatMap(URL.init(string:)) { + url = next + } else { + let bURL = self.baseURL!.appendingPathComponent(self.subAddress.drivePath).appendingPathComponent("root/search") + var components = URLComponents(url: bURL, resolvingAgainstBaseURL: false)! + let qItem = URLQueryItem(name: "q", value: (queryStr ?? "*")) + components.queryItems = [qItem] + if recursive { + components.queryItems?.append(URLQueryItem(name: "expand", value: "children")) + } + url = components.url! } - }, completionHandler: { (error) in - completionHandler(foundFiles, error) - }) - return progress + + var request = URLRequest(url: url) + request.httpMethod = "GET" + return request + }, pageHandler: { [weak self] (data, progress) -> (files: [FileObject], error: Error?, newToken: String?) in + guard let `self` = self else { return ([], nil, nil) } + guard let json = data?.deserializeJSON(), let entries = json["value"] as? [AnyObject] else { + let err = self.urlError(path, code: .badServerResponse) + return ([], err, nil) + } + + var foundFiles = [FileObject]() + for entry in entries { + if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, subAddress: self.subAddress, json: entry), query.evaluate(with: file.mapPredicate()) { + foundFiles.append(file) + foundItemHandler?(file) + } + } + + return (foundFiles, nil, json["@odata.nextLink"] as? String) + }, completionHandler: completionHandler) } open func url(of path: String, modifier: String? = nil) -> URL { diff --git a/Sources/OneDriveHelper.swift b/Sources/OneDriveHelper.swift index f68273b..84316b6 100644 --- a/Sources/OneDriveHelper.swift +++ b/Sources/OneDriveHelper.swift @@ -90,95 +90,6 @@ public final class OneDriveFileObject: FileObject { } } -// codebeat:disable[ARITY] -internal extension OneDriveFileProvider { - 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.set(httpAuthentication: credential, with: .oAuth2) - let task = session.dataTask(with: request, completionHandler: { (data, response, error) in - var responseError: FileProviderHTTPError? - var files = prevContents - if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { - responseError = self.serverError(with: rCode, path: path, data: data) - } - if let json = data?.deserializeJSON() { - if let entries = json["value"] as? [AnyObject] , entries.count > 0 { - for entry in entries { - if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, subAddress: self.subAddress, json: entry) { - files.append(file) - } - } - 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) - return - } - } - } - completionHandler(files, nil, responseError ?? error) - }) - task.taskDescription = FileOperationType.fetch(path: path).json - task.resume() - } - - func search(_ startPath: String = "", query: String, recursive: Bool, next: URL? = nil, progress: Progress, foundItem: @escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) { - if progress.isCancelled { - return - } - - let url: URL - if let next = next { - url = next - } else { - let bURL = self.baseURL!.appendingPathComponent(subAddress.drivePath).appendingPathComponent("root/search") - var components = URLComponents(url: bURL, resolvingAgainstBaseURL: false)! - let qItem = URLQueryItem(name: "q", value: query) - components.queryItems = [qItem] - if recursive { - components.queryItems?.append(URLQueryItem(name: "expand", value: "children")) - } - url = components.url! - } - - var request = URLRequest(url: url) - request.httpMethod = "GET" - request.set(httpAuthentication: credential, with: .oAuth2) - - let task = session.dataTask(with: request, completionHandler: { (data, response, error) in - var responseError: FileProviderHTTPError? - if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { - responseError = self.serverError(with: rCode, path: startPath, data: data) - } - if let json = data?.deserializeJSON() { - if let entries = json["value"] as? [AnyObject] , entries.count > 0 { - for entry in entries { - if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, subAddress: self.subAddress, json: entry) { - foundItem(file) - } - } - let next: URL? = (json["@odata.nextLink"] as? String).flatMap { URL(string: $0) } - if !progress.isCancelled, let next = next { - self.search(startPath, query: query, recursive: recursive, next: next, progress: progress, foundItem: foundItem, completionHandler: completionHandler) - } else { - completionHandler(responseError ?? error) - } - return - } - } - completionHandler(responseError ?? error) - }) - progress.cancellationHandler = { [weak task] in - task?.cancel() - } - progress.setUserInfoObject(Date(), forKey: .startingTimeKey) - task.resume() - } -} -// codebeat:enable[ARITY] - internal extension OneDriveFileProvider { static let dateFormatter = DateFormatter() static let decimalFormatter = NumberFormatter() diff --git a/Sources/WebDAVFileProvider.swift b/Sources/WebDAVFileProvider.swift index 1f6143a..357249b 100644 --- a/Sources/WebDAVFileProvider.swift +++ b/Sources/WebDAVFileProvider.swift @@ -49,14 +49,12 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { } self.init(baseURL: baseURL, credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential) - self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? "" self.useCache = aDecoder.decodeBool(forKey: "useCache") self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache") } override open func copy(with zone: NSZone? = nil) -> Any { let copy = WebDAVFileProvider(baseURL: self.baseURL!, credential: self.credential, cache: self.cache)! - copy.currentPath = self.currentPath copy.delegate = self.delegate copy.fileOperationDelegate = self.fileOperationDelegate copy.useCache = self.useCache