Fixed OneDrive provider

- Added authentication document
- Fixed date parsing
This commit is contained in:
Amir Abbas Mousavian
2018-02-26 23:47:41 +03:30
parent fd89a04c8e
commit 09cb34352f
7 changed files with 240 additions and 76 deletions
+83
View File
@@ -0,0 +1,83 @@
# Authentication
Dropbox and OneDrive are using OAuth2 authentication method. Here is sample codes to get Bearer token using [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift)
## Handling url scheme in App delegate
Add these lines to your application delegate:
```swift
extension AppDelegate {
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any]) -> Bool {
if url.host == "oauth-callback" {
OAuthSwift.handle(url: url)
}
// HANDLING OTHER PATERNS
}
}
```
## Dropbox
Your client id and secret must be given by Dropbox developer portal. Bearer tokens created by this method are permanent.
```swift
let appScheme = "YOUR_APP_SCHEME"
oauth = OAuth2Swift(consumerKey: "CLIENT_ID",
consumerSecret: "CLIENT_SECRET",
authorizeUrl: "https://www.dropbox.com/oauth2/authorize",
responseType: "token")!
oauth.authorizeURLHandler = SafariURLHandler(viewController: self, oauthSwift: oauth)
_ = oauth.authorize(withCallbackURL: URL(string: "\(appScheme)://oauth-callback/dropbox")!,
scope: "", state:"DROPBOX",
success: { credential, response, parameters in
let urlcredential = URLCredential(user: user ?? "anonymous", password: credential.oauthToken, persistence: .permanent)
// TODO: Save credential in keychain
// TODO: Create Dropbox provider using urlcredential
}, failure: { error in
print(error.localizedDescription)
}
)
```
## OneDrive
Your client id must be given by Microsoft developer portal. OneDrive doesn't need client secret for native apps, but will need to refresh token every one hour.
We must save refresh token in adition to bearer token and use it when we get `.unauthorized` 401 HTTP error in completion handlers to get a new bearer token.
```swift
let appScheme = "YOUR_APP_SCHEME"
oauth = OAuth2Swift.init(consumerKey: "CLIENT_ID",
consumerSecret: "",
authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
accessTokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
responseType: "code")!
oauth.authorizeURLHandler = SafariURLHandler(viewController: self, oauthSwift: oauth)
if let refreshToken = {SAVED_REFRESH_TOKEN} {
oauth.renewAccessToken(withRefreshToken: token,
success: { credential, response, parameters in
let urlcredential = URLCredential(user: user ?? "anonymous", password: credential.oauthToken, persistence: .permanent)
let refreshToken = credential.oauthRefreshToken
// TODO: Save refreshToken in keychain
// TODO: Save credential in keychain
// TODO: Create OneDrive provider using urlcredential
}, failure: { error in
print(error.localizedDescription)
// TODO: Clear saved refresh token and call this method again to get authorization token
})
} else {
_ = oauth.authorize(
withCallbackURL: URL(string: "\(appScheme)://oauth-callback/onedrive")!,
scope: "offline_access User.Read Files.ReadWrite Files.ReadWrite.All Files.ReadWrite.AppFolder", state: "ONEDRIVE",
success: { credential, response, parameters in
let credential = URLCredential(user: user ?? "anonymous", password: credential.oauthToken, persistence: .permanent)
// TODO: Save refreshToken in keychain
// TODO: Save credential in keychain
// TODO: Create OneDrive provider using credential
}, failure: { error in
print(error.localizedDescription)
})
}
```
+2 -2
View File
@@ -156,9 +156,9 @@ let webdavProvider = WebDAVFileProvider(baseURL: URL(string: "http://www.example
* In case you want to connect non-secure servers for WebDAV (http) or FTP in iOS 9+ / macOS 10.11+ you should disable App Transport Security (ATS) according to [this guide.](https://gist.github.com/mlynch/284699d676fe9ed0abfa)
* For Dropbox & OneDrive, user is clientID and password is Token which both 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.
* For Dropbox & OneDrive, user is clientID and password is Token which both 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. Please see [Authentication.md](Authentication.md) for detailed instruction.
For interaction with UI, set delegate variable of `FileProvider` object
For interaction with UI, set delegate property of `FileProvider` object.
You can use `url(of:)` method if provider to get direct access url (local or remote files) for some file systems which allows to do so (Dropbox doesn't support and returns path simply wrapped in URL).
+32 -13
View File
@@ -478,32 +478,50 @@ internal extension TimeInterval {
public extension Date {
/// Date formats used commonly in internet messaging defined by various RFCs.
public enum RFCStandards: String {
/// Date format defined by usenet, commonly used in old implementations.
/// Obsolete (2-digit year) date format defined by RFC 822 for http.
case rfc822 = "EEE',' dd' 'MMM' 'yy HH':'mm':'ss z"
/// Obsolete (2-digit year) date format defined by RFC 850 for usenet.
case rfc850 = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
/// Date format defined by RFC 1132 for http.
/// Date format defined by RFC 1123 for http.
case rfc1123 = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss z"
/// Date format defined by ISO 8601, also defined in RFC 3339. Used by Dropbox.
case iso8601 = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"
/// Date format defined by RFC 3339, as a profile of ISO 8601.
case rfc3339 = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZZZZZ"
/// Date format defined RFC 3339 as rare case with milliseconds.
case rfc3339Extended = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSSZZZZZ"
/// Date string returned by asctime() function.
case asctime = "EEE MMM d HH':'mm':'ss yyyy"
// Defining a http alias allows changing default time format if a new RFC becomes standard.
/// Equivalent to and defined by RFC 1123.
public static let http = RFCStandards.rfc1123
/// Equivalent to and defined by ISO 8610.
public static let rfc3339 = RFCStandards.iso8601
/// Equivalent to and defined by RFC 850.
public static let usenet = RFCStandards.rfc850
// Sorted by commonness
fileprivate static let allValues: [RFCStandards] = [.rfc1123, .rfc850, .iso8601, .asctime]
/* re. [RFC7231 section-7.1.1.1](https://tools.ietf.org/html/rfc7231#section-7.1.1.1)
"HTTP servers and client MUST accept all three HTTP-date formats" which are IMF-fixdate,
obsolete RFC 850 format and ANSI C's asctime() format.
ISO 8601 format is common in JSON and XML fields, defined by RFC 3339 as a timestamp format.
Though not mandated, we check string against them to allow using Date(rfcString:) in
wider and more general sitations.
We use RFC 822 instead of RFC 1123 to convert from string because NSDateFormatter can parse
both 2-digit and 4-digit year correctly when `dateFormat` year is 2-digit.
These values are sorted by frequency.
*/
fileprivate static let parsingCases: [RFCStandards] = [.rfc822, .rfc850, .asctime, .rfc3339, .rfc3339Extended]
}
private static let posixLocale = Locale(identifier: "en_US_POSIX")
private static let utcTimezone = TimeZone(identifier: "UTC")
/// Checks date string against various RFC standards and returns `Date`.
public init?(rfcString: String) {
let dateFor: DateFormatter = DateFormatter()
dateFor.locale = Locale(identifier: "en_US")
dateFor.locale = Date.posixLocale
for standard in RFCStandards.allValues {
for standard in RFCStandards.parsingCases {
dateFor.dateFormat = standard.rawValue
if let date = dateFor.date(from: rfcString) {
self = date
@@ -515,11 +533,12 @@ public extension Date {
}
/// Formats date according to RFCs standard.
public func format(with standard: RFCStandards, locale: Locale? = nil, timeZone: TimeZone? = nil) -> String {
/// - Note: local and timezone paramters should be nil for `.http` standard
internal func format(with standard: RFCStandards, locale: Locale? = nil, timeZone: TimeZone? = nil) -> String {
let fm = DateFormatter()
fm.dateFormat = standard.rawValue
fm.timeZone = timeZone ?? TimeZone(identifier: "UTC")
fm.locale = locale ?? Locale(identifier: "en_US_POSIX")
fm.timeZone = timeZone ?? Date.utcTimezone
fm.locale = locale ?? Date.posixLocale
return fm.string(from: self)
}
}
+10 -1
View File
@@ -252,7 +252,16 @@ open class VolumeObject {
allValues[.volumeNameKey] = newValue
}
}
/// The root directory of the resources volume, returned as an `URL` object.
open internal(set) var uuid: String? {
get {
return allValues[.volumeUUIDStringKey] as? String
}
set {
allValues[.volumeUUIDStringKey] = newValue
}
}
/// the volumes capacity in bytes, return -1 if is undetermined.
open internal(set) var totalCapacity: Int64 {
+11 -8
View File
@@ -307,12 +307,12 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
// WebDAV will override this function
}
fileprivate func doOperation(_ operation: FileOperationType, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
internal func doOperation(_ operation: FileOperationType, overwrite: Bool = false, progress: Progress? = nil, completionHandler: SimpleCompletionHandler) -> Progress? {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let progress = Progress(totalUnitCount: 1)
let progress = progress ?? Progress(totalUnitCount: 1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
@@ -321,12 +321,14 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderHTTPError?
if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
serverError = self.serverError(with: code, path: operation.source, data: data)
}
if let response = response as? HTTPURLResponse, FileProviderHTTPErrorCode(rawValue: response.statusCode) == .multiStatus, let data = data {
self.multiStatusHandler(source: operation.source, data: data, completionHandler: completionHandler)
if let response = response as? HTTPURLResponse {
if response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
serverError = self.serverError(with: code, path: operation.source, data: data)
}
if FileProviderHTTPErrorCode(rawValue: response.statusCode) == .multiStatus, let data = data {
self.multiStatusHandler(source: operation.source, data: data, completionHandler: completionHandler)
}
}
if serverError == nil && error == nil {
@@ -334,6 +336,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
} else {
progress.cancel()
}
completionHandler?(serverError ?? error)
self.delegateNotify(operation, error: serverError ?? error)
})
+72 -43
View File
@@ -92,6 +92,13 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
}
}
}
/// Microsoft Graph URL
public static var graphURL = URL(string: "https://graph.microsoft.com/")!
/// Microsoft Graph URL
public static var graphVersion = "v1.0"
/// Route for container, default is `.me`.
open let route: Route
@@ -129,7 +136,8 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
- 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 baseURL = (serverURL?.absoluteURL ?? OneDriveFileProvider.graphURL)
.appendingPathComponent(OneDriveFileProvider.graphVersion, isDirectory: true)
let refinedBaseURL = baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
self.route = route
super.init(baseURL: refinedBaseURL, credential: credential, cache: cache)
@@ -233,7 +241,8 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
/// Returns volume/provider information asynchronously.
/// - Parameter volumeInfo: Information of filesystem/Provider returned by system/server.
open override func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) {
var request = URLRequest(url: url(of: ""))
let url = URL(string: route.drivePath, relativeTo: baseURL)!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue(authentication: self.credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
@@ -244,6 +253,7 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
let volume = VolumeObject(allValues: [:])
volume.url = request.url
volume.uuid = json["id"] as? String
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
@@ -328,15 +338,18 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
*/
open func url(of path: String, modifier: String? = nil) -> URL {
var url: URL = baseURL!
var rpath: String = path
let isId = path.hasPrefix("id:")
var rpath: String = path.replacingOccurrences(of: "id:", with: "", options: .anchored)
//url.appendPathComponent("v1.0")
url.appendPathComponent(route.drivePath)
if isId {
url.appendPathComponent("root:")
} else {
if rpath.isEmpty {
url.appendPathComponent("root")
} else if isId {
url.appendPathComponent("items")
} else {
url.appendPathComponent("root:")
}
rpath = rpath.trimmingCharacters(in: pathTrimSet)
@@ -442,23 +455,24 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
case .fetch(path: let path):
method = "GET"
url = self.url(of: path, modifier: "content")
case .create(path: let path) where path.hasSuffix("/"):
method = "POST"
let parent = (path as NSString).deletingLastPathComponent
url = self.url(of: parent, modifier: "children")
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://"):
url = URL(string: self.url(of: path, modifier: "content").absoluteString + queryStr)!
case .copy(source: let source, destination: 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://"):
url = URL(string: self.url(of: dest, modifier: "content").absoluteString + queryStr)!
case .copy(source: let source, destination: let dest) where dest.hasPrefix("file://"):
method = "GET"
url = self.url(of: source, modifier: "content")
case .copy(source: let source, destination: _):
method = "POST"
url = self.url(of: source, modifier: "copy")
case .move(source: let source, destination: _):
method = "PATCH"
url = self.url(of: source)
@@ -471,29 +485,40 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
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)
// Remove gzip to fix availability of progress re. (Oleg Marchik)[https://github.com/evilutioner] PR (#61)
if method == "GET" {
request.setValue(acceptEncoding: .deflate)
request.addValue(acceptEncoding: .identity)
}
switch operation {
case .create(path: let path) where path.hasSuffix("/"):
request.setValue(contentType: .json)
var requestDictionary = [String: AnyObject]()
let name = (path as NSString).lastPathComponent
requestDictionary["name"] = name as NSString
requestDictionary["folder"] = NSDictionary()
requestDictionary["@microsoft.graph.conflictBehavior"] = "fail" as NSString
request.httpBody = Data(jsonDictionary: requestDictionary)
case .copy(let source, let dest) where !source.hasPrefix("file://") && !dest.hasPrefix("file://"),
.move(source: let source, destination: let dest):
request.setValue(contentType: .json)
request.setValue(contentType: .json, charset: .utf8)
let cdest = correctPath(dest) as NSString
var parentRefrence: [String: AnyObject] = [:]
var parentReference: [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
}
parentReference["id"] = cdest.components(separatedBy: "/").first?.replacingOccurrences(of: "id:", with: "", options: .anchored) as NSString?
} else {
parentRefrence["path"] = cdest.deletingLastPathComponent as NSString
parentReference["path"] = ("/drive/root:" as NSString).appendingPathComponent(cdest.deletingLastPathComponent) as NSString
}
switch self.route {
case .drive(uuid: let uuid):
parentReference["driveId"] = uuid.uuidString as NSString
default:
//parentReference["driveId"] = cachedDriveID as NSString? ?? ""
break
}
var requestDictionary = [String: AnyObject]()
requestDictionary["parentReference"] = parentRefrence as NSDictionary
requestDictionary["parentReference"] = parentReference as NSDictionary
requestDictionary["name"] = (cdest as NSString).lastPathComponent as NSString
request.httpBody = Data(jsonDictionary: requestDictionary)
default:
@@ -531,7 +556,7 @@ open class OneDriveFileProvider: HTTPFileProvider, 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"))
var request = URLRequest(url: self.url(of: path, modifier: "createLink"))
request.httpMethod = "POST"
let requestDictionary: [String: AnyObject] = ["type": "view" as NSString]
request.httpBody = Data(jsonDictionary: requestDictionary)
@@ -557,10 +582,6 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
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":
@@ -569,24 +590,32 @@ extension OneDriveFileProvider: ExtendedFileProvider {
return true
case "mp4", "mpg", "3gp", "mov", "avi", "wmv":
return true
case "doc", "docx", "xls", "xlsx", "ppt", "pptx", "pdf":
return true
default:
return false
}
}
open func propertiesOfFileSupported(path: String) -> Bool {
return true
}
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) -> Progress? {
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")
let thumbQuery: String
switch dimension.map( {max($0.width, $0.height) } ) ?? 0 {
case 0...96: thumbQuery = "small"
case 97...176: thumbQuery = "medium"
default: thumbQuery = "large"
}
let url = self.url(of: path, modifier: "thumbnails")
.appendingPathComponent("0").appendingPathComponent(thumbQuery)
.appendingPathComponent("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) {
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 400, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
let responseError = self.serverError(with: rCode, path: path, data: data)
completionHandler(nil, responseError)
return
+30 -9
View File
@@ -18,10 +18,9 @@ public struct FileProviderOneDriveError: FileProviderHTTPError {
/// Containts path, url and attributes of a OneDrive file or resource.
public final class OneDriveFileObject: FileObject {
internal init(baseURL: URL?, name: String, path: String) {
let rpath = (URL(string:path)?.appendingPathComponent(name).absoluteString)!.replacingOccurrences(of: "/", with: "", options: .anchored)
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: rpath)!
let url = URL(string: path, relativeTo: baseURL) ?? baseURL
super.init(url: url, name: name, path: rpath.removingPercentEncoding ?? path)
super.init(url: url, name: name, path: path)
}
internal convenience init? (baseURL: URL?, route: OneDriveFileProvider.Route, jsonStr: String) {
@@ -31,18 +30,31 @@ public final class OneDriveFileObject: FileObject {
internal convenience init? (baseURL: URL?, route: OneDriveFileProvider.Route, json: [String: AnyObject]) {
guard let name = json["name"] as? String else { return nil }
guard let path = json["parentReference"]?["path"] as? String else { return nil }
var lPath = path.replacingOccurrences(of: route.drivePath, with: "", options: .anchored, range: nil)
lPath = lPath.replacingOccurrences(of: "/:", with: "", options: .anchored)
lPath = lPath.replacingOccurrences(of: "//", with: "", options: .anchored)
self.init(baseURL: baseURL, name: name, path: lPath)
guard let id = json["id"] as? String else { return nil }
let path: String
if let refpath = json["parentReference"]?["path"] as? String {
let parentPath: String
if let colonIndex = refpath.index(of: ":") {
#if swift(>=4.0)
parentPath = String(refpath[refpath.index(after: colonIndex)...])
#else
parentPath = refpath.substring(from: refpath.index(after: colonIndex))
#endif
} else {
parentPath = refpath
}
path = (parentPath as NSString).appendingPathComponent(name)
} else {
path = "id:\(id)"
}
self.init(baseURL: baseURL, name: name, path: path)
self.id = id
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
self.childrensCount = json["folder"]?["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"]?["mimeType"] as? String).flatMap(ContentMIMEType.init(rawValue:)) ?? .stream
self.id = json["id"] as? String
self.entryTag = json["eTag"] as? String
let hashes = json["file"]?["hashes"] as? NSDictionary
// checks for both sha1 or quickXor. First is available in personal drives, second in business one.
@@ -313,8 +325,17 @@ internal extension OneDriveFileProvider {
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)
}
}