diff --git a/FileProvider.podspec b/FileProvider.podspec index 3857906..ae013f0 100644 --- a/FileProvider.podspec +++ b/FileProvider.podspec @@ -16,8 +16,8 @@ Pod::Spec.new do |s| # s.name = "FileProvider" - s.version = "0.9.0" - s.summary = "FileManager replacement for Local and Remote (WebDAV/Dropbox/SMB2) files on iOS and macOS." + s.version = "0.10.0" + s.summary = "FileManager replacement for Local and Remote (WebDAV/Dropbox/OneDrive/SMB2) files on iOS and macOS." # This description is used to generate tags and improve search results. # * Think: What does it do? Why did you write it? What is the focus? diff --git a/FileProvider.xcodeproj/project.pbxproj b/FileProvider.xcodeproj/project.pbxproj index 54aa1cc..5eda268 100644 --- a/FileProvider.xcodeproj/project.pbxproj +++ b/FileProvider.xcodeproj/project.pbxproj @@ -98,17 +98,20 @@ 79BD63A81E2CC2940035128C /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63A71E2CC2940035128C /* CoreGraphics.framework */; }; 79BD63AA1E2CC2BB0035128C /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63A91E2CC2BB0035128C /* AVFoundation.framework */; }; 79BD63AC1E2CC2C20035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63AB1E2CC2C20035128C /* ImageIO.framework */; }; - 79BD63AE1E2CC2EB0035128C /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63AD1E2CC2EB0035128C /* MediaPlayer.framework */; }; 79BD63B01E2CC3300035128C /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63AF1E2CC3300035128C /* libxml2.tbd */; }; 79BD63B21E2CC3350035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B11E2CC3350035128C /* ImageIO.framework */; }; - 79BD63B41E2CC33D0035128C /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B31E2CC33D0035128C /* MediaPlayer.framework */; }; 79BD63B61E2CC3860035128C /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B51E2CC3860035128C /* CoreFoundation.framework */; }; 79BD63B81E2CC38D0035128C /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B71E2CC38D0035128C /* AVFoundation.framework */; }; 79BD63BA1E2CC39B0035128C /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B91E2CC39B0035128C /* libxml2.tbd */; }; - 79BD63BC1E2CC3B90035128C /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63BB1E2CC3B90035128C /* MediaPlayer.framework */; }; 79BD63BE1E2CC3C20035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63BD1E2CC3C20035128C /* ImageIO.framework */; }; 79BD63C01E2CC3CD0035128C /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */; }; 79BD63C21E2CC3D30035128C /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63C11E2CC3D30035128C /* AVFoundation.framework */; }; + 79BD63C51E2D17880035128C /* OneDriveFileProvide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */; }; + 79BD63C61E2D17880035128C /* OneDriveFileProvide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */; }; + 79BD63C71E2D17880035128C /* OneDriveFileProvide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */; }; + 79BD63C81E2D17880035128C /* OneDriveHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C41E2D17880035128C /* OneDriveHelper.swift */; }; + 79BD63C91E2D17880035128C /* OneDriveHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C41E2D17880035128C /* OneDriveHelper.swift */; }; + 79BD63CA1E2D17880035128C /* OneDriveHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C41E2D17880035128C /* OneDriveHelper.swift */; }; 79F5745B1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; }; 79F5745C1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; }; 79F5745D1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; }; @@ -166,6 +169,8 @@ 79BD63BD1E2CC3C20035128C /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/ImageIO.framework; sourceTree = DEVELOPER_DIR; }; 79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/CoreGraphics.framework; sourceTree = DEVELOPER_DIR; }; 79BD63C11E2CC3D30035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; }; + 79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneDriveFileProvide.swift; sourceTree = ""; }; + 79BD63C41E2D17880035128C /* OneDriveHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneDriveHelper.swift; sourceTree = ""; }; 79F5745A1DFDB10A00179ABF /* FileObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileObject.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -177,7 +182,6 @@ 79BD63AA1E2CC2BB0035128C /* AVFoundation.framework in Frameworks */, 79BD63AC1E2CC2C20035128C /* ImageIO.framework in Frameworks */, 79BD63A81E2CC2940035128C /* CoreGraphics.framework in Frameworks */, - 79BD63AE1E2CC2EB0035128C /* MediaPlayer.framework in Frameworks */, 791950F51DE58A5400B4426E /* libxml2.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -189,7 +193,6 @@ 79BD63B81E2CC38D0035128C /* AVFoundation.framework in Frameworks */, 79BD63B61E2CC3860035128C /* CoreFoundation.framework in Frameworks */, 79BD63B21E2CC3350035128C /* ImageIO.framework in Frameworks */, - 79BD63B41E2CC33D0035128C /* MediaPlayer.framework in Frameworks */, 79BD63B01E2CC3300035128C /* libxml2.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -201,7 +204,6 @@ 79BD63C21E2CC3D30035128C /* AVFoundation.framework in Frameworks */, 79BD63C01E2CC3CD0035128C /* CoreGraphics.framework in Frameworks */, 79BD63BE1E2CC3C20035128C /* ImageIO.framework in Frameworks */, - 79BD63BC1E2CC3B90035128C /* MediaPlayer.framework in Frameworks */, 79BD63BA1E2CC39B0035128C /* libxml2.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -279,8 +281,6 @@ children = ( 7924B18B1D89DAE000589DB7 /* AEXML */, 799396991D48C02300086753 /* SMBTypes */, - 799396931D48C02300086753 /* DropboxFileProvider.swift */, - 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */, 799396941D48C02300086753 /* FileProvider.h */, 799396951D48C02300086753 /* FileProvider.swift */, 79F5745A1DFDB10A00179ABF /* FileObject.swift */, @@ -288,6 +288,10 @@ 792572401DF23BDA006A1526 /* LocalHelper.swift */, 79BD638B1E2CC2300035128C /* ExtendedLocalFileProvider.swift */, 7902C0851D61B56D00564440 /* RemoteSession.swift */, + 799396931D48C02300086753 /* DropboxFileProvider.swift */, + 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */, + 79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */, + 79BD63C41E2D17880035128C /* OneDriveHelper.swift */, 7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */, 799396971D48C02300086753 /* SMBClient.swift */, 799396981D48C02300086753 /* SMBFileProvider.swift */, @@ -477,11 +481,13 @@ 7924B1A21D89DAE000589DB7 /* Options.swift in Sources */, 792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */, 79F5745B1DFDB10B00179ABF /* FileObject.swift in Sources */, + 79BD63C51E2D17880035128C /* OneDriveFileProvide.swift in Sources */, 7924B1991D89DAE000589DB7 /* Element.swift in Sources */, 799396C81D48C02300086753 /* SMB2IOCtl.swift in Sources */, 799396D71D48C02300086753 /* SMB2Types.swift in Sources */, 7924B1B21D89FCDA00589DB7 /* FPSStreamTask.swift in Sources */, 799396C51D48C02300086753 /* SMB2FileOperation.swift in Sources */, + 79BD63C81E2D17880035128C /* OneDriveHelper.swift in Sources */, 799396BF1D48C02300086753 /* SMB2DataTypes.swift in Sources */, 799396B91D48C02300086753 /* SMBFileProvider.swift in Sources */, 794C220E1D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */, @@ -514,11 +520,13 @@ 7924B1A31D89DAE000589DB7 /* Options.swift in Sources */, 792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */, 79F5745C1DFDB10B00179ABF /* FileObject.swift in Sources */, + 79BD63C61E2D17880035128C /* OneDriveFileProvide.swift in Sources */, 7924B1B01D89F7DE00589DB7 /* FPSStreamTask.swift in Sources */, 7924B19A1D89DAE000589DB7 /* Element.swift in Sources */, 799396C91D48C02300086753 /* SMB2IOCtl.swift in Sources */, 799396D81D48C02300086753 /* SMB2Types.swift in Sources */, 799396C61D48C02300086753 /* SMB2FileOperation.swift in Sources */, + 79BD63C91E2D17880035128C /* OneDriveHelper.swift in Sources */, 799396C01D48C02300086753 /* SMB2DataTypes.swift in Sources */, 799396BA1D48C02300086753 /* SMBFileProvider.swift in Sources */, 794C220F1D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */, @@ -551,11 +559,13 @@ 7924B1A41D89DAE000589DB7 /* Options.swift in Sources */, 792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */, 79F5745D1DFDB10B00179ABF /* FileObject.swift in Sources */, + 79BD63C71E2D17880035128C /* OneDriveFileProvide.swift in Sources */, 7924B1B11D89F7DF00589DB7 /* FPSStreamTask.swift in Sources */, 7924B19B1D89DAE000589DB7 /* Element.swift in Sources */, 799396CA1D48C02300086753 /* SMB2IOCtl.swift in Sources */, 799396D91D48C02300086753 /* SMB2Types.swift in Sources */, 799396C71D48C02300086753 /* SMB2FileOperation.swift in Sources */, + 79BD63CA1E2D17880035128C /* OneDriveHelper.swift in Sources */, 799396C11D48C02300086753 /* SMB2DataTypes.swift in Sources */, 799396BB1D48C02300086753 /* SMBFileProvider.swift in Sources */, 794C22101D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */, @@ -585,7 +595,7 @@ 799396601D48B7BF00086753 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_VERSION_STRING = 0.8.3; + BUNDLE_VERSION_STRING = 0.10.0; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_EMPTY_BODY = YES; @@ -615,7 +625,7 @@ 799396611D48B7BF00086753 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_VERSION_STRING = 0.8.3; + BUNDLE_VERSION_STRING = 0.10.0; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_EMPTY_BODY = YES; diff --git a/README.md b/README.md index dc0aaca..7ed969e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) ---> -This library provides implementaion of WebDav and SMB2 (incomplete) and local files. +This library provides implementaion of WebDav, Dropbox, OneDrive and SMB2 (incomplete) and local files. All functions are async calls and it wont block your main thread. @@ -27,6 +27,7 @@ Local and WebDAV providers are fully tested and can be used in production enviro - [x] **LocalFileProvider** a wrapper around `FileManager` with some additions like searching and reading a portion of file. - [x] **WebDAVFileProvider** WebDAV protocol is defacto file transmission standard, replaced FTP. - [x] **DropboxFileProvider** A wrapper around Dropbox Web API. For now it has limitation in uploading files up to 150MB. +- [x] **OneDriveFileProvider** A wrapper around OneDrive Web API, works with `onedrive.com` and compatible servers. For now it has limitation in uploading files up to 100MB. - [ ] **SMBFileProvider** SMB2/3 introduced in 2006, which is a file and printer sharing protocol originated from Microsoft Windows and now is replacing AFP protocol on MacOS. I implemented data types and some basic functions but *main interface is not implemented yet!* SMB1/CIFS is depericated and very tricky to be implemented - [ ] **FTPFileProvider** while deprecated in 1990s, it's still in use on some Web hosts. - [ ] **AmazonS3FileProvider** @@ -117,7 +118,7 @@ let webdavProvider = WebDAVFileProvider(baseURL: URL(string: "http://www.example * In case you want to connect non-secure servers for WebDAV (http) 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, 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. For interaction with UI, set delegate variable of `FileProvider` object @@ -332,7 +333,7 @@ documentsProvider.unregisterNotifcation(path: provider.currentPath) Providers which conform `ExtendedFileProvider` are able to generate thumbnail or provide file meta-information for images, media and pdf files. -Local and Dropbox providers support this functionality. +Local, OneDrive and Dropbox providers support this functionality. ##### Thumbnails To check either file thumbnail is supported or not and fetch thumbnail, use (and modify) these example code: @@ -349,6 +350,8 @@ if documentsProvider.thumbnailOfFileSupported(path: path { } ``` +* Please note it won't cache generated images. if you don't do it yourself, it may hit you app's performance. + ##### Meta-informations To get meta-information like image/video taken date, dimension, etc., use (and modify) these example code: diff --git a/Sources/DropboxFileProvider.swift b/Sources/DropboxFileProvider.swift index bd86914..486bcb5 100644 --- a/Sources/DropboxFileProvider.swift +++ b/Sources/DropboxFileProvider.swift @@ -14,10 +14,14 @@ import CoreGraphics // in case of using this class with unencrypted HTTP connection. open class DropboxFileProvider: NSObject, FileProviderBasicRemote { - open static let type: String = "WebDAV" + open static let type: String = "DropBox" open let isPathRelative: Bool = true open let baseURL: URL? open var currentPath: String = "" + + open let apiURL = URL(string: "https://api.dropboxapi.com/2/")! + open let contentURL = URL(string: "https://content.dropboxapi.com/2")! + open var dispatch_queue: DispatchQueue { willSet { assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.") @@ -63,7 +67,7 @@ open class DropboxFileProvider: NSObject, FileProviderBasicRemote { } open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) { - let url = URL(string: "https://api.dropboxapi.com/2/files/get_metadata")! + let url = URL(string: "files/get_metadata", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") @@ -86,7 +90,7 @@ open class DropboxFileProvider: NSObject, FileProviderBasicRemote { } open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) { - let url = URL(string: "https://api.dropboxapi.com/2/users/get_space_usage")! + let url = URL(string: "users/get_space_usage", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") @@ -139,17 +143,17 @@ extension DropboxFileProvider: FileProviderOperations { let destPath = operation.destination switch operation { case .create: - url = "https://api.dropboxapi.com/2/files/create_folder" + url = "files/create_folder" case .copy: - url = "https://api.dropboxapi.com/2/files/copy" + url = "files/copy" case .move: - url = "https://api.dropboxapi.com/2/files/move" + url = "files/move" case .remove: - url = "https://api.dropboxapi.com/2/files/delete" + url = "files/delete" default: // modify, link, fetch return nil } - var request = URLRequest(url: URL(string: url)!) + var request = URLRequest(url: URL(string: url, relativeTo: apiURL)!) request.httpMethod = "POST" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") @@ -187,7 +191,7 @@ extension DropboxFileProvider: FileProviderOperations { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } - let url = URL(string: "https://content.dropboxapi.com/2/files/download")! + let url = URL(string: "files/download", relativeTo: contentURL)! var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") @@ -218,7 +222,7 @@ extension DropboxFileProvider: FileProviderOperations { extension DropboxFileProvider: FileProviderReadWrite { public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { let opType = FileOperationType.fetch(path: path) - let url = URL(string: "https://content.dropboxapi.com/2/files/download")! + let url = URL(string: "files/download", relativeTo: contentURL)! var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") @@ -279,7 +283,7 @@ extension DropboxFileProvider: FileProviderReadWrite { extension DropboxFileProvider { open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) { - let url = URL(string: "https://api.dropboxapi.com/2/files/get_temporary_link")! + let url = URL(string: "files/get_temporary_link", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") @@ -308,7 +312,7 @@ extension DropboxFileProvider { } open func copyItem(path: String, toRemoteURL destURL: URL, completionHandler: @escaping ((_ jobId: String?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) { - let url = URL(string: "https://api.dropboxapi.com/2/files/save_url")! + let url = URL(string: "files/save_url", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") @@ -370,13 +374,13 @@ extension DropboxFileProvider: ExtendedFileProvider { let url: URL switch (path as NSString).pathExtension.lowercased() { case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff": - url = URL(string: "https://content.dropboxapi.com/2/files/get_thumbnail")! + url = URL(string: "files/get_thumbnail", relativeTo: contentURL)! case "doc", "docx", "docm", "xls", "xlsx", "xlsm": fallthrough case "ppt", "pps", "ppsx", "ppsm", "pptx", "pptm": fallthrough case "rtf": - url = URL(string: "https://content.dropboxapi.com/2/files/get_preview")! + url = URL(string: "files/get_preview", relativeTo: contentURL)! default: return } @@ -410,7 +414,7 @@ extension DropboxFileProvider: ExtendedFileProvider { } public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) { - let url = URL(string: "https://api.dropboxapi.com/2/files/get_metadata")! + let url = URL(string: "files/get_metadata", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") diff --git a/Sources/DropboxHelper.swift b/Sources/DropboxHelper.swift index e922c98..a893c14 100644 --- a/Sources/DropboxHelper.swift +++ b/Sources/DropboxHelper.swift @@ -2,8 +2,8 @@ // DropboxHelper.swift // FileProvider // -// Created by Amir Abbas Mousavian on 5/18/95. -// +// Created by Amir Abbas Mousavian. +// Copyright © 2016 Mousavian. Distributed under MIT license. // import Foundation @@ -34,10 +34,10 @@ public final class DropboxFileObject: FileObject { open internal(set) var id: String? { get { - return allValues["NSURLDropboxDocumentIdentifyKey"] as? String + return allValues["NSURLDocumentIdentifyKey"] as? String } set { - allValues["NSURLDropboxDocumentIdentifyKey"] = newValue + allValues["NSURLDocumentIdentifyKey"] = newValue } } @@ -57,10 +57,10 @@ internal extension DropboxFileProvider { var requestDictionary = [String: AnyObject]() let url: URL if let cursor = cursor { - url = URL(string: "https://api.dropboxapi.com/2/files/list_folder/continue")! + url = URL(string: "files/list_folder/continue", relativeTo: apiURL)! requestDictionary["cursor"] = cursor as NSString? } else { - url = URL(string: "https://api.dropboxapi.com/2/files/list_folder")! + url = URL(string: "files/list_folder", relativeTo: apiURL)! requestDictionary["path"] = correctPath(path) as NSString? requestDictionary["recursive"] = recursive as NSNumber? } @@ -101,7 +101,7 @@ internal extension DropboxFileProvider { assert(data.count < 150*1024*1024, "Maximum size of allowed size to upload is 150MB") var requestDictionary = [String: Any]() let url: URL - url = URL(string: "https://content.dropboxapi.com/2/files/upload")! + url = URL(string: "files/upload", relativeTo: contentURL)! requestDictionary["path"] = correctPath(targetPath) as NSString? requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString requestDictionary["client_modified"] = string(from:modifiedDate) @@ -127,7 +127,7 @@ internal extension DropboxFileProvider { func upload_simple(_ targetPath: String, localFile: URL, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? { var requestDictionary = [String: Any]() let url: URL - url = URL(string: "https://content.dropboxapi.com/2/files/upload")! + url = URL(string: "files/upload", relativeTo: contentURL)! requestDictionary["path"] = correctPath(targetPath) as NSString? requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString requestDictionary["client_modified"] = string(from:modifiedDate) @@ -150,7 +150,7 @@ internal extension DropboxFileProvider { } func search(_ startPath: String = "", query: String, start: Int = 0, maxResultPerPage: Int = 25, maxResults: Int = -1, foundItem:@escaping ((_ file: DropboxFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) { - let url = URL(string: "https://api.dropboxapi.com/2/files/search")! + let url = URL(string: "files/search", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") @@ -206,7 +206,7 @@ internal extension DropboxFileProvider { fileObject.type = (json[".tag"] as? String) == "folder" ? .directory : .regular fileObject.isReadOnly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false fileObject.id = json["id"] as? String - fileObject.rev = json["id"] as? String + fileObject.rev = json["rev"] as? String return fileObject } diff --git a/Sources/ExtendedLocalFileProvider.swift b/Sources/ExtendedLocalFileProvider.swift index c04b49f..27b22c3 100644 --- a/Sources/ExtendedLocalFileProvider.swift +++ b/Sources/ExtendedLocalFileProvider.swift @@ -10,10 +10,9 @@ import Foundation import ImageIO import CoreGraphics import AVFoundation -import MediaPlayer #if os(iOS) || os(tvOS) import UIKit -#elseif os(OSX) +#elseif os(macOS) import Cocoa #endif @@ -166,7 +165,7 @@ public struct LocalFileInformationGenerator { assetImgGenerate.appliesPreferredTrackTransform = true let time = CMTimeMake(asset.duration.value / 3, asset.duration.timescale) if let cgImage = try? assetImgGenerate.copyCGImage(at: time, actualTime: nil) { - #if os(OSX) + #if os(macOS) return ImageClass(cgImage: cgImage, size: NSSize.zero) #else return ImageClass(cgImage: cgImage) diff --git a/Sources/FileProvider.swift b/Sources/FileProvider.swift index 3080f76..7cc3133 100644 --- a/Sources/FileProvider.swift +++ b/Sources/FileProvider.swift @@ -10,7 +10,7 @@ import Foundation #if os(iOS) || os(tvOS) import UIKit public typealias ImageClass = UIImage -#elseif os(OSX) +#elseif os(macOS) import Cocoa public typealias ImageClass = NSImage #endif @@ -33,6 +33,8 @@ public protocol FileProviderBasic: class { func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) + + func url(of path: String?) -> URL } public protocol FileProviderBasicRemote: FileProviderBasic { @@ -181,16 +183,25 @@ public protocol FileProviderMonitor: FileProviderBasic { public protocol FileProvider: FileProviderBasic, FileProviderOperations, FileProviderReadWrite, NSCopying { } +fileprivate let pathTrimSet = CharacterSet(charactersIn: " /") extension FileProviderBasic { public var type: String { return Self.type } public var bareCurrentPath: String { - return currentPath.trimmingCharacters(in: CharacterSet(charactersIn: ". /")) + return currentPath.trimmingCharacters(in: pathTrimSet) + } + + func escaped(path: String) -> String { + return path.trimmingCharacters(in: pathTrimSet) } public func absoluteURL(_ path: String? = nil) -> URL { + return url(of: path).absoluteURL + } + + public func url(of path: String? = nil) -> URL { let rpath: String if let path = path { rpath = path @@ -201,9 +212,9 @@ extension FileProviderBasic { if rpath.hasPrefix("/") && baseURL.absoluteString.hasSuffix("/") { var npath = rpath npath.remove(at: npath.startIndex) - return baseURL.appendingPathComponent(npath) + return URL(string: npath, relativeTo: baseURL)! } else { - return baseURL.appendingPathComponent(rpath) + return URL(string: rpath, relativeTo: baseURL)! } } else { return URL(fileURLWithPath: rpath).standardizedFileURL @@ -349,16 +360,16 @@ extension ExtendedFileProvider { return data.count > 4 && data.scanString(length: 4, encoding: .ascii) == "%PDF" } - internal static func convertToImage(pdfData: Data?) -> ImageClass? { + internal static func convertToImage(pdfData: Data?, page: Int = 1) -> ImageClass? { guard let pdfData = pdfData else { return nil } let cfPDFData: CFData = pdfData as CFData - if let provider = CGDataProvider(data: cfPDFData), let reference = CGPDFDocument(provider), let pageRef = reference.page(at: 1) { + if let provider = CGDataProvider(data: cfPDFData), let reference = CGPDFDocument(provider), let pageRef = reference.page(at: page) { let frame = pageRef.getBoxRect(CGPDFBox.mediaBox) var size = frame.size let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) - #if os(OSX) + #if os(macOS) let ppp = Int(NSScreen.main()?.backingScaleFactor ?? 1) // fetch device is retina or not size.width *= CGFloat(ppp) @@ -423,7 +434,7 @@ extension ExtendedFileProvider { let newSize = CGSize(width: width, height: height) - #if os(OSX) + #if os(macOS) var imageRect = NSRect(origin: CGPoint.zero, size: image.size) let imageRef = image.cgImage(forProposedRect: &imageRect, context: nil, hints: nil) diff --git a/Sources/OneDriveFileProvide.swift b/Sources/OneDriveFileProvide.swift new file mode 100644 index 0000000..81b979b --- /dev/null +++ b/Sources/OneDriveFileProvide.swift @@ -0,0 +1,352 @@ + +// +// OneDriveFileProvider.swift +// FileProvider +// +// Created by Amir Abbas Mousavian. +// Copyright © 2017 Mousavian. Distributed under MIT license. +// + +import Foundation +import CoreGraphics + +// Because this class uses NSURLSession, it's necessary to disable App Transport Security +// in case of using this class with unencrypted HTTP connection. + +open class OneDriveFileProvider: NSObject, FileProviderBasicRemote { + open static let type: String = "OneDrive" + open let isPathRelative: Bool = true + open let baseURL: URL? + open var drive: String + open var driveURL: URL { + return URL(string: "/drive/root:/", relativeTo: baseURL)! + } + open var currentPath: String = "" + + open var dispatch_queue: DispatchQueue { + willSet { + assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.") + } + } + open weak var delegate: FileProviderDelegate? + open let credential: URLCredential? + open private(set) var cache: URLCache? + public var useCache: Bool = false + public var validatingCache: Bool = true + + fileprivate var _session: URLSession? + fileprivate var sessionDelegate: SessionDelegate? + public var session: URLSession { + if _session == nil { + self.sessionDelegate = SessionDelegate(fileProvider: self, credential: credential) + let queue = OperationQueue() + //queue.underlyingQueue = dispatch_queue + let config = URLSessionConfiguration.default + config.urlCache = cache + config.requestCachePolicy = .returnCacheDataElseLoad + _session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: queue) + } + return _session! + } + + public init? (baseURL: URL?, drive: String = "root", credential: URLCredential?, cache: URLCache? = nil) { + self.baseURL = baseURL ?? URL(string: "https://api.onedrive.com") + self.drive = drive + dispatch_queue = DispatchQueue(label: "FileProvider.\(OneDriveFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent) + //let url = baseURL.uw_absoluteString + self.credential = credential + self.cache = cache + } + + deinit { + _session?.invalidateAndCancel() + } + + open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) { + list(path) { (contents, cursor, error) in + completionHandler(contents, error) + } + } + + open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) { + let url = URL(string: escaped(path: path), relativeTo: driveURL)! + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") + let task = session.dataTask(with: request, completionHandler: { (data, response, error) in + var dbError: FileProviderOneDriveError? + var fileObject: OneDriveFileObject? + if let response = response as? HTTPURLResponse { + let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) + dbError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil + if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let file = self.mapToFileObject(json) { + fileObject = file + } + } + completionHandler(fileObject, dbError ?? error) + }) + task.resume() + } + + open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) { + let url = URL(string: "/drive/root", relativeTo: baseURL)! + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") + let task = session.dataTask(with: request, completionHandler: { (data, response, error) in + var totalSize: Int64 = -1 + var usedSize: Int64 = 0 + if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) { + totalSize = (json["total"] as? NSNumber)?.int64Value ?? -1 + usedSize = (json["used"] as? NSNumber)?.int64Value ?? 0 + } + completionHandler(totalSize, usedSize) + }) + task.resume() + } + + open weak var fileOperationDelegate: FileOperationDelegate? +} + +extension OneDriveFileProvider: FileProviderOperations { + + + public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + let path = (atPath as NSString).appendingPathComponent(folderName) + "/" + return doOperation(.create(path: path), completionHandler: completionHandler) + } + + public func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + let filePath = (path as NSString).appendingPathComponent(fileName) + return self.writeContents(path: filePath, contents: data ?? Data(), completionHandler: completionHandler) + } + + public func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler) + } + + public func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler) + } + + public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + return doOperation(.remove(path: path), completionHandler: completionHandler) + } + + fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else { + return nil + } + guard let sourcePath = operation.source else { return nil } + let destPath = operation.destination + var request = URLRequest(url: URL(string: sourcePath, relativeTo: driveURL)!) + switch operation { + case .create: + request.httpMethod = "CREATE" + case .copy: + request.httpMethod = "POST" + case .move: + request.httpMethod = "PATCH" + case .remove: + request.httpMethod = "DELETE" + default: // modify, link, fetch + return nil + } + + request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") + var requestDictionary = [String: AnyObject]() + if let dest = correctPath(destPath) as NSString? { + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + requestDictionary["parentReference"] = ("/drive/\(drive):" + dest.deletingLastPathComponent) as NSString + requestDictionary["name"] = dest.lastPathComponent as NSString + request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8) + } + let task = session.dataTask(with: request, completionHandler: { (data, response, error) in + var dbError: FileProviderOneDriveError? + if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) { + dbError = FileProviderOneDriveError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) + } + completionHandler?(dbError ?? error) + self.delegateNotify(operation, error: dbError ?? error) + }) + task.taskDescription = operation.json + task.resume() + return RemoteOperationHandle(operationType: operation, tasks: [task]) + } + + public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath) + guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { + return nil + } + return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler) + } + + public func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString) + guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { + return nil + } + let url = URL(string: escaped(path: path) + ":/content", relativeTo: driveURL)! + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") + let task = session.downloadTask(with: request, completionHandler: { (cacheURL, response, error) in + guard let cacheURL = cacheURL, let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode < 300 else { + let code = FileProviderHTTPErrorCode(rawValue: (response as? HTTPURLResponse)?.statusCode ?? -1) + let errorData : Data? = nil //Data(contentsOf:cacheURL) // TODO: Figure out how to get error response data for the error description + let dbError : FileProviderOneDriveError? = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil + completionHandler?(dbError ?? error) + return + } + do { + try FileManager.default.moveItem(at: cacheURL, to: destURL) + completionHandler?(nil) + } catch let e { + completionHandler?(e) + } + }) + task.taskDescription = opType.json + task.resume() + return RemoteOperationHandle(operationType: opType, tasks: [task]) + } +} + +extension OneDriveFileProvider: FileProviderReadWrite { + public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { + let opType = FileOperationType.fetch(path: path) + let url = URL(string: escaped(path: path) + ":/content", relativeTo: driveURL)! + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") + if length > 0 { + request.setValue("bytes=\(offset)-\(offset + length)", forHTTPHeaderField: "Range") + } else if offset > 0 && length < 0 { + request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range") + } + let task = session.dataTask(with: request, completionHandler: { (data, response, error) in + var dbError: FileProviderOneDriveError? + if let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: httpResponse.statusCode) { + dbError = FileProviderOneDriveError(code: code, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) + } + let filedata = dbError ?? error == nil ? data : nil + completionHandler(filedata, dbError ?? error) + }) + task.taskDescription = opType.json + task.resume() + return RemoteOperationHandle(operationType: opType, tasks: [task]) + } + + public func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + let opType = FileOperationType.modify(path: path) + guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { + return nil + } + // FIXME: remove 150MB restriction + return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler) + } + + public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { + var foundFiles = [OneDriveFileObject]() + search(path, query: query, foundItem: { (file) in + foundFiles.append(file) + foundItemHandler?(file) + }, completionHandler: { (error) in + completionHandler(foundFiles, error) + }) + } + + 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() + } + + // TODO: Implement /copy_reference, /get_account & /get_current_account +} + + +extension OneDriveFileProvider: ExtendedFileProvider { + public func thumbnailOfFileSupported(path: String) -> Bool { + return true + } + + public 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 + } + } + + public func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) { + let url: URL + if let dimension = dimension { + url = URL(string: escaped(path: path) + ":/thumbnails/0/=c\(dimension.width)x\(dimension.height)/content", relativeTo: driveURL)! + } else { + url = URL(string: escaped(path: path) + ":/thumbnails/0/small/content", relativeTo: driveURL)! + } + + var request = URLRequest(url: url) + request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") + 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() + } + + public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) { + let url = URL(string: escaped(path: path), relativeTo: driveURL)! + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") + let task = session.dataTask(with: request, completionHandler: { (data, response, error) in + var dbError: FileProviderOneDriveError? + var dic = [String: Any]() + var keys = [String]() + if let response = response as? HTTPURLResponse { + let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) + dbError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil + if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) { + (dic, keys) = self.mapMediaInfo(json) + } + } + completionHandler(dic, keys, dbError ?? error) + }) + task.resume() + } +} + +extension OneDriveFileProvider: FileProvider { + open func copy(with zone: NSZone? = nil) -> Any { + let copy = OneDriveFileProvider(baseURL: self.baseURL, drive: self.drive, credential: self.credential, 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 + } +} diff --git a/Sources/OneDriveHelper.swift b/Sources/OneDriveHelper.swift new file mode 100644 index 0000000..b9c0b8e --- /dev/null +++ b/Sources/OneDriveHelper.swift @@ -0,0 +1,305 @@ +// +// OneDriveHelper.swift +// FileProvider +// +// Created by Amir Abbas Mousavian. +// Copyright © 2017 Mousavian. Distributed under MIT license. +// + +import Foundation + +public struct FileProviderOneDriveError: Error, CustomStringConvertible { + public let code: FileProviderHTTPErrorCode + public let path: String + public let errorDescription: String? + + public var description: String { + return code.description + } +} + +public final class OneDriveFileObject: FileObject { + internal init(name: String, path: String) { + super.init(absoluteURL: URL(string: path), name: name, path: path) + } + + + open internal(set) var id: String? { + get { + return allValues["NSURLDocumentIdentifyKey"] as? String + } + set { + allValues["NSURLDocumentIdentifyKey"] = newValue + } + } + + open internal(set) var contentType: String { + get { + return allValues["NSURLContentTypeKey"] as? String ?? "" + } + set { + allValues["NSURLContentTypeKey"] = newValue + } + } + + open internal(set) var entryTag: String? { + get { + return allValues["NSURLEntryTagKey"] as? String + } + set { + allValues["NSURLEntryTagKey"] = newValue + } + } +} + +// codebeat:disable[ARITY] +internal extension OneDriveFileProvider { + func list(_ path: String, cursor: String? = nil, prevContents: [OneDriveFileObject] = [], completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) { + let url: URL + if let cursor = cursor { + url = URL(string: cursor)! + } else { + url = URL(string: escaped(path: path), relativeTo: driveURL)! + } + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") + let task = session.dataTask(with: request, completionHandler: { (data, response, error) in + var responseError: FileProviderOneDriveError? + var files = prevContents + if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { + responseError = FileProviderOneDriveError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) + } + if let data = data, let jsonStr = String(data: data, encoding: .utf8) { + let json = jsonToDictionary(jsonStr) + if let entries = json?["value"] as? [AnyObject] , entries.count > 0 { + for entry in entries { + if let entry = entry as? [String: AnyObject], let file = self.mapToFileObject(entry) { + files.append(file) + } + } + let ncursor = json?["@odata.nextLink"] as? String + let hasmore = ncursor != nil + if hasmore { + self.list(path, cursor: ncursor, prevContents: files, completionHandler: completionHandler) + return + } + } + } + completionHandler(files, nil, responseError ?? error) + }) + task.taskDescription = FileOperationType.fetch(path: path).json + task.resume() + } + + func upload_simple(_ targetPath: String, data: Data, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + assert(data.count < 100*1024*1024, "Maximum size of allowed size to upload is 100MB") + let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail" + let url = URL(string: escaped(path: targetPath) + ":/content" + queryStr, relativeTo: driveURL)! + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") + request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type") + request.httpBody = data + let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in + var responseError: FileProviderOneDriveError? + if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { + responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) + } + completionHandler?(responseError ?? error) + self.delegateNotify(.create(path: targetPath), error: responseError ?? error) + }) + task.taskDescription = operation.json + task.resume() + return RemoteOperationHandle(operationType: operation, tasks: [task]) + } + + func upload_simple(_ targetPath: String, localFile: URL, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? { + let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail" + let url = URL(string: escaped(path: targetPath) + ":/content" + queryStr, relativeTo: driveURL)! + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") + request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type") + let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in + var responseError: FileProviderOneDriveError? + if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { + responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) + } + completionHandler?(responseError ?? error) + self.delegateNotify(.create(path: targetPath), error: responseError ?? error) + }) + task.taskDescription = operation.json + task.resume() + return RemoteOperationHandle(operationType: operation, tasks: [task]) + } + + func search(_ startPath: String = "", query: String, next: String? = nil, foundItem:@escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) { + let url: URL + if let next = next { + url = URL(string: next)! + } else if self.escaped(path: startPath) == "" { + url = URL(string: "/drive/\(drive)/view.search?q=\(query)", relativeTo: baseURL)! + } else { + url = URL(string: "\(escaped(path: startPath))/view.search?q=\(query)", relativeTo: driveURL)! + } + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") + + let task = session.dataTask(with: request, completionHandler: { (data, response, error) in + var responseError: FileProviderOneDriveError? + if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { + responseError = FileProviderOneDriveError(code: rCode, path: startPath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) + } + if let data = data, let jsonStr = String(data: data, encoding: .utf8) { + let json = jsonToDictionary(jsonStr) + if let entries = json?["value"] as? [AnyObject] , entries.count > 0 { + for entry in entries { + if let entry = entry as? [String: AnyObject], let file = self.mapToFileObject(entry) { + foundItem(file) + } + } + let next = json?["@odata.nextLink"] as? String + let hasmore = next != nil + if hasmore, let next = next { + self.search(startPath, query: query, next: next, foundItem: foundItem, completionHandler: completionHandler) + } else { + completionHandler(responseError ?? error) + } + return + } + } + completionHandler(responseError ?? error) + }) + task.resume() + } +} +// codebeat:enable[ARITY] + +internal extension OneDriveFileProvider { + func mapToFileObject(_ jsonStr: String) -> OneDriveFileObject? { + guard let json = jsonToDictionary(jsonStr) else { return nil } + return self.mapToFileObject(json) + } + + func mapToFileObject(_ json: [String: AnyObject]) -> OneDriveFileObject? { + guard let name = json["name"] as? String else { return nil } + guard let path = (json["parentReference"] as? NSDictionary)?["path"] as? String else { return nil } + let lPath = path.replacingOccurrences(of: "/drive/\(drive):", with: "/", options: .anchored, range: nil) + let fileObject = OneDriveFileObject(name: name, path: lPath) + if let webURL = json["webUrl"] as? String, let absolluteURL = URL(string: webURL) { + fileObject.absoluteURL = absolluteURL + } + fileObject.size = (json["size"] as? NSNumber)?.int64Value ?? -1 + fileObject.modifiedDate = resolve(dateString: json["lastModifiedDateTime"] as? String ?? "") + fileObject.creationDate = resolve(dateString: json["createdDateTime"] as? String ?? "") + fileObject.type = (json["folder"] as? String) != nil ? .directory : .regular + fileObject.id = json["id"] as? String + fileObject.entryTag = json["eTag"] as? String + return fileObject + } + + 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]() + + 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 { + keys.append("Dimensions") + dic["Dimensions"] = "\(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 + keys.append("Location") + let latStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: latitude)) + let longStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: longitude)) + dic["Location"] = "\(latStr), \(longStr)" + } + if let parent = json["image"] as? [String: Any] ?? json["video"] as? [String: Any], let duration = parent["duration"] as? UInt64 { + keys.append("Duration") + dic["Duration"] = OneDriveFileProvider.formatshort(interval: TimeInterval(duration) / 1000) + } + if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = self.resolve(dateString: timeTakenStr) { + keys.append("Date taken") + OneDriveFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + dic["Date taken"] = OneDriveFileProvider.dateFormatter.string(from: timeTaken) + } + + if let photo = json["photo"] as? [String: Any] { + if let devicemake = photo["cameraMake"] as? String { + keys.append("Device make") + dic["Device make"] = devicemake + } + if let devicemodel = photo["cameraModel"] as? String { + keys.append("Device model") + dic["Device model"] = devicemodel + } + if let focallen = photo["focalLength"] as? Double { + keys.append("Focal length") + dic["Focal length"] = focallen + } + if let fnum = photo["fNumber"] as? Double { + keys.append("F number") + dic["F number"] = fnum + } + if let expNom = photo["exposureNumerator"] as? Double, let expDen = photo["exposureDenominator"] as? Double { + keys.append("Exposure time") + dic["Exposure time"] = "\(Int(expNom))/\(Int(expDen))" + } + if let iso = photo["iso"] as? Int64 { + keys.append("ISO speed") + dic["ISO speed"] = iso + } + + } + + if let audio = json["audio"] as? [String: Any] { + for (key, value) in audio { + if key == "bitrate" || key == "isVariableBitrate" { continue } + let casedKey = spaceCamelCase(key) + keys.append(casedKey) + dic[casedKey] = value + } + } + + if let video = json["video"] as? [String: Any] { + if let bitRate = video["bitrate"] as? Int { + keys.append("Bitrate") + dic["Bitrate"] = bitRate + } + } + + return (dic, keys) + } + + func delegateNotify(_ operation: FileOperationType, error: Error?) { + DispatchQueue.main.async(execute: { + if error == nil { + self.delegate?.fileproviderSucceed(self, operation: operation) + } else { + self.delegate?.fileproviderFailed(self, operation: operation) + } + }) + } +}