From b380685932bb7cb324e87f0dc61bd01bdad64fd1 Mon Sep 17 00:00:00 2001 From: Amir Abbas Date: Tue, 5 Sep 2017 02:26:09 +0430 Subject: [PATCH] Added VolumeObject for storageProperties method - Refined error handling in HTTP provider - Added contentType and hash to OneDriveFileObject --- Sources/CloudFileProvider.swift | 8 +-- Sources/DropboxFileProvider.swift | 22 ++++---- Sources/DropboxHelper.swift | 7 +-- Sources/FTPFileProvider.swift | 14 ++--- Sources/FileObject.swift | 91 ++++++++++++++++++++++++++++++ Sources/FileProvider.swift | 46 ++++++--------- Sources/HTTPFileProvider.swift | 54 ++++++++---------- Sources/LocalFileProvider.swift | 14 +++-- Sources/OneDriveFileProvider.swift | 25 ++++---- Sources/OneDriveHelper.swift | 27 ++++++--- Sources/SMBFileProvider.swift | 6 +- Sources/WebDAVFileProvider.swift | 69 ++++++++++++---------- 12 files changed, 245 insertions(+), 138 deletions(-) diff --git a/Sources/CloudFileProvider.swift b/Sources/CloudFileProvider.swift index ce85a4a..4fec59c 100644 --- a/Sources/CloudFileProvider.swift +++ b/Sources/CloudFileProvider.swift @@ -123,7 +123,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { `contents`: An array of `FileObject` identifying the the directory entries. `error`: Error returned by system. */ - open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) { + open override func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) { // FIXME: create runloop for dispatch_queue, start query on it dispatch_queue.async { let pathURL = self.url(of: path) @@ -178,7 +178,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { /// Please don't rely this function to get iCloud drive total and remaining capacity /// - Important: iCloud Storage size and free space is unavailable, it returns local space - open override func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) { + open override func storageProperties(completionHandler: @escaping (VolumeObject?) -> Void) { super.storageProperties(completionHandler: completionHandler) } @@ -192,7 +192,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { `attributes`: A `FileObject` containing the attributes of the item. `error`: Error returned by system. */ - open override func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) { + open override func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) { dispatch_queue.async { let pathURL = self.url(of: path) let query = NSMetadataQuery() @@ -249,7 +249,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { - 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)) -> Progress? { + open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { let mapDict: [String: String] = ["url": NSMetadataItemURLKey, "name": NSMetadataItemFSNameKey, "path": NSMetadataItemPathKey, "filesize": NSMetadataItemFSSizeKey, "modifiedDate": NSMetadataItemFSContentChangeDateKey, "creationDate": NSMetadataItemFSCreationDateKey, "contentType": NSMetadataItemContentTypeKey] diff --git a/Sources/DropboxFileProvider.swift b/Sources/DropboxFileProvider.swift index 39f6f8d..c550bfe 100644 --- a/Sources/DropboxFileProvider.swift +++ b/Sources/DropboxFileProvider.swift @@ -57,14 +57,14 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { return copy } - open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) { + open override func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) { let progress = Progress(parent: nil, userInfo: nil) list(path, progress: progress) { (contents, cursor, error) in completionHandler(contents, error) } } - open override func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) { + open override func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) { let url = URL(string: "files/get_metadata", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" @@ -87,24 +87,26 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { task.resume() } - open override func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) { + open override func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) { let url = URL(string: "users/get_space_usage", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" request.set(httpAuthentication: credential, with: .oAuth2) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in - var totalSize: Int64 = -1 - var usedSize: Int64 = 0 - if let json = data?.deserializeJSON() { - totalSize = ((json["allocation"] as? NSDictionary)?["allocated"] as? NSNumber)?.int64Value ?? -1 - usedSize = (json["used"] as? NSNumber)?.int64Value ?? 0 + guard let json = data?.deserializeJSON() else { + completionHandler(nil) + return } - completionHandler(totalSize, usedSize) + + let volume = VolumeObject(allValues: [:]) + volume.totalCapacity = ((json["allocation"] as? NSDictionary)?["allocated"] as? NSNumber)?.int64Value ?? -1 + volume.usage = (json["used"] as? NSNumber)?.int64Value ?? 0 + completionHandler(volume) }) task.resume() } - open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? { + open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { let progress = Progress(parent: nil, userInfo: nil) var foundFiles = [DropboxFileObject]() if let queryStr = query.findValue(forKey: "name", operator: .beginsWith) as? String { diff --git a/Sources/DropboxHelper.swift b/Sources/DropboxHelper.swift index e26c6cf..e23fbf2 100644 --- a/Sources/DropboxHelper.swift +++ b/Sources/DropboxHelper.swift @@ -51,13 +51,12 @@ public final class DropboxFileObject: FileObject { /// The document identifier is a value assigned by the Dropbox to a file. /// This value is used to identify the document regardless of where it is moved on a volume. - /// The identifier persists across system restarts. open internal(set) var id: String? { get { - return allValues[.documentIdentifierKey] as? String + return allValues[.fileResourceIdentifierKey] as? String } set { - allValues[.documentIdentifierKey] = newValue + allValues[.fileResourceIdentifierKey] = newValue } } @@ -75,8 +74,6 @@ public final class DropboxFileObject: FileObject { // codebeat:disable[ARITY] internal extension DropboxFileProvider { - - 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 } diff --git a/Sources/FTPFileProvider.swift b/Sources/FTPFileProvider.swift index 41c05a4..f4e8a93 100644 --- a/Sources/FTPFileProvider.swift +++ b/Sources/FTPFileProvider.swift @@ -152,7 +152,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil internal var serverSupportsRFC3659: Bool = true - open func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) { + open func contentsOfDirectory(path: String, completionHandler: @escaping ([FileObject], Error?) -> Void) { self.contentsOfDirectory(path: path, rfc3659enabled: serverSupportsRFC3659, completionHandler: completionHandler) } @@ -167,7 +167,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil `contents`: An array of `FileObject` identifying the the directory entries. `error`: Error returned by system. */ - open func contentsOfDirectory(path apath: String, rfc3659enabled: Bool , completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) { + open func contentsOfDirectory(path apath: String, rfc3659enabled: Bool , completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) { let path = ftpPath(apath) let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!) @@ -207,7 +207,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil } } - open func attributesOfItem(path: String, completionHandler: @escaping ((FileObject?, Error?) -> Void)) { + open func attributesOfItem(path: String, completionHandler: @escaping (FileObject?, Error?) -> Void) { self.attributesOfItem(path: path, rfc3659enabled: serverSupportsRFC3659, completionHandler: completionHandler) } @@ -222,7 +222,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil `attributes`: A `FileObject` containing the attributes of the item. `error`: Error returned by system. */ - open func attributesOfItem(path apath: String, rfc3659enabled: Bool, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) { + open func attributesOfItem(path apath: String, rfc3659enabled: Bool, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) { let path = ftpPath(apath) let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!) @@ -270,13 +270,13 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil } } - open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) { + open func storageProperties(completionHandler: @escaping (_ volume: VolumeObject?) -> Void) { dispatch_queue.async { - completionHandler(-1, 0) + completionHandler(nil) } } - open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? { + open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { let progress = Progress(parent: nil, userInfo: nil) if recursive { return self.recursiveList(path: path, useMLST: true, foundItemsHandler: { items in diff --git a/Sources/FileObject.swift b/Sources/FileObject.swift index 1e983df..39fd156 100644 --- a/Sources/FileObject.swift +++ b/Sources/FileObject.swift @@ -207,6 +207,97 @@ open class FileObject: Equatable { } } +/// Containts attributes of a provider. +open class VolumeObject { + /// A `Dictionary` contains volume information, using `URLResourceKey` keys. + open internal(set) var allValues: [URLResourceKey: Any] + + public init(allValues: [URLResourceKey: Any]) { + self.allValues = allValues + } + + /// The root directory of the resource’s volume, returned as an `URL` object. + open internal(set) var url: URL? { + get { + return allValues[.volumeURLKey] as? URL + } + set { + allValues[.volumeURLKey] = newValue + } + } + + /// The name of the volume. + open internal(set) var name: String? { + get { + return allValues[.volumeNameKey] as? String + } + set { + allValues[.volumeNameKey] = newValue + } + } + + + /// the volume’s capacity in bytes, return -1 if is undetermined. + open internal(set) var totalCapacity: Int64 { + get { + return allValues[.volumeTotalCapacityKey] as? Int64 ?? -1 + } + set { + allValues[.volumeTotalCapacityKey] = newValue + } + } + + /// The volume’s available capacity in bytes. + open internal(set) var availableCapacity: Int64 { + get { + return allValues[.volumeAvailableCapacityKey] as? Int64 ?? 0 + } + set { + allValues[.volumeAvailableCapacityKey] = newValue + } + } + + open internal(set) var usage: Int64 { + get { + return totalCapacity >= 0 ? totalCapacity - availableCapacity : -availableCapacity + } + set { + availableCapacity = totalCapacity >= 0 ? totalCapacity - newValue : -newValue + } + } + + /// the volume’s creation date, returned as an `Date` object, or NULL if it cannot be determined + open internal(set) var creationDate: Date? { + get { + return allValues[.volumeCreationDateKey] as? Date + } + set { + allValues[.volumeCreationDateKey] = newValue + } + } + + /// Determining whether the volume is read-only + open internal(set) var isReadOnly: Bool { + get { + return allValues[.volumeIsReadOnlyKey] as? Bool ?? false + } + set { + allValues[.volumeIsReadOnlyKey] = newValue + } + } + + @available(iOS 10.0, macOS 10.12, tvOS 10.0, *) + open internal(set) var isEncrypted: Bool { + get { + return allValues[.volumeIsEncryptedKey] as? Bool ?? false + } + set { + allValues[.volumeIsEncryptedKey] = !newValue + } + } +} + + /// Sorting FileObject array by given criteria, **not thread-safe** public struct FileObjectSorting { diff --git a/Sources/FileProvider.swift b/Sources/FileProvider.swift index 3562994..c4b1234 100644 --- a/Sources/FileProvider.swift +++ b/Sources/FileProvider.swift @@ -82,11 +82,11 @@ public protocol FileProviderBasic: class, NSSecureCoding { - `attributes`: A `FileObject` containing the attributes of the item. - `error`: Error returned by system. */ - func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) + func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) - /// Returns total and used capacity in provider container asynchronously. - func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) + /// Returns volume/provider information asynchronously. + func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) /** Search files inside directory using query asynchronously. @@ -101,7 +101,7 @@ public protocol FileProviderBasic: class, NSSecureCoding { - completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured. */ @discardableResult - func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? + func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? /** Search files inside directory using query asynchronously. @@ -126,7 +126,7 @@ public protocol FileProviderBasic: class, NSSecureCoding { - Returns: An `Progress` to get progress or cancel progress. */ @discardableResult - func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? + func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? /** Returns an independent url to access the file. Some providers like `Dropbox` due to their nature. @@ -150,7 +150,7 @@ public protocol FileProviderBasic: class, NSSecureCoding { } extension FileProviderBasic { - public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? { + public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { let predicate = NSPredicate(format: "name BEGINSWITH[c] %@", query) return self.searchFiles(path: path, recursive: recursive, query: predicate, foundItemHandler: foundItemHandler, completionHandler: completionHandler) } @@ -166,6 +166,14 @@ extension FileProviderBasic { operation_queue.maxConcurrentOperationCount = newValue } } + + /// Returns total and used capacity in provider container asynchronously. + @available(*, deprecated, message: "Use storageProperties which returns VolumeObject") + func storageProperties(completionHandler: @escaping (_ total: Int64, _ used: Int64) -> Void) { + self.storageProperties { (info) in + completionHandler(info?.totalCapacity ?? -1, info?.usage ?? 0) + } + } } /// Checking equality of two file provider, regardless of current path queues and delegates. @@ -783,7 +791,7 @@ public protocol ExtendedFileProvider: FileProviderBasic { func propertiesOfFileSupported(path: String) -> Bool /** - Generates ans returns a thumbnail preview of document asynchronously. The defualt dimension of returned image is different + Generates and returns a thumbnail preview of document asynchronously. The defualt dimension of returned image is different regarding provider type, usually 64x64 pixels. - Parameters: @@ -795,7 +803,7 @@ public protocol ExtendedFileProvider: FileProviderBasic { func thumbnailOfFile(path: String, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) /** - Generates ans returns a thumbnail preview of document asynchronously. The defualt dimension of returned image is different + Generates and returns a thumbnail preview of document asynchronously. The defualt dimension of returned image is different regarding provider type, usually 64x64 pixels. Default value used when `dimenstion` is `nil`. - Note: `LocalFileInformationGenerator` variables can be set to change default behavior of @@ -1027,26 +1035,8 @@ public enum FileOperationType: CustomStringConvertible { } /// Allows to get progress or cancel an in-progress operation, useful for remote providers -@available(*, obsoleted: 1.0, message: "Use Progress class class instead.") -public protocol OperationHandle { - /// Operation supposed to be done on files. Contains file paths as associated value. - var operationType: FileOperationType { get } - - /// Bytes written/read by operation so far. - var bytesSoFar: Int64 { get } - - /// Total bytes of operation. - var totalBytes: Int64 { get } - - /// Operation is progress or not, Returns false if operation is done or not initiated yet. - var inProgress: Bool { get } - - /// Progress of operation, usually equals with `bytesSoFar/totalBytes`. or NaN if not available. - var progress: Float { get } - - /// Cancels operation while in progress, or cancels data/download/upload url session task. - func cancel() -> Bool -} +@available(*, obsoleted: 1.0, message: "Use Foudation.Progress class instead.") +public protocol OperationHandle {} /// Delegate methods for reporting provider's operation result and progress, when it's ready to update /// user interface. diff --git a/Sources/HTTPFileProvider.swift b/Sources/HTTPFileProvider.swift index 16381e9..bb675c1 100644 --- a/Sources/HTTPFileProvider.swift +++ b/Sources/HTTPFileProvider.swift @@ -12,7 +12,7 @@ import Foundation The abstract base class for all REST/Web based providers such as WebDAV, Dropbox, OneDrive, Google Drive, etc. and encapsulates basic functionalitis such as downloading/uploading. - No instance of this class should (and can) be created. Use derivated classes instead. It leads to a crash with `fatalError()`. + No instance of this class should (and can) be created. Use derived classes instead. It leads to a crash with `fatalError()`. */ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite { open class var type: String { fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") } @@ -132,25 +132,25 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi } } - open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) { + open func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) { fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") } - open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) { + open func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) { fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") } - open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) { + open func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) { fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") } - open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? { + open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") } open func isReachable(completionHandler: @escaping (Bool) -> Void) { - self.storageProperties { total, _ in - completionHandler(total > 0) + self.storageProperties { volume in + completionHandler(volume != nil) } } @@ -193,20 +193,17 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? { let operation = FileOperationType.copy(source: path, destination: destURL.absoluteString) let request = self.request(for: operation) + let cantLoadError = throwError(path, code: .cannotLoadFromNetwork) return self.download_simple(path: path, request: request, operation: operation, completionHandler: { [weak self] (tempURL, error) in - if let error = error { - completionHandler?(error) - self?.delegateNotify(operation, error: error) - return - } - - guard let tempURL = tempURL else { - completionHandler?(error) - self?.delegateNotify(operation, error: error) - return - } - do { + if let error = error { + throw error + } + + guard let tempURL = tempURL else { + throw cantLoadError + } + try FileManager.default.moveItem(at: tempURL, to: destURL) completionHandler?(nil) self?.delegateNotify(operation) @@ -227,19 +224,18 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi let operation = FileOperationType.fetch(path: path) var request = self.request(for: operation) + let cantLoadError = throwError(path, code: .cannotLoadFromNetwork) request.set(httpRangeWithOffset: offset, length: length) return self.download_simple(path: path, request: request, operation: operation, completionHandler: { (tempURL, error) in - if let error = error { - completionHandler(nil, error) - return - } - - guard let tempURL = tempURL else { - completionHandler(nil, error) - return - } - do { + if let error = error { + throw error + } + + guard let tempURL = tempURL else { + throw cantLoadError + } + let data = try Data(contentsOf: tempURL) completionHandler(data, nil) } catch { diff --git a/Sources/LocalFileProvider.swift b/Sources/LocalFileProvider.swift index 7de94b3..46ae3ae 100644 --- a/Sources/LocalFileProvider.swift +++ b/Sources/LocalFileProvider.swift @@ -166,11 +166,15 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } } - open func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) { - let values = try? baseURL?.resourceValues(forKeys: [.volumeTotalCapacityKey, .volumeAvailableCapacityKey]) - let totalSize = Int64(values??.volumeTotalCapacity ?? -1) - let freeSize = Int64(values??.volumeAvailableCapacity ?? 0) - completionHandler(totalSize, totalSize - freeSize) + public func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) { + dispatch_queue.async { + var keys: Set = [.volumeTotalCapacityKey, .volumeAvailableCapacityKey, .volumeURLKey, .volumeNameKey, .volumeIsReadOnlyKey, .volumeCreationDateKey] + if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { + keys.insert(.isEncryptedKey) + } + let values: URLResourceValues? = self.baseURL.flatMap { try? $0.resourceValues(forKeys: keys) } + completionHandler(values.flatMap({ VolumeObject(allValues: $0.allValues) })) + } } open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? { diff --git a/Sources/OneDriveFileProvider.swift b/Sources/OneDriveFileProvider.swift index 878f3a8..dcbbe27 100644 --- a/Sources/OneDriveFileProvider.swift +++ b/Sources/OneDriveFileProvider.swift @@ -67,13 +67,13 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { return copy } - open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) { + open override func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) { list(path) { (contents, cursor, error) in completionHandler(contents, error) } } - open override func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) { + open override func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) { var request = URLRequest(url: url(of: path)) request.httpMethod = "GET" request.set(httpAuthentication: credential, with: .oAuth2) @@ -92,23 +92,28 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { task.resume() } - open override func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) { + open override func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) { var request = URLRequest(url: url(of: "")) request.httpMethod = "GET" request.set(httpAuthentication: credential, with: .oAuth2) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in - var totalSize: Int64 = -1 - var usedSize: Int64 = 0 - if let json = data?.deserializeJSON() { - totalSize = (json["total"] as? NSNumber)?.int64Value ?? -1 - usedSize = (json["used"] as? NSNumber)?.int64Value ?? 0 + guard let json = data?.deserializeJSON() else { + completionHandler(nil) + return } - completionHandler(totalSize, usedSize) + + let volume = VolumeObject(allValues: [:]) + volume.url = request.url + volume.name = json["name"] as? String + volume.creationDate = (json["createdDateTime"] as? String).flatMap { Date(rfcString: $0) } + volume.totalCapacity = (json["quota"]?["total"] as? NSNumber)?.int64Value ?? -1 + volume.availableCapacity = (json["quota"]?["remaining"] as? NSNumber)?.int64Value ?? 0 + completionHandler(volume) }) task.resume() } - open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? { + 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 diff --git a/Sources/OneDriveHelper.swift b/Sources/OneDriveHelper.swift index bf2a16c..7763808 100644 --- a/Sources/OneDriveHelper.swift +++ b/Sources/OneDriveHelper.swift @@ -34,35 +34,38 @@ 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 } + guard let path = json["parentReference"]?["path"] as? String else { return 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 = Date(rfcString: json["lastModifiedDateTime"] as? String ?? "") - self.creationDate = Date(rfcString: json["createdDateTime"] as? String ?? "") + self.modifiedDate = (json["lastModifiedDateTime"] as? String).flatMap { Date(rfcString: $0) } + self.creationDate = (json["createdDateTime"] as? String).flatMap { Date(rfcString: $0) } self.type = json["folder"] != nil ? .directory : .regular + self.contentType = json["file"]?["mimeType"] as? String ?? "application/octet-stream" self.id = json["id"] as? String self.entryTag = json["eTag"] as? String + let hashes = json["file"]?["hashes"] as? NSDictionary + // checks for both sha1 or quickXor. First is available in personal drives, second in business one. + self.hash = (hashes?["sha1Hash"] as? String) ?? (hashes?["quickXorHash"] as? String) } /// The document identifier is a value assigned by the OneDrive to a file. /// This value is used to identify the document regardless of where it is moved on a volume. - /// The identifier persists across system restarts. open internal(set) var id: String? { get { - return allValues[.documentIdentifierKey] as? String + return allValues[.fileResourceIdentifierKey] as? String } set { - allValues[.documentIdentifierKey] = newValue + allValues[.fileResourceIdentifierKey] = newValue } } /// MIME type of file contents returned by OneDrive server. open internal(set) var contentType: String { get { - return allValues[.mimeTypeKey] as? String ?? "" + return allValues[.mimeTypeKey] as? String ?? "application/octet-stream" } set { allValues[.mimeTypeKey] = newValue @@ -78,6 +81,16 @@ public final class OneDriveFileObject: FileObject { allValues[.entryTagKey] = newValue } } + + /// Calculated hash from OneDrive server. Hex string SHA1 in personal or Base65 string [QuickXOR](https://dev.onedrive.com/snippets/quickxorhash.htm) in business drives. + open internal(set) var hash: String? { + get { + return allValues[.documentIdentifierKey] as? String + } + set { + allValues[.documentIdentifierKey] = newValue + } + } } // codebeat:disable[ARITY] diff --git a/Sources/SMBFileProvider.swift b/Sources/SMBFileProvider.swift index ee445d7..6f5338d 100644 --- a/Sources/SMBFileProvider.swift +++ b/Sources/SMBFileProvider.swift @@ -56,15 +56,15 @@ class SMBFileProvider: FileProvider, FileProviderMonitor { return true } - open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObjectClass], _ error: Error?) -> Void)) { + open func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObjectClass], _ error: Error?) -> Void) { NotImplemented() } - open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObjectClass?, _ error: Error?) -> Void)) { + open func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObjectClass?, _ error: Error?) -> Void) { NotImplemented() } - open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) { + open func storageProperties(completionHandler: @escaping (_ volume: VolumeObject?) -> Void) { NotImplemented() } diff --git a/Sources/WebDAVFileProvider.swift b/Sources/WebDAVFileProvider.swift index 0f1b460..1ac3fb8 100644 --- a/Sources/WebDAVFileProvider.swift +++ b/Sources/WebDAVFileProvider.swift @@ -22,6 +22,9 @@ import CoreGraphics */ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { override open class var type: String { return "WebDAV" } + + /// An enum which defines HTTP Authentication method, usually you should it default `.digest`. + /// If the server uses OAuth authentication, credential must be set with token as `password`, like Dropbox. public var credentialType: URLRequest.AuthenticationType = .digest /** @@ -36,8 +39,8 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { if !["http", "https"].contains(baseURL.uw_scheme.lowercased()) { return nil } - let refinedBaseURL = (baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")).absoluteURL - super.init(baseURL: refinedBaseURL, credential: credential, cache: cache) + let refinedBaseURL = (baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")) + super.init(baseURL: refinedBaseURL.absoluteURL, credential: credential, cache: cache) } public required convenience init?(coder aDecoder: NSCoder) { @@ -76,7 +79,7 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { - `contents`: An array of `FileObject` identifying the the directory entries. - `error`: Error returned by system. */ - open func contentsOfDirectory(path: String, including: [URLResourceKey], completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) { + open func contentsOfDirectory(path: String, including: [URLResourceKey], completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) { let operation = FileOperationType.fetch(path: path) let url = self.url(of: path).appendingPathComponent("") var request = URLRequest(url: url) @@ -84,8 +87,7 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { request.setValue("1", forHTTPHeaderField: "Depth") request.set(httpAuthentication: credential, with: credentialType) request.set(httpContentType: .xml, charset: .utf8) - request.httpBody = "\n\n\(WebDavFileObject.propString(including))\n".data(using: .utf8) - request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length") + request.httpBody = WebDavFileObject.xmlProp(including) runDataTask(with: request, operation: operation, completionHandler: { (data, response, error) in var responseError: FileProviderWebDavError? if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { @@ -105,7 +107,7 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { }) } - override open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) { + override open func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) { self.attributesOfItem(path: path, including: [], completionHandler: completionHandler) } @@ -120,15 +122,14 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { - `attributes`: A `FileObject` containing the attributes of the item. - `error`: Error returned by system. */ - open func attributesOfItem(path: String, including: [URLResourceKey], completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) { + open func attributesOfItem(path: String, including: [URLResourceKey], completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) { let url = self.url(of: path) var request = URLRequest(url: url) request.httpMethod = "PROPFIND" request.setValue("0", forHTTPHeaderField: "Depth") request.set(httpAuthentication: credential, with: credentialType) request.set(httpContentType: .xml, charset: .utf8) - request.httpBody = "\n\n\(WebDavFileObject.propString(including))\n".data(using: .utf8) - request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length") + request.httpBody = WebDavFileObject.xmlProp(including) runDataTask(with: request, completionHandler: { (data, response, error) in var responseError: FileProviderWebDavError? if let code = (response as? HTTPURLResponse)?.statusCode, code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { @@ -145,7 +146,7 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { }) } - override open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) { + override open func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) { // Not all WebDAV clients implements RFC2518 which allows geting storage quota. // In this case you won't get error. totalSize is NSURLSessionTransferSizeUnknown // and used space is zero. @@ -157,23 +158,24 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { request.setValue("0", forHTTPHeaderField: "Depth") request.set(httpAuthentication: credential, with: credentialType) request.set(httpContentType: .xml, charset: .utf8) - request.httpBody = "\n\n\n".data(using: .utf8) - request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length") + request.httpBody = WebDavFileObject.xmlProp([.volumeTotalCapacityKey, .volumeAvailableCapacityKey, .creationDateKey]) runDataTask(with: request, completionHandler: { (data, response, error) in - var totalSize: Int64 = -1 - var usedSize: Int64 = 0 - if let data = data { - let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL) - if let attr = xresponse.first { - totalSize = Int64(attr.prop["quota-available-bytes"] ?? "") ?? -1 - usedSize = Int64(attr.prop["quota-used-bytes"] ?? "") ?? 0 - } + guard let data = data, let attr = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL).first else { + completionHandler(nil) + return } - completionHandler(totalSize, usedSize) + + let volume = VolumeObject(allValues: [:]) + volume.creationDate = attr.prop["creationdate"].flatMap { Date(rfcString: $0) } + volume.availableCapacity = attr.prop["quota-available-bytes"].flatMap({ Int64($0) }) ?? 0 + if let usage = attr.prop["quota-used-bytes"].flatMap({ Int64($0) }) { + volume.totalCapacity = volume.availableCapacity + usage + } + completionHandler(volume) }) } - override open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? { + override open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { let url = self.url(of: path) var request = URLRequest(url: url) request.httpMethod = "PROPFIND" @@ -181,7 +183,7 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { request.setValue(recursive ? "infinity" : "1", forHTTPHeaderField: "Depth") request.set(httpAuthentication: credential, with: credentialType) request.set(httpContentType: .xml, charset: .utf8) - request.httpBody = "\n\n".data(using: .utf8) + request.httpBody = WebDavFileObject.xmlProp([]) let progress = Progress(parent: nil, userInfo: nil) progress.setUserInfoObject(url, forKey: .fileURLKey) let task = session.dataTask(with: request) { (data, response, error) in @@ -222,8 +224,7 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { request.setValue("0", forHTTPHeaderField: "Depth") request.set(httpAuthentication: credential, with: credentialType) request.set(httpContentType: .xml, charset: .utf8) - request.httpBody = "\n\n\n".data(using: .utf8) - request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length") + request.httpBody = WebDavFileObject.xmlProp([.volumeTotalCapacityKey, .volumeAvailableCapacityKey]) runDataTask(with: request, completionHandler: { (data, response, error) in let status = (response as? HTTPURLResponse)?.statusCode ?? 400 completionHandler(status < 300) @@ -245,7 +246,6 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { request.set(httpContentType: .xml, charset: .utf8) let body = "\n\ntrue\n\n" request.httpBody = body.data(using: .utf8) - request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length") runDataTask(with: request, completionHandler: { (data, response, error) in var responseError: FileProviderWebDavError? if let code = (response as? HTTPURLResponse)?.statusCode, code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { @@ -497,9 +497,9 @@ public final class WebDavFileObject: FileObject { let path = relativePath.hasPrefix("/") ? relativePath : ("/" + relativePath) super.init(url: href, name: name, path: path) self.size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown - self.creationDate = Date(rfcString: davResponse.prop["creationdate"] ?? "") - self.modifiedDate = Date(rfcString: davResponse.prop["getlastmodified"] ?? "") - self.contentType = davResponse.prop["getcontenttype"] ?? "octet/stream" + self.creationDate = davResponse.prop["creationdate"].flatMap { Date(rfcString: $0) } + self.modifiedDate = davResponse.prop["getlastmodified"].flatMap { Date(rfcString: $0) } + self.contentType = davResponse.prop["getcontenttype"] ?? "application/octet-stream" self.isHidden = (Int(davResponse.prop["ishidden"] ?? "0") ?? 0) > 0 self.type = self.contentType == "httpd/unix-directory" ? .directory : .regular self.entryTag = davResponse.prop["getetag"] @@ -508,7 +508,7 @@ public final class WebDavFileObject: FileObject { /// MIME type of the file. open internal(set) var contentType: String { get { - return allValues[.mimeTypeKey] as? String ?? "" + return allValues[.mimeTypeKey] as? String ?? "application/octet-stream" } set { allValues[.mimeTypeKey] = newValue @@ -539,6 +539,11 @@ public final class WebDavFileObject: FileObject { return "ishidden" case URLResourceKey.entryTagKey: return "getetag" + case URLResourceKey.volumeTotalCapacityKey: + // WebDAV doesn't have total capacity, but it's can be calculated via used capacity + return "quota-used-bytes" + case URLResourceKey.volumeAvailableCapacityKey: + return "quota-available-bytes" default: return nil } @@ -556,6 +561,10 @@ public final class WebDavFileObject: FileObject { } return propKeys } + + internal class func xmlProp(_ keys: [URLResourceKey]) -> Data { + return "\n\n\(WebDavFileObject.propString(keys))\n".data(using: .utf8)! + } } /// Error returned by WebDAV server when trying to access or do operations on a file or folder.