// // OneDriveFileProvider.swift // FileProvider // // Created by Amir Abbas Mousavian. // Copyright © 2017 Mousavian. Distributed under MIT license. // import Foundation import CoreGraphics /** Allows accessing to OneDrive stored files, either hosted on Microsoft servers or business coprporate one. This provider doesn't cache or save files internally, however you can set `useCache` and `cache` properties to use Foundation `NSURLCache` system. - Note: Uploading files and data are limited to 100MB, for now. */ open class OneDriveFileProvider: HTTPFileProvider { override open class var type: String { return "OneDrive" } /// Drive name for user, default is `root`. Changing its value will effect on new operations. open var drive: String /** Initializer for Onedrive provider with given client ID and Token. These parameters must be retrieved via [Authentication for the OneDrive API](https://dev.onedrive.com/auth/readme.htm). 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. - Parameters: - credential: a `URLCredential` object with Client ID set as `user` and Token set as `password`. - serverURL: server url, Set it if you are trying to connect OneDrive Business server, otherwise leave it `nil` to connect to OneDrive Personal uses. - drive: drive name for user on server, default value is `root`. - cache: A URLCache to cache downloaded files and contents. */ public init(credential: URLCredential?, serverURL: URL? = nil, drive: String = "root", cache: URLCache? = nil) { let baseURL = serverURL?.absoluteURL ?? URL(string: "https://api.onedrive.com/")! let refinedBaseURL = baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("") self.drive = drive super.init(baseURL: refinedBaseURL, credential: credential, cache: cache) } public required convenience init?(coder aDecoder: NSCoder) { self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential, serverURL: aDecoder.decodeObject(forKey: "baseURL") as? URL, drive: aDecoder.decodeObject(forKey: "drive") as? String ?? "root") self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? "" self.useCache = aDecoder.decodeBool(forKey: "useCache") self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache") } open override func encode(with aCoder: NSCoder) { super.encode(with: aCoder) aCoder.encode(self.drive, forKey: "drive") } open override func copy(with zone: NSZone? = nil) -> Any { let copy = OneDriveFileProvider(credential: self.credential, serverURL: self.baseURL, drive: self.drive, 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 } 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)) { var request = URLRequest(url: url(of: path)) request.httpMethod = "GET" request.set(httpAuthentication: credential, with: .oAuth2) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var serverError: FileProviderOneDriveError? var fileObject: OneDriveFileObject? if let response = response as? HTTPURLResponse { let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil if let json = data?.deserializeJSON(), let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: json) { fileObject = file } } completionHandler(fileObject, serverError ?? error) }) task.resume() } open override func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) { 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 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 } completionHandler(totalSize, usedSize) }) task.resume() } 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(parent: nil, userInfo: nil) progress.setUserInfoObject(url(of: path), forKey: .fileURLKey) search(path, query: finalQueryStr, progress: progress, foundItem: { (file) in if query.evaluate(with: file.mapPredicate()) { foundFiles.append(file) foundItemHandler?(file) } }, completionHandler: { (error) in completionHandler(foundFiles, error) }) return progress } open func url(of path: String? = nil, modifier: String? = nil) -> URL { var rpath: String if let path = path { rpath = path } else { rpath = self.currentPath } let driveURL = baseURL!.appendingPathComponent("drive/\(drive):/") if rpath.hasPrefix("/") { _=rpath.characters.removeFirst() } if rpath.isEmpty { if let modifier = modifier { return driveURL.appendingPathComponent(modifier) } return driveURL } rpath = rpath.trimmingCharacters(in: pathTrimSet) if let modifier = modifier { rpath = rpath + ":/" + modifier } return driveURL.appendingPathComponent(rpath) } open override func isReachable(completionHandler: @escaping (Bool) -> Void) { var request = URLRequest(url: url()) request.httpMethod = "HEAD" request.set(httpAuthentication: credential, with: .oAuth2) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in let status = (response as? HTTPURLResponse)?.statusCode ?? 400 completionHandler(status == 200) }) task.resume() } override func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey : Any] = [:]) -> URLRequest { let method: String let url: URL switch operation { case .fetch(path: let path): method = "GET" url = self.url(of: path, modifier: "content") case .modify(path: let path): method = "PUT" let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail" url = self.url(of: path, modifier: "content\(queryStr)") case .create(path: let path): method = "CREATE" url = self.url(of: path) case .copy(let source, let dest) where !source.hasPrefix("file://") && !dest.hasPrefix("file://"): method = "POST" url = self.url(of: source) case .copy(let source, let dest) where source.hasPrefix("file://"): method = "PUT" let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail" url = self.url(of: dest, modifier: "content\(queryStr)") case .copy(let source, let dest) where dest.hasPrefix("file://"): method = "GET" url = self.url(of: source, modifier: "content") case .move(source: let source, destination: _): method = "PATCH" url = self.url(of: source) case .remove(path: let path): method = "DELETE" url = self.url(of: path) default: // link fatalError("Unimplemented operation \(operation.description) in \(#file)") } var request = URLRequest(url: url) request.httpMethod = method request.set(httpAuthentication: credential, with: .oAuth2) if let dest = correctPath(operation.destination) as NSString?, !dest.hasPrefix("file://") { request.set(contentType: .json) var requestDictionary = [String: AnyObject]() requestDictionary["parentReference"] = ("/drive/\(drive):" + dest.deletingLastPathComponent) as NSString requestDictionary["name"] = dest.lastPathComponent as NSString request.httpBody = Data(jsonDictionary: requestDictionary) } return request } override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError { return FileProviderOneDriveError(code: code, path: path ?? "", errorDescription: data.flatMap({ String(data: $0, encoding: .utf8) })) } override func upload_simple(_ targetPath: String, request: URLRequest, data: Data?, localFile: URL?, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? { let size = data?.count ?? Int((try? localFile?.resourceValues(forKeys: [.fileSizeKey]))??.fileSize ?? -1) if size > 100 * 1024 * 1024 { let error = FileProviderOneDriveError(code: .payloadTooLarge, path: targetPath, errorDescription: nil) completionHandler?(error) self.delegateNotify(operation, error: error) return nil } return super.upload_simple(targetPath, request: request, data: data, localFile: localFile, operation: operation, completionHandler: completionHandler) } } extension OneDriveFileProvider { fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) { /* There is two ways to monitor folders changing in OneDrive. 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() } } extension OneDriveFileProvider: FileProviderSharing { open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) { var request = URLRequest(url: self.url(of: path, modifier: "action.createLink")) request.httpMethod = "POST" let requestDictionary: [String: AnyObject] = ["type": "view" as NSString] request.httpBody = Data(jsonDictionary: requestDictionary) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var serverError: FileProviderOneDriveError? var link: URL? if let response = response as? HTTPURLResponse { let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil if let json = data?.deserializeJSON() { if let linkDic = json["link"] as? NSDictionary, let linkStr = linkDic["webUrl"] as? String { link = URL(string: linkStr) } } } completionHandler(link, nil, nil, serverError ?? error) }) task.resume() } } extension OneDriveFileProvider: ExtendedFileProvider { open func thumbnailOfFileSupported(path: String) -> Bool { return true } open 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", "wma": return true case "mp4", "mpg", "3gp", "mov", "avi", "wmv": return true default: return false } } open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) { let url: URL if let dimension = dimension { url = self.url(of: path, modifier: "thumbnails/0/=c\(dimension.width)x\(dimension.height)/content") } else { url = self.url(of: path, modifier: "thumbnails/0/small/content") } var request = URLRequest(url: url) request.set(httpAuthentication: credential, with: .oAuth2) let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in var image: ImageClass? = nil if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { let responseError = FileProviderOneDriveError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) completionHandler(nil, responseError) return } if let data = data { image = ImageClass(data: data) } completionHandler(image, error) }) task.resume() } open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) { var request = URLRequest(url: url(of: path)) request.httpMethod = "GET" request.set(httpAuthentication: credential, with: .oAuth2) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var serverError: FileProviderOneDriveError? var dic = [String: Any]() var keys = [String]() if let response = response as? HTTPURLResponse { let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil if let json = data?.deserializeJSON() { (dic, keys) = self.mapMediaInfo(json) } } completionHandler(dic, keys, serverError ?? error) }) task.resume() } }