diff --git a/FileProvider.podspec b/FileProvider.podspec index 5cc1cf0..2febaa3 100644 --- a/FileProvider.podspec +++ b/FileProvider.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = "FileProvider" - s.version = "0.3.4" + s.version = "0.4.0" s.summary = "NSFileManager replacement for Local and Remote (WebDAV/Dropbox/SMB2) files on iOS and MacOS." # This description is used to generate tags and improve search results. diff --git a/README.md b/README.md index 9b6017c..8f13ef0 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ Local and WebDAV providers are fully tested and can be used in production enviro - [x] **LocalFileProvider** a wrapper around `NSFileManager` with some additions like searching and reading a portion of file. - [x] **WebDAVFileProvider** WebDAV protocol is usual file transmission system on Macs. +- [x] **DropboxFileProvider** *implemented but not tested* - [ ] **SMBFileProvider** SMB/CIFS and SMB2/3 are file and printer sharing protocol which is originated from IBM & Microsoft and SMB2/3 is now replacing AFP protocol on MacOS. I implemented data types and some basic functions but *main interface is not implemented yet!* -- [ ] **DropboxFileProvider** *almost completed. upload, thumbnail and search functions not implemented yet* - [ ] **FTPFileProvider** - [ ] **AmazonS3FileProvider** @@ -155,26 +155,36 @@ There is a `FileObject` class which holds file attributes like size and creation For a single file: documentsProvider.attributesOfItemAtPath(path: "/file.txt", completionHandler: { - (attributes: LocalFileObject?, error: ErrorType?) -> Void} in + (attributes: LocalFileObject?, error: ErrorType?) -> Void in if let attributes = attributes { print("File Size: \(attributes.size)") print("Creation Date: \(attributes.createdDate)") print("Modification Date: \(modifiedDate)") print("Is Read Only: \(isReadOnly)") } - ) + }) To get list of files in a directory: documentsProvider.contentsOfDirectoryAtPath(path: "/", completionHandler: { - (contents: [LocalFileObject], error: ErrorType?) -> Void} in + (contents: [LocalFileObject], error: ErrorType?) -> Void in for file in contents { print("Name: \(attributes.name)") print("Size: \(attributes.size)") print("Creation Date: \(attributes.createdDate)") print("Modification Date: \(modifiedDate)") } - ) + }) + +To get size of strage and used/free space: + + func storageProperties(completionHandler: {(total: Int64, used: Int64) -> Void in + print("Total Storage Space: \(total)") + print("Used Space: \(used)") + print("Free Space: \(total - frees)") + }) + +* if this function is unavailable on provider or an error has been occurred, total space will be reported "-1" and used space "0" ### Change current directory diff --git a/Sources/DropboxFileProvider.swift b/Sources/DropboxFileProvider.swift index cc468ab..c9aa853 100644 --- a/Sources/DropboxFileProvider.swift +++ b/Sources/DropboxFileProvider.swift @@ -77,7 +77,7 @@ public class DropboxFileProvider: NSObject, FileProviderBasic { } public func attributesOfItemAtPath(path: String, completionHandler: ((attributes: FileObject?, error: ErrorType?) -> Void)) { - let url = NSURL(string: "https://api.dropboxapi.com/2/files/list_revisions")! + let url = NSURL(string: "https://api.dropboxapi.com/2/files/get_metadata")! let request = NSMutableURLRequest(URL: url) request.HTTPMethod = "POST" request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") @@ -91,12 +91,9 @@ public class DropboxFileProvider: NSObject, FileProviderBasic { } let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) let dbError: FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path) : nil - if let data = data, let jsonStr = String(data: data, encoding: NSUTF8StringEncoding) { - let json = self.jsonToDictionary(jsonStr) - if (json?["is_deleted"] as? NSNumber)?.boolValue ?? false, let entries = json?["entries"] as? [AnyObject] where entries.count > 0 , let entry = entries[0] as? [String: AnyObject], let file = self.mapToFileObject(entry) { - completionHandler(attributes: file, error: dbError) - return - } + if let data = data, let jsonStr = String(data: data, encoding: NSUTF8StringEncoding), let json = self.jsonToDictionary(jsonStr), let file = self.mapToFileObject(json) { + completionHandler(attributes: file, error: dbError) + return } completionHandler(attributes: nil, error: dbError) return @@ -106,6 +103,23 @@ public class DropboxFileProvider: NSObject, FileProviderBasic { task.resume() } + public func storageProperties(completionHandler: ((total: Int64, used: Int64) -> Void)) { + let url = NSURL(string: "https://api.dropboxapi.com/2/users/get_space_usage")! + let request = NSMutableURLRequest(URL: url) + request.HTTPMethod = "POST" + request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") + let task = session.dataTaskWithRequest(request) { (data, response, error) in + if let data = data, let jsonStr = String(data: data, encoding: NSUTF8StringEncoding), let json = self.jsonToDictionary(jsonStr) { + let totalSize = ((json["allocation"] as? NSDictionary)?["allocated"] as? NSNumber)?.longLongValue ?? -1 + let usedSize = (json["used"] as? NSNumber)?.longLongValue ?? 0 + completionHandler(total: totalSize, used: usedSize) + return + } + completionHandler(total: -1, used: 0) + } + task.resume() + } + public weak var fileOperationDelegate: FileOperationDelegate? } @@ -182,15 +196,12 @@ extension DropboxFileProvider: FileProviderOperations { } public func copyLocalFileToPath(localFile: NSURL, toPath: String, completionHandler: SimpleCompletionHandler) { - NotImplemented() - let request = NSMutableURLRequest(URL: absoluteURL(toPath)) - request.HTTPMethod = "PUT" - let task = session.uploadTaskWithRequest(request, fromFile: localFile) { (data, response, error) in + guard let data = NSData(contentsOfURL: localFile) else { + let error = throwError(localFile.uw_absoluteString, code: NSURLError.FileDoesNotExist) completionHandler?(error: error) - self.delegateNotify(.Move(source: localFile.uw_absoluteString, destination: toPath), error: error) + return } - task.taskDescription = self.dictionaryToJSON(["type": "Copy", "source": localFile.uw_absoluteString, "dest": toPath]) - task.resume() + upload_simple(toPath, data: data, overwrite: true, operation: .Copy(source: localFile.absoluteString, destination: toPath), completionHandler: completionHandler) } public func copyPathToLocalFile(path: String, toLocalURL destURL: NSURL, completionHandler: SimpleCompletionHandler) { @@ -215,6 +226,7 @@ extension DropboxFileProvider: FileProviderOperations { completionHandler?(error: e) } }) + task.taskDescription = self.dictionaryToJSON(["type": "Copy", "source": path, "dest": destURL.uw_absoluteString]) task.resume() } } @@ -256,28 +268,18 @@ extension DropboxFileProvider: FileProviderReadWrite { } public func writeContentsAtPath(path: String, contents data: NSData, atomically: Bool = false, completionHandler: SimpleCompletionHandler) { - NotImplemented() - let url = atomically ? absoluteURL(path).uw_URLByAppendingPathExtension("tmp") : absoluteURL(path) - let request = NSMutableURLRequest(URL: url) - request.HTTPMethod = "PUT" - let task = session.uploadTaskWithRequest(request, fromData: data) { (data, response, error) in - defer { - self.delegateNotify(.Modify(path: path), error: error) - } - if atomically { - self.moveItemAtPath((path as NSString).stringByAppendingPathExtension("tmp")!, toPath: path, completionHandler: completionHandler) - } - if let error = error { - // If there is no error, completionHandler has been executed by move command - completionHandler?(error: error) - } - } - task.taskDescription = self.dictionaryToJSON(["type": "Modify", "source": path]) - task.resume() + // FIXME: remove 150MB restriction + upload_simple(path, data: data, overwrite: true, operation: .Modify(path: path), completionHandler: completionHandler) } public func searchFilesAtPath(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: ((files: [FileObject], error: ErrorType?) -> Void)) { - NotImplemented() + var foundFiles = [DropboxFileObject]() + search(path, query: query, foundItem: { (file) in + foundFiles.append(file) + foundItemHandler?(file) + }, completionHandler: { (error) in + completionHandler(files: foundFiles, error: error) + }) } private func registerNotifcation(path: String, eventHandler: (() -> Void)) { @@ -292,6 +294,8 @@ extension DropboxFileProvider: FileProviderReadWrite { private func unregisterNotifcation(path: String) { NotImplemented() } + + // TODO: Implement /copy_reference, /get_preview & /get_thumbnail, /get_temporary_link, /save_url, /get_account & /get_current_account } private extension DropboxFileProvider { @@ -316,7 +320,6 @@ private extension DropboxFileProvider { if let code = (response as? NSHTTPURLResponse)?.statusCode where code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { responseError = FileProviderDropboxError(code: rCode, path: path) } - if let data = data, let jsonStr = String(data: data, encoding: NSUTF8StringEncoding) { let json = self.jsonToDictionary(jsonStr) if let entries = json?["entries"] as? [AnyObject] where entries.count > 0 { @@ -340,6 +343,90 @@ private extension DropboxFileProvider { } task.resume() } + + private func upload_simple(targetPath: String, data: NSData, modifiedDate: NSDate = NSDate(), overwrite: Bool, operation: FileOperation, completionHandler: SimpleCompletionHandler) { + assert(data.length < 150*1024*1024, "Maximum size of allowed size to upload is 150MB") + var requestDictionary = [String: AnyObject]() + let url: NSURL + url = NSURL(string: "https://content.dropboxapi.com/2/files/upload")! + requestDictionary["path"] = correctPath(targetPath) + requestDictionary["mode"] = overwrite ? "overwrite" : "add" + let dateFormatter = NSDateFormatter() + dateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssz" + requestDictionary["client_modified"] = dateFormatter.stringFromDate(modifiedDate) + let request = NSMutableURLRequest(URL: url) + request.HTTPMethod = "POST" + request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") + request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type") + request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg") + request.HTTPBody = data + let task = session.uploadTaskWithRequest(request, fromData: data) { (data, response, error) in + var responseError: FileProviderDropboxError? + if let code = (response as? NSHTTPURLResponse)?.statusCode where code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { + responseError = FileProviderDropboxError(code: rCode, path: targetPath) + } + defer { + self.delegateNotify(.Create(path: targetPath), error: responseError ?? error) + } + completionHandler?(error: responseError ?? error) + } + var dic: [String: AnyObject] = ["type": operation.description] + switch operation { + case .Create(path: let s): + dic["source"] = s + case .Copy(source: let s, destination: let d): + dic["source"] = s + dic["dest"] = d + case .Modify(path: let s): + dic["source"] = s + case .Move(source: let s, destination: let d): + dic["source"] = s + dic["dest"] = d + default: + break + } + task.taskDescription = self.dictionaryToJSON(dic) + task.resume() + } + + func search(startPath: String = "", query: String, start: Int = 0, maxResultPerPage: Int = 25, foundItem:((file: DropboxFileObject) -> Void), completionHandler: ((error: ErrorType?) -> Void)) { + let url = NSURL(string: "https://api.dropboxapi.com/2/files/search")! + let request = NSMutableURLRequest(URL: url) + request.HTTPMethod = "POST" + request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + var requestDictionary: [String: AnyObject] = ["path": startPath] + requestDictionary["query"] = query + requestDictionary["start"] = start + requestDictionary["max_results"] = maxResultPerPage + request.HTTPBody = dictionaryToJSON(requestDictionary)?.dataUsingEncoding(NSUTF8StringEncoding) + let task = session.dataTaskWithRequest(request) { (data, response, error) in + var responseError: FileProviderDropboxError? + if let code = (response as? NSHTTPURLResponse)?.statusCode where code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { + responseError = FileProviderDropboxError(code: rCode, path: startPath) + } + if let data = data, let jsonStr = String(data: data, encoding: NSUTF8StringEncoding) { + let json = self.jsonToDictionary(jsonStr) + if let entries = json?["matches"] as? [AnyObject] where entries.count > 0 { + for entry in entries { + if let entry = entry as? [String: AnyObject], let file = self.mapToFileObject(entry) { + foundItem(file: file) + } + } + let rstart = json?["start"] as? Int + let hasmore = (json?["more"] as? NSNumber)?.boolValue ?? false + if hasmore, let rstart = rstart { + self.search(startPath, query: query, start: rstart + entries.count, maxResultPerPage: maxResultPerPage, foundItem: foundItem, completionHandler: completionHandler) + } else { + completionHandler(error: responseError ?? error) + } + return + } + } + completionHandler(error: responseError ?? error) + } + task.resume() + } } internal extension DropboxFileProvider { diff --git a/Sources/FileProvider.swift b/Sources/FileProvider.swift index 3b76e2f..aa23614 100644 --- a/Sources/FileProvider.swift +++ b/Sources/FileProvider.swift @@ -122,6 +122,8 @@ public protocol FileProviderBasic: class { */ func contentsOfDirectoryAtPath(path: String, completionHandler: ((contents: [FileObject], error: ErrorType?) -> Void)) func attributesOfItemAtPath(path: String, completionHandler: ((attributes: FileObject?, error: ErrorType?) -> Void)) + + func storageProperties(completionHandler: ((total: Int64, used: Int64) -> Void)) } public protocol FileProviderOperations: FileProviderBasic { diff --git a/Sources/LocalFileProvider.swift b/Sources/LocalFileProvider.swift index 371b071..3d78a36 100644 --- a/Sources/LocalFileProvider.swift +++ b/Sources/LocalFileProvider.swift @@ -85,6 +85,13 @@ public class LocalFileProvider: FileProvider, FileProviderMonitor { return fileAttr } + public func storageProperties(completionHandler: ((total: Int64, used: Int64) -> Void)) { + let dict = (try? NSFileManager.defaultManager().attributesOfFileSystemForPath(baseURL?.path ?? "/")) as NSDictionary?; + let totalSize = dict?.objectForKey(NSFileSystemSize)?.longLongValue ?? -1; + let freeSize = dict?.objectForKey(NSFileSystemFreeSize)?.longLongValue ?? 0; + completionHandler(total: totalSize, used: totalSize - freeSize) + } + public func attributesOfItemAtPath(path: String, completionHandler: ((attributes: FileObject?, error: ErrorType?) -> Void)) { dispatch_async(dispatch_queue) { completionHandler(attributes: self.attributesOfItemAtURL(self.absoluteURL(path)), error: nil) diff --git a/Sources/SMBFileProvider.swift b/Sources/SMBFileProvider.swift index d5c9e70..6077724 100644 --- a/Sources/SMBFileProvider.swift +++ b/Sources/SMBFileProvider.swift @@ -40,6 +40,10 @@ public class SMBFileProvider: FileProvider, FileProviderMonitor { NotImplemented() } + public func storageProperties(completionHandler: ((total: Int64, used: Int64) -> Void)) { + NotImplemented() + } + public weak var fileOperationDelegate: FileOperationDelegate? public func createFolder(folderName: String, atPath: String, completionHandler: SimpleCompletionHandler) { diff --git a/Sources/WebDAVFileProvider.swift b/Sources/WebDAVFileProvider.swift index 873e3bc..561a695 100644 --- a/Sources/WebDAVFileProvider.swift +++ b/Sources/WebDAVFileProvider.swift @@ -114,6 +114,34 @@ public class WebDAVFileProvider: NSObject, FileProviderBasic { task.resume() } + public func storageProperties(completionHandler: ((total: Int64, used: Int64) -> Void)) { + // Not all WebDAV clients implements RFC2518 which allows geting storage quota. + // In this case you won't get error. totalSize is NSURLSessionTransferSizeUnknown + // and used space is zero. + guard let baseURL = baseURL else { + return + } + let request = NSMutableURLRequest(URL: baseURL) + request.HTTPMethod = "PROPFIND" + request.setValue("0", forHTTPHeaderField: "Depth") + request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type") + request.HTTPBody = "\n\n\n".dataUsingEncoding(NSUTF8StringEncoding) + request.setValue(String(request.HTTPBody!.length), forHTTPHeaderField: "Content-Length") + let task = session.dataTaskWithRequest(request) { (data, response, error) in + if let data = data { + let xresponse = self.parseXMLResponse(data) + if let attr = xresponse.first { + let totalSize = Int64(attr.prop["quota-available-bytes"] ?? "") + let usedSize = Int64(attr.prop["quota-used-bytes"] ?? "") + completionHandler(total: totalSize ?? -1, used: usedSize ?? 0) + return + } + } + completionHandler(total: -1, used: 0) + } + task.resume() + } + public weak var fileOperationDelegate: FileOperationDelegate? }