Files
FileProvider/Sources/OneDriveFileProvider.swift

516 lines
23 KiB
Swift

//
// 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: You can pass file id instead of file path, e.g `"id:1234abcd"`, to point to a file or folder by ID.
- Note: Uploading files and data are limited to 100MB, for now.
*/
open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
override open class var type: String { return "OneDrive" }
/// Route to access file container on OneDrive. For default logined user use `.me` otherwise you can acesss
/// container based on drive id, group id, site id or user id for another user's default container
public enum Route: RawRepresentable {
/// Access to default container for current user
case me
/// Access to a specific drive by id
case drive(uuid: UUID)
/// Access to a default drive of a group by their id
case group(uuid: UUID)
/// Access to a default drive of a site by their id
case site(uuid: UUID)
/// Access to a default drive of a user by their id
case user(uuid: UUID)
public init?(rawValue: String) {
let components = rawValue.components(separatedBy: ";")
guard let type = components.first else {
return nil
}
if type == "me" {
self = .me
}
guard let uuid = components.last.flatMap({ UUID(uuidString: $0) }) else {
return nil
}
switch type {
case "drive":
self = .drive(uuid: uuid)
case "group":
self = .group(uuid: uuid)
case "site":
self = .site(uuid: uuid)
case "user":
self = .user(uuid: uuid)
default:
return nil
}
}
public var rawValue: String {
switch self {
case .me:
return "me;"
case .drive(uuid: let uuid):
return "drive;" + uuid.uuidString
case .group(uuid: let uuid):
return "group;" + uuid.uuidString
case .site(uuid: let uuid):
return "site;" + uuid.uuidString
case .user(uuid: let uuid):
return "user;" + uuid.uuidString
}
}
/// Return path component in URL for selected drive
var drivePath: String {
switch self {
case .me:
return "me/drive"
case .drive(uuid: let uuid):
return "drives/" + uuid.uuidString
case .group(uuid: let uuid):
return "groups/" + uuid.uuidString + "/drive"
case .site(uuid: let uuid):
return "sites/" + uuid.uuidString + "/drive"
case .user(uuid: let uuid):
return "users/" + uuid.uuidString + "/drive"
}
}
}
/// Route for container, default is `.me`.
open let route: Route
/**
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.
- 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 user.
- drive: drive name for user on server, default value is `root`.
- cache: A URLCache to cache downloaded files and contents.
*/
@available(*, deprecated, message: "use init(credential:, serverURL:, route:, cache:) instead.")
public init(credential: URLCredential?, serverURL: URL? = nil, drive: String?, cache: URLCache? = nil) {
let baseURL = serverURL?.absoluteURL ?? URL(string: "https://api.onedrive.com/")!
let refinedBaseURL = baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
self.route = drive.flatMap({ UUID(uuidString: $0) }).flatMap({ Route.drive(uuid: $0) }) ?? .me
super.init(baseURL: refinedBaseURL, credential: credential, cache: cache)
}
/**
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.
- route: drive name for user on server, default value is `.me`.
- cache: A URLCache to cache downloaded files and contents.
*/
public init(credential: URLCredential?, serverURL: URL? = nil, route: Route = .me, cache: URLCache? = nil) {
let baseURL = serverURL?.absoluteURL ?? URL(string: "https://api.onedrive.com/")!
let refinedBaseURL = baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
self.route = route
super.init(baseURL: refinedBaseURL, credential: credential, cache: cache)
}
public required convenience init?(coder aDecoder: NSCoder) {
let route: Route
if let driveId = aDecoder.decodeObject(forKey: "drive") as? String, let uuid = UUID(uuidString: driveId) {
route = .drive(uuid: uuid)
} else {
route = (aDecoder.decodeObject(forKey: "route") as? String).flatMap({ Route(rawValue: $0) }) ?? .me
}
self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential,
serverURL: aDecoder.decodeObject(forKey: "baseURL") as? URL,
route: route)
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.route.rawValue, forKey: "route")
}
open override func copy(with zone: NSZone? = nil) -> Any {
let copy = OneDriveFileProvider(credential: self.credential, serverURL: self.baseURL, route: self.route, cache: self.cache)
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) {
_ = paginated(path, requestHandler: { [weak self] (token) -> URLRequest? in
guard let `self` = self else { return nil }
let url = token.flatMap(URL.init(string:)) ?? self.url(of: path, modifier: "children")
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue(authentication: self.credential, with: .oAuth2)
return request
}, pageHandler: { [weak self] (data, _) -> (files: [FileObject], error: Error?, newToken: String?) in
guard let `self` = self else { return ([], nil, nil) }
guard let json = data?.deserializeJSON(), let entries = json["value"] as? [AnyObject] else {
let err = self.urlError(path, code: .badServerResponse)
return ([], err, nil)
}
var files = [FileObject]()
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: entry) {
files.append(file)
}
}
return (files, nil, json["@odata.nextLink"] as? String)
}, completionHandler: completionHandler)
}
open override func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
var request = URLRequest(url: url(of: path))
request.httpMethod = "GET"
request.setValue(authentication: self.credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderHTTPError?
var fileObject: OneDriveFileObject?
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 file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: json) {
fileObject = file
}
}
completionHandler(fileObject, serverError ?? error)
})
task.resume()
}
open override func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) {
var request = URLRequest(url: url(of: ""))
request.httpMethod = "GET"
request.setValue(authentication: self.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.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? {
let queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first
return paginated(path, requestHandler: { [weak self] (token) -> URLRequest? in
guard let `self` = self else { return nil }
let url: URL
if let next = token.flatMap(URL.init(string:)) {
url = next
} else {
let bURL = self.baseURL!.appendingPathComponent(self.route.drivePath).appendingPathComponent("root/search")
var components = URLComponents(url: bURL, resolvingAgainstBaseURL: false)!
let qItem = URLQueryItem(name: "q", value: (queryStr ?? "*"))
components.queryItems = [qItem]
if recursive {
components.queryItems?.append(URLQueryItem(name: "expand", value: "children"))
}
url = components.url!
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
return request
}, pageHandler: { [weak self] (data, progress) -> (files: [FileObject], error: Error?, newToken: String?) in
guard let `self` = self else { return ([], nil, nil) }
guard let json = data?.deserializeJSON(), let entries = json["value"] as? [AnyObject] else {
let err = self.urlError(path, code: .badServerResponse)
return ([], err, nil)
}
var foundFiles = [FileObject]()
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: entry), query.evaluate(with: file.mapPredicate()) {
foundFiles.append(file)
foundItemHandler?(file)
}
}
return (foundFiles, nil, json["@odata.nextLink"] as? String)
}, completionHandler: completionHandler)
}
open func url(of path: String, modifier: String? = nil) -> URL {
var url: URL = baseURL!
var rpath: String = path
let isId = path.hasPrefix("id:")
url.appendPathComponent(route.drivePath)
if isId {
url.appendPathComponent("root:")
} else {
url.appendPathComponent("items")
}
rpath = rpath.trimmingCharacters(in: pathTrimSet)
switch (modifier == nil, rpath.isEmpty, isId) {
case (true, false, _):
url.appendPathComponent(rpath)
case (true, true, _):
break
case (false, true, _):
url.appendPathComponent(modifier!)
case (false, false, true):
url.appendPathComponent(rpath)
url.appendPathComponent(modifier!)
case (false, false, false):
url.appendPathComponent(rpath + ":")
url.appendPathComponent(modifier!)
}
return url
}
open override func isReachable(completionHandler: @escaping (Bool) -> Void) {
var request = URLRequest(url: url(of: ""))
request.httpMethod = "HEAD"
request.setValue(authentication: 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 {
func correctPath(_ path: String) -> String {
if path.hasPrefix("id:") {
return path
}
var p = path.hasPrefix("/") ? path : "/" + path
if p.hasSuffix("/") {
p.remove(at: p.index(before:p.endIndex))
}
return p
}
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.setValue(authentication: self.credential, with: .oAuth2)
// Remove gzip to fix availability of progress per (Oleg Marchik)[https://github.com/evilutioner] PR (#61)
request.setValue(acceptEncoding: .deflate)
request.addValue(acceptEncoding: .identity)
switch operation {
case .copy(let source, let dest) where !source.hasPrefix("file://") && !dest.hasPrefix("file://"),
.move(source: let source, destination: let dest):
request.setValue(contentType: .json)
let cdest = correctPath(dest) as NSString
var parentRefrence: [String: AnyObject] = [:]
if cdest.hasPrefix("id:") {
parentRefrence["id"] = cdest.components(separatedBy: "/").first as NSString?
switch self.route {
case .drive(uuid: let uuid):
parentRefrence["driveId"] = uuid.uuidString as NSString
default:
break
}
} else {
parentRefrence["path"] = cdest.deletingLastPathComponent as NSString
}
var requestDictionary = [String: AnyObject]()
requestDictionary["parentReference"] = parentRefrence as NSDictionary
requestDictionary["name"] = (cdest as NSString).lastPathComponent as NSString
request.httpBody = Data(jsonDictionary: requestDictionary)
default:
break
}
return request
}
override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
let errorDesc: String?
if let response = data?.deserializeJSON() {
errorDesc = response["error"]?["message"] as? String
} else {
errorDesc = data.flatMap({ String(data: $0, encoding: .utf8) })
}
return FileProviderOneDriveError(code: code, path: path ?? "", errorDescription: errorDesc)
}
override var maxUploadSimpleSupported: Int64 {
return 104_857_600 // 100MB
}
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()
}
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: FileProviderHTTPError?
var link: URL?
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() {
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.setValue(authentication: 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 = self.serverError(with: rCode, path: path, data: data)
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.setValue(authentication: credential, with: .oAuth2)
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() {
(dic, keys) = self.mapMediaInfo(json)
}
}
completionHandler(dic, keys, serverError ?? error)
})
task.resume()
}
}