// // DropboxFileProvider.swift // FileProvider // // Created by Amir Abbas Mousavian. // Copyright © 2016 Mousavian. Distributed under MIT license. // import Foundation import CoreGraphics open class DropboxFileProvider: FileProviderBasicRemote { open class var type: String { return "DropBox" } open let isPathRelative: Bool open let baseURL: URL? open var currentPath: String /// Dropbox RPC API URL, which is equal with [https://api.dropboxapi.com/2/](https://api.dropboxapi.com/2/) open let apiURL: URL /// Dropbox contents download/upload API URL, which is equal with [https://content.dropboxapi.com/2/](https://content.dropboxapi.com/2/) open let contentURL: URL open var dispatch_queue: DispatchQueue open var operation_queue: OperationQueue { willSet { assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.") } } open weak var delegate: FileProviderDelegate? open let credential: URLCredential? open private(set) var cache: URLCache? public var useCache: Bool public var validatingCache: Bool fileprivate var _session: URLSession? fileprivate var sessionDelegate: SessionDelegate? public var session: URLSession { if _session == nil { self.sessionDelegate = SessionDelegate(fileProvider: self, credential: credential) let config = URLSessionConfiguration.default config.urlCache = cache config.requestCachePolicy = .returnCacheDataElseLoad _session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: self.operation_queue) } return _session! } /** 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). There are libraries like [p2/OAuth2](https://github.com/p2/OAuth2) or [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) which can facilate the procedure to retrieve token. The latter is easier to use and prefered. Also you can use [auth0/Lock](https://github.com/auth0/Lock.iOS-OSX) which provides graphical user interface. - Parameter credential: a `URLCredential` object with Client ID set as `user` and Token set as `password`. - Parameter cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used. */ public init(credential: URLCredential?, cache: URLCache? = nil) { self.baseURL = nil self.isPathRelative = true self.currentPath = "" self.useCache = false self.validatingCache = true self.cache = cache self.credential = credential self.apiURL = URL(string: "https://api.dropboxapi.com/2/")! self.contentURL = URL(string: "https://content.dropboxapi.com/2/")! dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent) operation_queue = OperationQueue() operation_queue.name = "FileProvider.\(type(of: self).type).Operation" } deinit { if fileProviderCancelTasksOnInvalidating { _session?.invalidateAndCancel() } else { _session?.finishTasksAndInvalidate() } } open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) { list(path) { (contents, cursor, error) in completionHandler(contents, error) } } open 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" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString] request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var serverError: FileProviderDropboxError? var fileObject: DropboxFileObject? if let response = response as? HTTPURLResponse { let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let file = DropboxFileObject(json: json) { fileObject = file } } completionHandler(fileObject, serverError ?? error) }) task.resume() } open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) { let url = URL(string: "users/get_space_usage", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var totalSize: Int64 = -1 var usedSize: Int64 = 0 if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) { totalSize = ((json["allocation"] as? NSDictionary)?["allocated"] as? NSNumber)?.int64Value ?? -1 usedSize = (json["used"] as? NSNumber)?.int64Value ?? 0 } completionHandler(totalSize, usedSize) }) task.resume() } open weak var fileOperationDelegate: FileOperationDelegate? } extension DropboxFileProvider: FileProviderOperations { public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let path = (atPath as NSString).appendingPathComponent(folderName) + "/" return doOperation(.create(path: path), completionHandler: completionHandler) } public func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let filePath = (path as NSString).appendingPathComponent(fileName) return self.writeContents(path: filePath, contents: data ?? Data(), completionHandler: completionHandler) } public func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler) } public func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler) } public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { return doOperation(.remove(path: path), completionHandler: completionHandler) } fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else { return nil } let url: String guard let sourcePath = operation.source else { return nil } let destPath = operation.destination switch operation { case .create: url = "files/create_folder" case .copy: url = "files/copy" case .move: url = "files/move" case .remove: url = "files/delete" default: // modify, link, fetch return nil } var request = URLRequest(url: URL(string: url, relativeTo: apiURL)!) request.httpMethod = "POST" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") var requestDictionary = [String: AnyObject]() if let dest = correctPath(destPath) as NSString? { requestDictionary["from_path"] = correctPath(sourcePath) as NSString? requestDictionary["to_path"] = dest } else { requestDictionary["path"] = correctPath(sourcePath) as NSString? } request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var serverError: FileProviderDropboxError? if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) { serverError = FileProviderDropboxError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) } completionHandler?(serverError ?? error) self.delegateNotify(operation, error: serverError ?? error) }) task.taskDescription = operation.json task.resume() return RemoteOperationHandle(operationType: operation, tasks: [task]) } public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler) } public func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } let url = URL(string: "files/download", relativeTo: contentURL)! var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") let requestDictionary: [String: AnyObject] = ["path": path as NSString] let requestJson = dictionaryToJSON(requestDictionary) ?? "" request.setValue(requestJson, forHTTPHeaderField: "Dropbox-API-Arg") let task = session.downloadTask(with: request, completionHandler: { (cacheURL, response, error) in guard let cacheURL = cacheURL, let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode < 300 else { let code = FileProviderHTTPErrorCode(rawValue: (response as? HTTPURLResponse)?.statusCode ?? -1) let errorData : Data? = nil //Data(contentsOf:cacheURL) // TODO: Figure out how to get error response data for the error description let serverError : FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil completionHandler?(serverError ?? error) return } do { try FileManager.default.moveItem(at: cacheURL, to: destURL) completionHandler?(nil) } catch let e { completionHandler?(e) } }) task.taskDescription = opType.json task.resume() return RemoteOperationHandle(operationType: opType, tasks: [task]) } } extension DropboxFileProvider: FileProviderReadWrite { public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { if length == 0 || offset < 0 { dispatch_queue.async { completionHandler(Data(), nil) } return nil } let opType = FileOperationType.fetch(path: path) let url = URL(string: "files/download", relativeTo: contentURL)! var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") if length > 0 { request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range") } else if offset > 0 && length < 0 { request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range") } let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString] request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg") let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var serverError: FileProviderDropboxError? if let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: httpResponse.statusCode) { serverError = FileProviderDropboxError(code: code, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) } let filedata = serverError ?? error == nil ? data : nil completionHandler(filedata, serverError ?? error) }) task.taskDescription = opType.json task.resume() return RemoteOperationHandle(operationType: opType, tasks: [task]) } public func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.modify(path: path) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } // FIXME: remove 150MB restriction return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler) } public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { var foundFiles = [DropboxFileObject]() search(path, query: query, foundItem: { (file) in foundFiles.append(file) foundItemHandler?(file) }, completionHandler: { (error) in completionHandler(foundFiles, error) }) } fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) { /* There is two ways to monitor folders changing in Dropbox. Either using webooks * which means you have to implement a server to translate it to push notifications * or using apiv2 list_folder/longpoll method. The second one is implemeted here. * Tough webhooks are much more efficient, longpoll is much simpler to implement! * You can implemnt your own webhook service and replace this method accordingly. */ NotImplemented() } fileprivate func unregisterNotifcation(path: String) { NotImplemented() } // TODO: Implement /get_account & /get_current_account } extension DropboxFileProvider { /// *DEPRECATED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead. @available(*, deprecated, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.") open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) { self.publicLink(to: path) { (url, file, _, error) in completionHandler(url, file, error) } } /// *DEPRECATED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead. @available(*, deprecated, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.") open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) { self.publicLink(to: path) { (url, file, expiration, error) in completionHandler(url, file, expiration, error) } } /** Genrates a public url to a file to be shared with other users and can be downloaded without authentication. - Important: URL will be available for a limitied time (4 hours according to Dropbox documentation). - Parameters: - to: path of file, including file/directory name. - completionHandler: a block with result of directory entries or error. `link`: a url returned by Dropbox to share. `attribute`: a `FileObject` containing the attributes of the item. `expiration`: a `Date` object, determines when the public url will expires. `error`: Error returned by Dropbox. */ open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) { let url = URL(string: "files/get_temporary_link", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString] request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var serverError: FileProviderDropboxError? var link: URL? var fileObject: DropboxFileObject? if let response = response as? HTTPURLResponse { let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) { if let linkStr = json["link"] as? String { link = URL(string: linkStr) } if let attribDic = json["metadata"] as? [String: AnyObject] { fileObject = DropboxFileObject(json: attribDic) } } } let expiration: Date? = link != nil ? Date(timeIntervalSinceNow: 4 * 60 * 60) : nil completionHandler(link, fileObject, expiration, serverError ?? error) }) task.resume() } /** Downloads a file from remote url to designated path asynchronously. - Parameters: - remoteURL: a valid remote url to file. - to: Destination path of file, including file/directory name. - completionHandler: a block with result of directory entries or error. `jobId`: Job ID returned by Dropbox to monitor the copy/download progress. `attribute`: A `FileObject` containing the attributes of the item. `error`: Error returned by Dropbox. */ open func copyItem(remoteURL: URL, to toPath: String, completionHandler: @escaping ((_ jobId: String?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) { if remoteURL.isFileURL { completionHandler(nil, nil, self.throwError(remoteURL.path, code: URLError.badURL)) return } let url = URL(string: "files/save_url", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "url" : remoteURL.absoluteString as NSString] request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var serverError: FileProviderDropboxError? var jobId: String? var fileObject: DropboxFileObject? if let response = response as? HTTPURLResponse { let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) serverError = code != nil ? FileProviderDropboxError(code: code!, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) { jobId = json["async_job_id"] as? String if let attribDic = json["metadata"] as? [String: AnyObject] { fileObject = DropboxFileObject(json: attribDic) } } } completionHandler(jobId, fileObject, serverError ?? error) }) task.resume() } /** Copys a file from another user Dropbox storage to designated path asynchronously. - Parameters: - reference: a valid reference string from another user via `copy_reference/get` REST method. - to: Destination path of file, including file/directory name. - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. */ open func copyItem(reference: String, to toPath: String, completionHandler: SimpleCompletionHandler) { let url = URL(string: "files/copy_reference/save", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "copy_reference" : reference as NSString] request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var serverError: FileProviderDropboxError? if let response = response as? HTTPURLResponse { let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) serverError = code != nil ? FileProviderDropboxError(code: code!, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil } completionHandler?(serverError ?? error) }) task.resume() } } extension DropboxFileProvider: ExtendedFileProvider { public func thumbnailOfFileSupported(path: String) -> Bool { switch (path as NSString).pathExtension.lowercased() { case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff": return true case "doc", "docx", "docm", "xls", "xlsx", "xlsm": return true case "ppt", "pps", "ppsx", "ppsm", "pptx", "pptm": return true case "rtf": return true default: return false } } public func propertiesOfFileSupported(path: String) -> Bool { let fileExt = (path as NSString).pathExtension.lowercased() switch fileExt { case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff": return true /*case "mp3", "aac", "m4a": return true*/ case "mp4", "mpg", "3gp", "mov", "avi": return true default: return false } } /// Default value for dimension is 64x64, according to Dropbox documentation public func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) { let url: URL switch (path as NSString).pathExtension.lowercased() { case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff": url = URL(string: "files/get_thumbnail", relativeTo: contentURL)! case "doc", "docx", "docm", "xls", "xlsx", "xlsm": fallthrough case "ppt", "pps", "ppsx", "ppsm", "pptx", "pptm": fallthrough case "rtf": url = URL(string: "files/get_preview", relativeTo: contentURL)! default: return } var request = URLRequest(url: url) request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") var requestDictionary: [String: AnyObject] = ["path": path as NSString] requestDictionary["format"] = "jpeg" as NSString if let dimension = dimension { requestDictionary["size"] = "w\(Int(dimension.width))h\(Int(dimension.height))" as NSString } request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg") let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in var image: ImageClass? = nil if let r = response as? HTTPURLResponse, let result = r.allHeaderFields["Dropbox-API-Result"] as? String, let jsonResult = jsonToDictionary(result) { if jsonResult["error"] != nil { completionHandler(nil, self.throwError(path, code: URLError.cannotDecodeRawData as FoundationErrorEnum)) } } if let data = data { if DropboxFileProvider.dataIsPDF(data) { image = DropboxFileProvider.convertToImage(pdfData: data) } else if let contentType = (response as? HTTPURLResponse)?.allHeaderFields["Content-Type"] as? String, contentType.contains("text/html") { // TODO: Implement converting html returned type of get_preview to image } else { image = ImageClass(data: data) } } completionHandler(image, error) }) task.resume() } public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) { let url = URL(string: "files/get_metadata", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)] request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var serverError: FileProviderDropboxError? var dic = [String: Any]() var keys = [String]() if let response = response as? HTTPURLResponse { let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let properties = json["media_info"] as? [String: Any] { (dic, keys) = self.mapMediaInfo(properties) } } completionHandler(dic, keys, serverError ?? error) }) task.resume() } } extension DropboxFileProvider: FileProvider { 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 copy.validatingCache = self.validatingCache return copy } }