394 lines
18 KiB
Swift
394 lines
18 KiB
Swift
//
|
|
// OneDriveHelper.swift
|
|
// FileProvider
|
|
//
|
|
// Created by Amir Abbas Mousavian.
|
|
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// Error returned by OneDrive server when trying to access or do operations on a file or folder.
|
|
public struct FileProviderOneDriveError: FileProviderHTTPError {
|
|
public let code: FileProviderHTTPErrorCode
|
|
public let path: String
|
|
public let serverDescription: String?
|
|
}
|
|
|
|
/// Containts path, url and attributes of a OneDrive file or resource.
|
|
public final class OneDriveFileObject: FileObject {
|
|
internal convenience init? (baseURL: URL?, route: OneDriveFileProvider.Route, jsonStr: String) {
|
|
guard let json = jsonStr.deserializeJSON() else { return nil }
|
|
self.init(baseURL: baseURL, route: route, json: json)
|
|
}
|
|
|
|
internal init? (baseURL: URL?, route: OneDriveFileProvider.Route, json: [String: Any]) {
|
|
guard let name = json["name"] as? String else { return nil }
|
|
guard let id = json["id"] as? String else { return nil }
|
|
let path: String
|
|
if let refpath = (json["parentReference"] as? [String: Any])?["path"] as? String {
|
|
let parentPath: String
|
|
if let colonIndex = refpath.firstIndex(of: ":") {
|
|
parentPath = String(refpath[refpath.index(after: colonIndex)...])
|
|
} else {
|
|
parentPath = refpath
|
|
}
|
|
path = parentPath.appendingPathComponent(name)
|
|
} else {
|
|
path = "id:\(id)"
|
|
}
|
|
let url = baseURL.map { OneDriveFileObject.url(of: path, modifier: nil, baseURL: $0, route: route) }
|
|
super.init(url: url, name: name, path: path)
|
|
self.id = id
|
|
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
|
|
self.childrensCount = (json["folder"] as? [String: Any])?["childCount"] as? Int
|
|
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"] as? [String: Any])?["mimeType"] as? String).flatMap(ContentMIMEType.init(rawValue:)) ?? .stream
|
|
self.entryTag = json["eTag"] as? String
|
|
let hashes = (json["file"] as? [String: Any])?["hashes"] as? [String: Any]
|
|
// checks for both sha1 or quickXor. First is available in personal drives, second in business one.
|
|
self.fileHash = (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.
|
|
public internal(set) var id: String? {
|
|
get {
|
|
return allValues[.fileResourceIdentifierKey] as? String
|
|
}
|
|
set {
|
|
allValues[.fileResourceIdentifierKey] = newValue
|
|
}
|
|
}
|
|
|
|
/// MIME type of file contents returned by OneDrive server.
|
|
public internal(set) var contentType: ContentMIMEType {
|
|
get {
|
|
return (allValues[.mimeTypeKey] as? String).flatMap(ContentMIMEType.init(rawValue:)) ?? .stream
|
|
}
|
|
set {
|
|
allValues[.mimeTypeKey] = newValue.rawValue
|
|
}
|
|
}
|
|
|
|
/// HTTP E-Tag, can be used to mark changed files.
|
|
public internal(set) var entryTag: String? {
|
|
get {
|
|
return allValues[.entryTagKey] as? String
|
|
}
|
|
set {
|
|
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.
|
|
public internal(set) var fileHash: String? {
|
|
get {
|
|
return allValues[.documentIdentifierKey] as? String
|
|
}
|
|
set {
|
|
allValues[.documentIdentifierKey] = newValue
|
|
}
|
|
}
|
|
|
|
static func url(of path: String, modifier: String?, baseURL: URL, route: OneDriveFileProvider.Route) -> URL {
|
|
var url: URL = baseURL
|
|
let isId = path.hasPrefix("id:")
|
|
var rpath: String = path.replacingOccurrences(of: "id:", with: "", options: .anchored)
|
|
|
|
//url.appendPathComponent("v1.0")
|
|
url.appendPathComponent(route.drivePath)
|
|
|
|
if rpath.isEmpty {
|
|
url.appendPathComponent("root")
|
|
} else if isId {
|
|
url.appendPathComponent("items")
|
|
} else {
|
|
url.appendPathComponent("root:")
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
static func relativePathOf(url: URL, baseURL: URL?, route: OneDriveFileProvider.Route) -> String {
|
|
let base = baseURL?.appendingPathComponent(route.drivePath).path ?? ""
|
|
|
|
let crudePath = url.path.replacingOccurrences(of: base, with: "", options: .anchored)
|
|
.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
|
|
|
switch crudePath {
|
|
case hasPrefix("items/"):
|
|
let components = crudePath.components(separatedBy: "/")
|
|
return components.dropFirst().first.map { "id:\($0)" } ?? ""
|
|
case hasPrefix("root:"):
|
|
return crudePath.components(separatedBy: ":").dropFirst().first ?? ""
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
}
|
|
|
|
extension OneDriveFileProvider {
|
|
func upload_multipart_data(_ targetPath: String, data: Data, operation: FileOperationType,
|
|
overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
|
return self.upload_multipart(targetPath, operation: operation, size: Int64(data.count), overwrite: overwrite, dataProvider: {
|
|
let range = $0.clamped(to: 0..<Int64(data.count))
|
|
return data[range]
|
|
}, completionHandler: completionHandler)
|
|
}
|
|
|
|
func upload_multipart_file(_ targetPath: String, file: URL, operation: FileOperationType,
|
|
overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
|
// upload task can't handle uploading file
|
|
|
|
return self.upload_multipart(targetPath, operation: operation, size: file.fileSize, overwrite: overwrite, dataProvider: { range in
|
|
guard let handle = FileHandle(forReadingAtPath: file.path) else {
|
|
throw CocoaError(.fileNoSuchFile, path: targetPath)
|
|
}
|
|
|
|
defer {
|
|
handle.closeFile()
|
|
}
|
|
|
|
let offset = range.lowerBound
|
|
handle.seek(toFileOffset: UInt64(offset))
|
|
guard Int64(handle.offsetInFile) == offset else {
|
|
throw CocoaError(.fileReadTooLarge, path: targetPath)
|
|
}
|
|
|
|
return handle.readData(ofLength: range.count)
|
|
}, completionHandler: completionHandler)
|
|
}
|
|
|
|
private func upload_multipart(_ targetPath: String, operation: FileOperationType, size: Int64, overwrite: Bool,
|
|
dataProvider: @escaping (Range<Int64>) throws -> Data, completionHandler: SimpleCompletionHandler) -> Progress? {
|
|
guard size > 0 else { return nil }
|
|
|
|
let progress = Progress(totalUnitCount: size)
|
|
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
|
progress.kind = .file
|
|
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
|
|
|
let createURL = self.url(of: targetPath, modifier: "createUploadSession")
|
|
var createRequest = URLRequest(url: createURL)
|
|
createRequest.httpMethod = "POST"
|
|
createRequest.setValue(authentication: self.credential, with: .oAuth2)
|
|
createRequest.setValue(contentType: .json)
|
|
if overwrite {
|
|
createRequest.httpBody = Data(jsonDictionary: ["item": ["@microsoft.graph.conflictBehavior": "replace"] as NSDictionary])
|
|
} else {
|
|
createRequest.httpBody = Data(jsonDictionary: ["item": ["@microsoft.graph.conflictBehavior": "fail"] as NSDictionary])
|
|
}
|
|
let createSessionTask = session.dataTask(with: createRequest) { (data, response, error) in
|
|
if let error = error {
|
|
completionHandler?(error)
|
|
return
|
|
}
|
|
|
|
if let data = data, let json = data.deserializeJSON(),
|
|
let uploadURL = (json["uploadUrl"] as? String).flatMap(URL.init(string:)) {
|
|
self.upload_multipart(url: uploadURL, operation: operation, size: size, progress: progress, dataProvider: dataProvider, completionHandler: completionHandler)
|
|
}
|
|
}
|
|
createSessionTask.resume()
|
|
|
|
return progress
|
|
}
|
|
|
|
private func upload_multipart(url: URL, operation: FileOperationType, size: Int64, range: Range<Int64>? = nil, uploadedSoFar: Int64 = 0,
|
|
progress: Progress, dataProvider: @escaping (Range<Int64>) throws -> Data, completionHandler: SimpleCompletionHandler) {
|
|
guard !progress.isCancelled else { return }
|
|
|
|
let maximumSize: Int64 = 10_485_760 // Recommended by OneDrive documentations and divides evenly by 320 KiB, max 60MiB.
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "PUT"
|
|
request.setValue(authentication: self.credential, with: .oAuth2)
|
|
|
|
let finalRange: Range<Int64>
|
|
if let range = range {
|
|
if range.count > maximumSize {
|
|
finalRange = range.lowerBound..<(range.upperBound + maximumSize)
|
|
} else {
|
|
finalRange = range
|
|
}
|
|
} else {
|
|
finalRange = 0..<min(maximumSize, size)
|
|
}
|
|
request.setValue(contentRange: finalRange, totalBytes: size)
|
|
|
|
let data: Data
|
|
do {
|
|
data = try dataProvider(finalRange)
|
|
} catch {
|
|
dispatch_queue.async {
|
|
completionHandler?(error)
|
|
}
|
|
self.delegateNotify(operation, error: error)
|
|
return
|
|
}
|
|
let task = session.uploadTask(with: request, from: data)
|
|
|
|
var dictionary: [String: Any] = ["type": operation.description]
|
|
dictionary["source"] = operation.source
|
|
dictionary["dest"] = operation.destination
|
|
dictionary["uploadedBytes"] = NSNumber(value: uploadedSoFar)
|
|
dictionary["totalBytes"] = NSNumber(value: data.count)
|
|
task.taskDescription = String(jsonDictionary: dictionary)
|
|
sessionDelegate?.observerProgress(of: task, using: progress, kind: .upload)
|
|
progress.cancellationHandler = { [weak task, weak self] in
|
|
task?.cancel()
|
|
var deleteRequest = URLRequest(url: url)
|
|
deleteRequest.httpMethod = "DELETE"
|
|
self?.session.dataTask(with: deleteRequest).resume()
|
|
}
|
|
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
|
|
|
var allData = Data()
|
|
dataCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { data in
|
|
allData.append(data)
|
|
}
|
|
// We retain self here intentionally to allow resuming upload, This behavior may change anytime!
|
|
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak task] error in
|
|
if let error = error {
|
|
progress.cancel()
|
|
completionHandler?(error)
|
|
self.delegateNotify(operation, error: error)
|
|
return
|
|
}
|
|
|
|
guard let json = allData.deserializeJSON() else {
|
|
let error = URLError(.badServerResponse, userInfo: [NSURLErrorKey: url, NSURLErrorFailingURLErrorKey: url, NSURLErrorFailingURLStringErrorKey: url.absoluteString])
|
|
completionHandler?(error)
|
|
self.delegateNotify(operation, error: error)
|
|
return
|
|
}
|
|
|
|
if let _ = json["error"] {
|
|
let code = ((task?.response as? HTTPURLResponse)?.statusCode).flatMap(FileProviderHTTPErrorCode.init(rawValue:)) ?? .badRequest
|
|
let error = self.serverError(with: code, path: self.relativePathOf(url: url), data: allData)
|
|
completionHandler?(error)
|
|
self.delegateNotify(operation, error: error)
|
|
return
|
|
}
|
|
|
|
if let ranges = json["nextExpectedRanges"] as? [String], let firstRange = ranges.first {
|
|
let uploaded = uploadedSoFar + Int64(finalRange.count)
|
|
let comp = firstRange.components(separatedBy: "-")
|
|
let lower = comp.first.flatMap(Int64.init) ?? uploaded
|
|
let upper = comp.dropFirst().first.flatMap(Int64.init) ?? Int64.max
|
|
let range = Range<Int64>(uncheckedBounds: (lower: lower, upper: upper))
|
|
self.upload_multipart(url: url, operation: operation, size: size, range: range, uploadedSoFar: uploaded, progress: progress,
|
|
dataProvider: dataProvider, completionHandler: completionHandler)
|
|
return
|
|
}
|
|
|
|
if let _ = json["id"] as? String {
|
|
completionHandler?(nil)
|
|
self.delegateNotify(operation)
|
|
}
|
|
}
|
|
|
|
task.resume()
|
|
}
|
|
|
|
static let dateFormatter = DateFormatter()
|
|
static let decimalFormatter = NumberFormatter()
|
|
|
|
func mapMediaInfo(_ json: [String: Any]) -> (dictionary: [String: Any], keys: [String]) {
|
|
|
|
func spaceCamelCase(_ text: String) -> String {
|
|
var newString: String = ""
|
|
|
|
let upperCase = CharacterSet.uppercaseLetters
|
|
for scalar in text.unicodeScalars {
|
|
if upperCase.contains(scalar) {
|
|
newString.append(" ")
|
|
}
|
|
let character = Character(scalar)
|
|
newString.append(character)
|
|
}
|
|
|
|
return newString.capitalized
|
|
}
|
|
|
|
var dic = [String: Any]()
|
|
var keys = [String]()
|
|
|
|
func add(key: String, value: Any?) {
|
|
if let value = value, !((value as? String)?.isEmpty ?? false) {
|
|
keys.append(key)
|
|
dic[key] = value
|
|
}
|
|
}
|
|
|
|
if let parent = json["image"] as? [String: Any] ?? json["video"] as? [String: Any], let height = parent["height"] as? UInt64, let width = parent["width"] as? UInt64 {
|
|
add(key: "Dimensions", value: "\(width)x\(height)")
|
|
}
|
|
if let location = json["location"] as? [String: Any], let latitude = location["latitude"] as? Double, let longitude = location["longitude"] as? Double {
|
|
OneDriveFileProvider.decimalFormatter.numberStyle = .decimal
|
|
OneDriveFileProvider.decimalFormatter.maximumFractionDigits = 5
|
|
let latStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: latitude))!
|
|
let longStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))!
|
|
add(key: "Location", value: "\(latStr), \(longStr)")
|
|
}
|
|
if let parent = json["image"] as? [String: Any] ?? json["video"] as? [String: Any], let duration = parent["duration"] as? UInt64 {
|
|
add(key: "Duration", value: (TimeInterval(duration) / 1000).formatshort)
|
|
}
|
|
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = Date(rfcString: timeTakenStr) {
|
|
OneDriveFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
|
add(key: "Date taken", value: OneDriveFileProvider.dateFormatter.string(from: timeTaken))
|
|
}
|
|
|
|
if let photo = json["photo"] as? [String: Any] {
|
|
add(key: "Device make", value: photo["cameraMake"] as? String)
|
|
add(key: "Device model", value: photo["cameraModel"] as? String)
|
|
add(key: "focalLength", value: photo["focalLength"] as? Double)
|
|
add(key: "fNumber", value: photo["fNumber"] as? Double)
|
|
if let expNom = photo["exposureNumerator"] as? Double, let expDen = photo["exposureDenominator"] as? Double {
|
|
add(key: "Exposure time", value: "\(Int(expNom))/\(Int(expDen))")
|
|
}
|
|
add(key: "ISO speed", value: photo["iso"] as? Int64)
|
|
}
|
|
|
|
if let audio = json["audio"] as? [String: Any] {
|
|
for (key, value) in audio {
|
|
var value = value
|
|
if key == "bitrate" || key == "isVariableBitrate" { continue }
|
|
let casedKey = spaceCamelCase(key)
|
|
switch casedKey {
|
|
case "Duration":
|
|
value = (value as? Int64).map { (TimeInterval($0) / 1000).formatshort } as Any
|
|
case "Bitrate":
|
|
value = (value as? Int64).map { "\($0)kbps" } as Any
|
|
default:
|
|
break
|
|
}
|
|
add(key: casedKey, value: value)
|
|
}
|
|
}
|
|
|
|
add(key: "Bitrate", value: (json["video"] as? NSDictionary)?["bitrate"] as? Int)
|
|
|
|
return (dic, keys)
|
|
}
|
|
}
|