Files
FileProvider/Sources/DropboxFileProvider.swift
2019-04-04 12:42:17 +04:30

511 lines
24 KiB
Swift

//
// DropboxFileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2016 Mousavian. Distributed under MIT license.
//
import Foundation
#if os(macOS) || os(iOS) || os(tvOS)
import CoreGraphics
#endif
/**
Allows accessing to Dropbox stored files. This provider doesn't cache or save files internally, however you can
set `useCache` and `cache` properties to use Foundation `NSURLCache` system.
- Note: You can pass file id or rev instead of file path, e.g `"id:1234abcd"` or `"rev:1234abcd"`, to point to a file or folder by ID.
- Note: Uploading files and data are limited to 150MB, for now.
*/
open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
override open class var type: String { return "Dropbox" }
/// Dropbox RPC API URL, which is equal with [https://api.dropboxapi.com/2/](https://api.dropboxapi.com/2/)
public let apiURL: URL
/// Dropbox contents download/upload API URL, which is equal with [https://content.dropboxapi.com/2/](https://content.dropboxapi.com/2/)
public let contentURL: URL
/**
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.
- 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.
*/
public init(credential: URLCredential?, cache: URLCache? = nil) {
self.apiURL = URL(string: "https://api.dropboxapi.com/2/")!
self.contentURL = URL(string: "https://content.dropboxapi.com/2/")!
super.init(baseURL: nil, credential: credential, cache: cache)
}
public required convenience init?(coder aDecoder: NSCoder) {
self.init(credential: aDecoder.decodeObject(of: URLCredential.self, forKey: "credential"))
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.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
copy.useCache = self.useCache
copy.validatingCache = self.validatingCache
return copy
}
/**
Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler.
If the directory contains no entries or an error is occured, this method will return the empty array.
- Parameters:
- path: path to target directory. If empty, root will be iterated.
- completionHandler: a closure with result of directory entries or error.
- contents: An array of `FileObject` identifying the the directory entries.
- error: Error returned by system.
*/
open override func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
let query = NSPredicate(format: "TRUEPREDICATE")
_ = searchFiles(path: path, recursive: false, query: query, foundItemHandler: nil, completionHandler: completionHandler)
}
/**
Returns a `FileObject` containing the attributes of the item (file, directory, symlink, etc.) at the path in question via asynchronous completion handler.
If the directory contains no entries or an error is occured, this method will return the empty `FileObject`.
- Parameters:
- path: path to target directory. If empty, attributes of root will be returned.
- completionHandler: a closure with result of directory entries or error.
- 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) {
let url = URL(string: "files/get_metadata", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue(authentication: credential, with: .oAuth2)
request.setValue(contentType: .json)
let requestDictionary: [String: Any] = ["path": correctPath(path)!]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderHTTPError?
var fileObject: DropboxFileObject?
if let response = response as? HTTPURLResponse, response.statusCode >= 400 {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
}
if let json = data?.deserializeJSON(), let file = DropboxFileObject(json: json) {
fileObject = file
}
completionHandler(fileObject, serverError ?? error)
})
task.resume()
}
/// Returns volume/provider information asynchronously.
/// - Parameter volumeInfo: Information of filesystem/Provider returned by system/server.
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.setValue(authentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
guard let json = data?.deserializeJSON() else {
completionHandler(nil)
return
}
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()
}
/**
Search files inside directory using query asynchronously.
Sample predicates:
```
NSPredicate(format: "(name CONTAINS[c] 'hello') && (fileSize >= 10000)")
NSPredicate(format: "(modifiedDate >= %@)", Date())
NSPredicate(format: "(path BEGINSWITH %@)", "folder/child folder")
```
- Note: Don't pass Spotlight predicates to this method directly, use `FileProvider.convertSpotlightPredicateTo()` method to get usable predicate.
- Important: A file name criteria should be provided for Dropbox.
- Parameters:
- path: location of directory to start search
- recursive: Searching subdirectories of path
- query: An `NSPredicate` object with keys like `FileObject` members, except `size` which becomes `filesize`.
- foundItemHandler: Closure which is called when a file is found
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
- files: all files meat the `query` criteria.
- error: `Error` returned by server if occured.
- Returns: An `Progress` to get progress or cancel progress. Use `completedUnitCount` to iterate count of found items.
*/
@discardableResult
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
let queryStr: String?
if query.predicateFormat == "TRUEPREDICATE" {
queryStr = nil
} else {
queryStr = query.findValue(forKey: "name", operator: .beginsWith) as? String
}
let requestHandler = self.listRequest(path: path, queryStr: queryStr, recursive: recursive)
let queryIsTruePredicate = query.predicateFormat == "TRUEPREDICATE"
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? [Any] else {
let err = URLError(.badServerResponse, url: self?.url(of: path))
return ([], err, nil)
}
var files = [FileObject]()
for entry in entries {
if let entry = entry as? [String: Any], let file = DropboxFileObject(json: entry), queryIsTruePredicate || query.evaluate(with: file.mapPredicate()) {
files.append(file)
progress.completedUnitCount += 1
foundItemHandler?(file)
}
}
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 {
func uploadRequest(to path: String) -> URLRequest {
var requestDictionary = [String: Any]()
let url: URL = URL(string: "files/upload", relativeTo: contentURL)!
requestDictionary["path"] = correctPath(path)
requestDictionary["mode"] = (overwrite ? "overwrite" : "add")
//requestDictionary["client_modified"] = (attributes[.contentModificationDateKey] as? Date)?.format(with: .rfc3339)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue(authentication: credential, with: .oAuth2)
request.setValue(contentType: .stream)
request.setValue(dropboxArgKey: requestDictionary)
return request
}
func downloadRequest(from path: String) -> URLRequest {
let url = URL(string: "files/download", relativeTo: contentURL)!
var request = URLRequest(url: url)
request = URLRequest(url: url)
request.setValue(authentication: credential, with: .oAuth2)
request.setValue(dropboxArgKey: ["path": correctPath(path)!])
return request
}
// content operations
switch operation {
case .copy(source: let source, destination: let dest) where dest.lowercased().hasPrefix("file://"):
return downloadRequest(from: source)
case .fetch(let path):
return downloadRequest(from: path)
case .copy(source: let source, destination: let dest) where source.lowercased().hasPrefix("file://"):
return uploadRequest(to: dest)
case .modify(let path):
return uploadRequest(to: path)
default:
return self.apiRequest(for: operation, overwrite: overwrite)
}
}
func apiRequest(for operation: FileOperationType, overwrite: Bool = false) -> URLRequest {
let url: String
let sourcePath = operation.source
let destPath = operation.destination
var requestDictionary = [String: Any]()
switch operation {
case .create:
url = "files/create_folder_v2"
case .copy:
url = "files/copy_v2"
requestDictionary["allow_shared_folder"] = NSNumber(value: true)
case .move:
url = "files/move_v2"
requestDictionary["allow_shared_folder"] = NSNumber(value: true)
case .remove:
url = "files/delete_v2"
default: // modify, link, fetch
fatalError("Unimplemented operation \(operation.description) in \(#file)")
}
var request = URLRequest(url: URL(string: url, relativeTo: apiURL)!)
request.httpMethod = "POST"
request.setValue(authentication: credential, with: .oAuth2)
request.setValue(contentType: .json)
if let dest = correctPath(destPath) {
requestDictionary["from_path"] = correctPath(sourcePath)
requestDictionary["to_path"] = dest
} else {
requestDictionary["path"] = correctPath(sourcePath)
}
request.httpBody = Data(jsonDictionary: requestDictionary)
return request
}
override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
let errorDesc: String?
if let response = data?.deserializeJSON() {
errorDesc = (response["user_message"] as? String) ?? ((response["error"] as? [String: Any])?["tag"] as? String)
} else {
errorDesc = data.flatMap({ String(data: $0, encoding: .utf8) })
}
return FileProviderDropboxError(code: code, path: path ?? "", serverDescription: errorDesc)
}
override var maxUploadSimpleSupported: Int64 {
return 157_286_400 // 150MB
}
/*
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
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ 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(authentication: credential, with: .oAuth2)
request.setValue(contentType: .json)
let requestDictionary: [String: Any] = ["path": correctPath(path)!]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderHTTPError?
var link: URL?
var fileObject: DropboxFileObject?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
if let json = data?.deserializeJSON() {
link = (json["link"] as? String).flatMap(URL.init(string:))
fileObject = (json["metadata"] as? [String: Any]).flatMap(DropboxFileObject.init(json:))
}
}
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 closure with result of directory entries or error.
- `jobId`: Job ID returned by Dropbox to monitor the copy/download progress.
- `attribute`: A `FileObject` containing the attributes of the item.
- `error`: Error returned by Dropbox.
*/
open func copyItem(remoteURL: URL, to toPath: String, completionHandler: @escaping ((_ jobId: String?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
if remoteURL.isFileURL {
completionHandler(nil, nil, URLError(.badURL, url: remoteURL))
return
}
let url = URL(string: "files/save_url", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue(authentication: credential, with: .oAuth2)
request.setValue(contentType: .json)
let requestDictionary: [String: Any] = ["path": correctPath(toPath)!, "url" : remoteURL.absoluteString]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderHTTPError?
var jobId: String?
var fileObject: DropboxFileObject?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code.flatMap { self.serverError(with: $0, path: toPath, data: data) }
if let json = data?.deserializeJSON() {
jobId = json["async_job_id"] as? String
fileObject = (json["metadata"] as? [String: Any]).flatMap(DropboxFileObject.init(json:))
}
}
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(authentication: credential, with: .oAuth2)
request.setValue(contentType: .json)
let requestDictionary: [String: Any] = ["path": correctPath(toPath)!, "copy_reference" : reference ]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderHTTPError?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code.flatMap { self.serverError(with: $0, path: toPath, data: data) }
}
completionHandler?(serverError ?? error)
})
task.resume()
}
}
extension DropboxFileProvider: ExtendedFileProvider {
open func propertiesOfFileSupported(path: String) -> Bool {
let fileExt = path.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
}
}
@discardableResult
open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) -> Progress? {
let url = URL(string: "files/get_metadata", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue(authentication: credential, with: .oAuth2)
request.setValue(contentType: .json)
let requestDictionary: [String: Any] = ["path": correctPath(path)!, "include_media_info": NSNumber(value: true)]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderHTTPError?
var dic = [String: Any]()
var keys = [String]()
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
if let json = data?.deserializeJSON(), let properties = (json["media_info"] as? [String: Any])?["metadata"] as? [String: Any] {
(dic, keys) = self.mapMediaInfo(properties)
}
}
completionHandler(dic, keys, serverError ?? error)
})
task.resume()
return nil
}
#if os(macOS) || os(iOS) || os(tvOS)
open func thumbnailOfFileSupported(path: String) -> Bool {
switch path.pathExtension.lowercased() {
case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff":
return true
case "doc", "docx", "docm", "xls", "xlsx", "xlsm":
return false
case "ppt", "pps", "ppsx", "ppsm", "pptx", "pptm":
return false
case "rtf":
return false
default:
return false
}
}
/// Default value for dimension is 64x64, according to Dropbox documentation
@discardableResult
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) -> Progress? {
let url: URL
let thumbAPI: Bool
switch path.pathExtension.lowercased() {
case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff":
url = URL(string: "files/get_thumbnail", relativeTo: contentURL)!
thumbAPI = true
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)!
thumbAPI = false
default:
return nil
}
var request = URLRequest(url: url)
request.setValue(authentication: credential, with: .oAuth2)
var requestDictionary: [String: Any] = ["path": path]
if thumbAPI {
requestDictionary["format"] = "jpeg"
let size: String
switch dimension?.height ?? 64 {
case 0...32: size = "w32h32"
case 33...64: size = "w64h64"
case 65...128: size = "w128h128"
case 129...480: size = "w640h480"
default: size = "w1024h768"
}
requestDictionary["size"] = size
}
request.setValue(dropboxArgKey: requestDictionary)
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 = result.deserializeJSON() {
if jsonResult["error"] != nil {
completionHandler(nil, URLError(.cannotDecodeRawData, url: self.url(of: path)))
}
}
if let data = data {
if data.isPDF, let pageImage = DropboxFileProvider.convertToImage(pdfData: data, maxSize: dimension) {
image = pageImage
} 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 {
if let dimension = dimension {
image = DropboxFileProvider.scaleDown(data: data, toSize: dimension)
} else {
}
}
}
completionHandler(image, error)
})
task.resume()
return nil
}
#endif
}