Compare commits

..

8 Commits

Author SHA1 Message Date
Amir Abbas Mousavian 5a7dfa1039 Merge commit 'b84e6ea7abbec6f9a83144d2dcd6744b568d682f'
* commit 'b84e6ea7abbec6f9a83144d2dcd6744b568d682f':
  Updated Date
2017-02-02 13:28:08 +03:30
Amir Abbas Mousavian be7a370a42 Added Documentation, get contents handling in Local provider optimized
- Changed default thumbnail dimension to 64x64 in `LocalFileProvider`
2017-02-02 13:25:40 +03:30
Amir Abbas Mousavian b84e6ea7ab Updated Date 2017-02-02 01:03:48 +03:30
Amir Abbas Mousavian 82b1d2c450 Added new badges 2017-02-01 22:18:20 +03:30
Amir Abbas Mousavian c38ee1ccd3 standardized FileObject.path property
- FileObject.path now has heading slash in all scenarios
- fixes #27, WebDAV fileObject.path was not relative
2017-02-01 21:23:11 +03:30
Amir Abbas Mousavian 1a44df3fd7 Refactoring, better webdav response handling
- Added Dropbox copy from reference method
- refactored `mapToFileObject` methods into `FileObject` initializers
- fixed `requestDictionary` type to `[String: AnyObject]`
2017-02-01 14:08:28 +03:30
Amir Abbas Mousavian eff3725680 Added release version badge 2017-02-01 00:21:48 +03:30
Amir Abbas Mousavian 9368f636d3 Added carthage badge 2017-01-31 23:53:06 +03:30
15 changed files with 719 additions and 246 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
#
s.name = "FileProvider"
s.version = "0.11.2"
s.version = "0.12.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.
+2 -2
View File
@@ -603,7 +603,7 @@
799396601D48B7BF00086753 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.11.2;
BUNDLE_VERSION_STRING = 0.12.0;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -633,7 +633,7 @@
799396611D48B7BF00086753 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.11.2;
BUNDLE_VERSION_STRING = 0.12.0;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2016 Amir Abbas Mousavian
Copyright (c) 2016-17 Amir Abbas Mousavian
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+25 -12
View File
@@ -3,17 +3,21 @@
>This Swift library provide a swifty way to deal with local and remote files and directories in a unified way.
[![Swift Version][swift-image]][swift-url]
[![Platform][platform-image]](#)
[![License][license-image]][license-url]
[![Platform](https://img.shields.io/badge/Platform-iOS%2C%20macOS%2C%20tvOS-lightgray.svg)]()
[![Release versin][release-image]][release-url]
[![CocoaPods version](https://img.shields.io/cocoapods/v/FileProvider.svg)][cocoapods]
[![Carthage compatible][carthage-image]](https://github.com/Carthage/Carthage)
[![Build Status][travis-image]][travis-url]
[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/FileProvider.svg)](https://cocoapods.org/pods/FileProvider)
[![codebeat badge][codebeat-image]][codebeat-url]
[![Codebeat Badge][codebeat-image]][codebeat-url]
[![Cocoapods Docs][docs-image]](docs-url)
[![Cocoapods Downloads][cocoapods-downloads]][cocoapods]
[![Cocoapods Apps][cocoapods-apps]][cocoapods]
<!---
[![codecov](https://codecov.io/gh/amosavian/FileProvider/branch/master/graph/badge.svg)](https://codecov.io/gh/amosavian/FileProvider)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![codecov](https://codecov.io/gh/amosavian/FileProvider/branch/master/graph/badge.svg)](https://codecov.io/gh/amosavian/FileProvider)
--->
This library provides implementaion of WebDav, Dropbox, OneDrive and SMB2 (incomplete) and local files.
@@ -27,13 +31,13 @@ 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.
* 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 (business) servers.
* For now it has limitation in uploading files up to 100MB.
- [x] **CloudFileProvider** A wrapper around app's ubiquitous container to iCloud Drive in iOS 8+ API.
- [ ] **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.
* Data types and some basic functions are implemented but *main interface is not implemented yet!*
* SMB1/CIFS is depericated and very tricky to be implemented
* Data types and some basic functions are implemented 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**
- [ ] **GoogleDriveFileProvider**
@@ -404,11 +408,20 @@ Distributed under the MIT license. See `LICENSE` for more information.
[https://github.com/amosavian/](https://github.com/amosavian/)
[swift-image]:https://img.shields.io/badge/swift-3.0-orange.svg
[cocoapods]: https://cocoapods.org/pods/FileProvider
[swift-image]: https://img.shields.io/badge/swift-3.0-orange.svg
[swift-url]: https://swift.org/
[license-image]: https://img.shields.io/badge/License-MIT-blue.svg
[platform-image]: https://img.shields.io/cocoapods/p/FileProvider.svg
[license-image]: https://img.shields.io/github/license/amosavian/FileProvider.svg
[license-url]: LICENSE
[codebeat-image]: https://codebeat.co/badges/7b359f48-78eb-4647-ab22-56262a827517
[codebeat-url]: https://codebeat.co/projects/github-com-amosavian-fileprovider
[travis-image]: https://img.shields.io/travis/amosavian/FileProvider/master.svg
[travis-url]: https://travis-ci.org/amosavian/FileProvider
[travis-url]: https://travis-ci.org/amosavian/FileProvider
[release-url]: https://github.com/amosavian/FileProvider/releases
[release-image]: https://img.shields.io/github/release/amosavian/FileProvider.svg
[carthage-image]: https://img.shields.io/badge/Carthage-compatible-4BC51D.svg
[cocoapods-downloads]: https://img.shields.io/cocoapods/dt/FileProvider.svg
[cocoapods-apps]: https://img.shields.io/cocoapods/at/FileProvider.svg
[docs-image]: https://img.shields.io/cocoapods/metrics/doc-percent/FileProvider.svg
[docs-url]: http://cocoadocs.org/docsets/FileProvider/
+4 -1
View File
@@ -366,7 +366,9 @@ open class CloudFileProvider: LocalFileProvider {
return file
}
/// Removes local copy of file, but spares cloud copy
/// Removes local copy of file, but spares cloud copy/
/// - Parameter path: Path of file or directory to be remoed from local
/// - Parameter completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
open func evictItem(path: String, completionHandler: SimpleCompletionHandler) {
operation_queue.addOperation {
do {
@@ -378,6 +380,7 @@ open class CloudFileProvider: LocalFileProvider {
}
}
/// Returns a pulic url with expiration date, can be shared with other people.
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
operation_queue.addOperation {
do {
+35 -12
View File
@@ -82,7 +82,7 @@ open class DropboxFileProvider: FileProviderBasicRemote {
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestDictionary = ["path": correctPath(path)! as NSString]
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderDropboxError?
@@ -90,7 +90,7 @@ open class DropboxFileProvider: FileProviderBasicRemote {
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
dbError = code != nil ? FileProviderDropboxError(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) {
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let file = DropboxFileObject(json: json) {
fileObject = file
}
}
@@ -205,8 +205,8 @@ extension DropboxFileProvider: FileProviderOperations {
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
let requestDictionary = ["path": path]
let requestJson = dictionaryToJSON(requestDictionary as [String : AnyObject]) ?? ""
let requestDictionary: [String: AnyObject] = ["path": path as NSString]
let requestJson = dictionaryToJSON(requestDictionary) ?? ""
request.setValue(requestJson, forHTTPHeaderField: "Dropbox-API-Arg")
let task = session.downloadTask(with: request, completionHandler: { (cacheURL, response, error) in
guard let cacheURL = cacheURL, let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
@@ -231,6 +231,10 @@ extension DropboxFileProvider: FileProviderOperations {
extension DropboxFileProvider: FileProviderReadWrite {
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
if length == 0 {
return nil
}
let opType = FileOperationType.fetch(path: path)
let url = URL(string: "files/download", relativeTo: contentURL)!
var request = URLRequest(url: url)
@@ -241,8 +245,8 @@ extension DropboxFileProvider: FileProviderReadWrite {
} else if offset > 0 && length < 0 {
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
}
let requestDictionary = ["path": correctPath(path)! as NSString]
request.setValue(dictionaryToJSON(requestDictionary as [String : AnyObject]), forHTTPHeaderField: "Dropbox-API-Arg")
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderDropboxError?
if let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: httpResponse.statusCode) {
@@ -305,7 +309,7 @@ extension DropboxFileProvider {
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestDictionary = ["path": correctPath(path)! as NSString]
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderDropboxError?
@@ -319,7 +323,7 @@ extension DropboxFileProvider {
link = URL(string: linkStr)
}
if let attribDic = json["metadata"] as? [String: AnyObject] {
fileObject = self.mapToFileObject(attribDic)
fileObject = DropboxFileObject(json: attribDic)
}
}
}
@@ -336,7 +340,7 @@ extension DropboxFileProvider {
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestDictionary = ["path": correctPath(toPath)! as NSString, "url" : remoteURL.absoluteString as NSString]
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "url" : remoteURL.absoluteString as NSString]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderDropboxError?
@@ -348,7 +352,7 @@ extension DropboxFileProvider {
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
jobId = json["async_job_id"] as? String
if let attribDic = json["metadata"] as? [String: AnyObject] {
fileObject = self.mapToFileObject(attribDic)
fileObject = DropboxFileObject(json: attribDic)
}
}
}
@@ -356,6 +360,25 @@ extension DropboxFileProvider {
})
task.resume()
}
open func copyItem(reference: String, to toPath: String, completionHandler:SimpleCompletionHandler) {
let url = URL(string: "files/save_url", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "copy_reference" : reference as NSString]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderDropboxError?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
dbError = code != nil ? FileProviderDropboxError(code: code!, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
}
completionHandler?(dbError ?? error)
})
task.resume()
}
}
extension DropboxFileProvider: ExtendedFileProvider {
@@ -405,7 +428,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
}
var request = URLRequest(url: url)
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
var requestDictionary = ["path": path as NSString]
var requestDictionary: [String: AnyObject] = ["path": path as NSString]
requestDictionary["format"] = "jpeg" as NSString
if let dimension = dimension {
requestDictionary["size"] = "w\(Int(dimension.width))h\(Int(dimension.height))" as NSString
@@ -438,7 +461,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestDictionary = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)]
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderDropboxError?
+27 -28
View File
@@ -23,6 +23,24 @@ public final class DropboxFileObject: FileObject {
super.init(url: URL(string: path) ?? URL(string: "/")!, name: name, path: path)
}
internal convenience init? (jsonStr: String) {
guard let json = jsonToDictionary(jsonStr) else { return nil }
self.init(json: json)
}
internal convenience init? (json: [String: AnyObject]) {
guard let name = json["name"] as? String else { return nil }
guard let path = json["path_display"] as? String else { return nil }
self.init(name: name, path: path)
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
self.serverTime = resolve(dateString: json["server_modified"] as? String ?? "")
self.modifiedDate = resolve(dateString: json["client_modified"] as? String ?? "")
self.type = (json[".tag"] as? String) == "folder" ? .directory : .regular
self.isReadOnly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false
self.id = json["id"] as? String
self.rev = json["rev"] as? String
}
open internal(set) var serverTime: Date? {
get {
return allValues["NSURLServerDateKey"] as? Date
@@ -79,7 +97,7 @@ internal extension DropboxFileProvider {
let json = jsonToDictionary(jsonStr)
if let entries = json?["entries"] as? [AnyObject] , entries.count > 0 {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = self.mapToFileObject(entry) {
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
files.append(file)
}
}
@@ -104,17 +122,17 @@ internal extension DropboxFileProvider {
self.delegateNotify(.create(path: targetPath), error: error)
return nil
}
var requestDictionary = [String: Any]()
var requestDictionary = [String: AnyObject]()
let url: URL
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)
requestDictionary["client_modified"] = rfc3339utc(of: modifiedDate) as NSString
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.setValue(dictionaryToJSON(requestDictionary as [String : AnyObject]), forHTTPHeaderField: "Dropbox-API-Arg")
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
request.httpBody = data
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
var responseError: FileProviderDropboxError?
@@ -137,17 +155,17 @@ internal extension DropboxFileProvider {
self.delegateNotify(.create(path: targetPath), error: error)
return nil
}
var requestDictionary = [String: Any]()
var requestDictionary = [String: AnyObject]()
let url: URL
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)
requestDictionary["client_modified"] = rfc3339utc(of: modifiedDate) as NSString
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.setValue(dictionaryToJSON(requestDictionary as [String : AnyObject]), forHTTPHeaderField: "Dropbox-API-Arg")
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
var responseError: FileProviderDropboxError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
@@ -181,7 +199,7 @@ internal extension DropboxFileProvider {
let json = jsonToDictionary(jsonStr)
if let entries = json?["matches"] as? [AnyObject] , entries.count > 0 {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = self.mapToFileObject(entry) {
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
foundItem(file)
}
}
@@ -203,25 +221,6 @@ internal extension DropboxFileProvider {
// codebeat:enable[ARITY]
internal extension DropboxFileProvider {
func mapToFileObject(_ jsonStr: String) -> DropboxFileObject? {
guard let json = jsonToDictionary(jsonStr) else { return nil }
return self.mapToFileObject(json)
}
func mapToFileObject(_ json: [String: AnyObject]) -> DropboxFileObject? {
guard let name = json["name"] as? String else { return nil }
guard let path = json["path_display"] as? String else { return nil }
let fileObject = DropboxFileObject(name: name, path: path)
fileObject.size = (json["size"] as? NSNumber)?.int64Value ?? -1
fileObject.serverTime = resolve(dateString: json["server_modified"] as? String ?? "")
fileObject.modifiedDate = resolve(dateString: json["client_modified"] as? String ?? "")
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["rev"] as? String
return fileObject
}
static let dateFormatter = DateFormatter()
static let decimalFormatter = NumberFormatter()
@@ -241,7 +240,7 @@ internal extension DropboxFileProvider {
let longStr = DropboxFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))
dic["Location"] = "\(latStr), \(longStr)"
}
if let timeTakenStr = json["time_taken"] as? String, let timeTaken = self.resolve(dateString: timeTakenStr) {
if let timeTakenStr = json["time_taken"] as? String, let timeTaken = resolve(dateString: timeTakenStr) {
keys.append("Date taken")
DropboxFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dic["Date taken"] = DropboxFileProvider.dateFormatter.string(from: timeTaken)
+2 -1
View File
@@ -61,6 +61,7 @@ extension LocalFileProvider: ExtendedFileProvider {
}
public func thumbnailOfFile(path: String, dimension: CGSize? = nil, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
let dimension = dimension ?? CGSize(width: 64, height: 64)
(dispatch_queue).async {
var thumbnailImage: ImageClass? = nil
// Check cache
@@ -85,7 +86,7 @@ extension LocalFileProvider: ExtendedFileProvider {
}
if let image = thumbnailImage {
let scaledImage = dimension != nil ? LocalFileProvider.scaleDown(image: image, toSize: dimension!) : image
let scaledImage = LocalFileProvider.scaleDown(image: image, toSize: dimension)
completionHandler(scaledImage, nil)
}
}
+41 -1
View File
@@ -10,6 +10,7 @@ import Foundation
/// Containts path and attributes of a file or resource.
open class FileObject {
/// A `Dictionary` contains file information, using `URLResourceKey` keys.
open internal(set) var allValues: [String: Any]
internal init(allValues: [String: Any]) {
@@ -140,6 +141,37 @@ open class FileObject {
}
}
internal func resolve(dateString: String) -> Date? {
let dateFor: DateFormatter = DateFormatter()
dateFor.locale = Locale(identifier: "en_US")
dateFor.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"
if let rfc3339 = dateFor.date(from: dateString) {
return rfc3339
}
dateFor.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss z"
if let rfc1123 = dateFor.date(from: dateString) {
return rfc1123
}
dateFor.dateFormat = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
if let rfc850 = dateFor.date(from: dateString) {
return rfc850
}
dateFor.dateFormat = "EEE MMM d HH':'mm':'ss yyyy"
if let asctime = dateFor.date(from: dateString) {
return asctime
}
return nil
}
internal func rfc3339utc(of date:Date) -> String {
let fm = DateFormatter()
fm.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
fm.timeZone = TimeZone(identifier:"UTC")
fm.locale = Locale(identifier:"en_US_POSIX")
return fm.string(from:date)
}
/// Sorting FileObject array by given criteria, **not thread-safe**
public struct FileObjectSorting {
@@ -184,13 +216,19 @@ public struct FileObjectSorting {
public static let createdAscending = FileObjectSorting(type: .creationDate, ascending: true)
public static let createdDesceding = FileObjectSorting(type: .creationDate, ascending: false)
/// Initializes a `FileObjectSorting` allows to sort an `Array` of `FileObject`.
///
/// - Parameters:
/// - type: Determines to sort based on which file property.
/// - ascending: `true` of resulting `Array` is ascending
/// - isDirectoriesFirst: Puts directoris on the top of resulting `Array`.
public init (type: SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) {
self.sortType = type
self.ascending = ascending
self.isDirectoriesFirst = isDirectoriesFirst
}
/// Sorts array of FileObjects by criterias set in properties
/// Sorts array of `FileObject`s by criterias set in properties
public func sort(_ files: [FileObject]) -> [FileObject] {
return files.sorted {
if isDirectoriesFirst {
@@ -228,11 +266,13 @@ public struct FileObjectSorting {
}
extension Array where Element: FileObject {
/// Returns a sorted array of `FileObject`s by criterias set in properties.
public func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) -> [Element] {
let sorting = FileObjectSorting(type: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
return sorting.sort(self) as! [Element]
}
/// Sorts array of `FileObject`s by criterias set in properties
public mutating func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) {
self = self.sorted(by: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
}
+387 -31
View File
@@ -18,25 +18,89 @@ public typealias ImageClass = NSImage
public typealias SimpleCompletionHandler = ((_ error: Error?) -> Void)?
public protocol FileProviderBasic: class {
/// An string to identify type of provider.
static var type: String { get }
/// An string to identify type of provider.
var type: String { get }
/// The paths in arguments should resolved against base url.
var isPathRelative: Bool { get }
/// The url of which paths should resolve against.
var baseURL: URL? { get }
/// Current active path used in `contentsOfDirectory(path:completionHandler:)` method.
var currentPath: String { get set }
/**
Dispatch queue usually used in query methods.
Set it to a new object to switch between cuncurrent and serial queues.
- **Default:** Cuncurrent `DispatchQueue` object.
*/
var dispatch_queue: DispatchQueue { get set }
/// Operation queue ususlly used in file operation methods.
/// use `maximumOperationTasks` property of provider to manage operation queue.
var operation_queue: OperationQueue { get set }
/// Delegate to update UI after finishing file operations.
var delegate: FileProviderDelegate? { get set }
/**
login credential for provider. Should be set in `init` method.
**Example initialization:**
````
let credential = URLCredential(user: "user", password: "password", persistence: .forSeession)
````
- Note: In OAuth based providers like `DropboxFileProvider` and `OneDriveFileProvider`, password is Token.
use [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) library to fetch clientId and Token of user.
*/
var credential: URLCredential? { get }
/**
Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler.
If the directory contains no entries or an error is occured, this method will return the empty array.
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
- Parameter completionHandler: a block with result of directory entries or error.
`contents`: An array of `FileObject` identifying the the directory entries.
`error`: Error returned by system.
*/
func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void))
/**
Returns a `FileObject` containing the attributes of the item (file, directory, symlink, etc.) at the path in question via asynchronous completion handler.
If the directory contains no entries or an error is occured, this method will return the empty `FileObject`.
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
- Parameter completionHandler: a block with result of directory entries or error.
`attributes`: A `FileObject` containing the attributes of the item.
`error`: Error returned by system.
*/
func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void))
/// Returns total and used space in provider container asynchronously.
func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void))
/**
Returns an independent url to access the file. Some providers like `Dropbox` due to their nature.
don't return an absolute url to be used to access file directly.
- Parameter path: Relative path of file or directory.
- Returns: An url, can be used to access to file directly.
*/
func url(of path: String?) -> URL
}
public extension FileProviderBasic {
/// The maximum number of queued operations that can execute at the same time.
///
/// The default value of this property is `OperationQueue.defaultMaxConcurrentOperationCount`.
public var maximumOperationTasks: Int {
get {
return operation_queue.maxConcurrentOperationCount
@@ -48,9 +112,19 @@ public extension FileProviderBasic {
}
public protocol FileProviderBasicRemote: FileProviderBasic {
///
var session: URLSession { get }
/// A `URLCache` to cache downloaded files and contents.
///
/// If set to nil, `URLCache.shared` object will be used.
/// - Note: It has no effect unless setting `useCache` property to `true`.
var cache: URLCache? { get }
/// Determine to use `cache` property to cache downloaded file objects. Doesn't have effect on query type methods.
var useCache: Bool { get set }
/// Validating cached data using E-Tag or Revision identifier if possible.
var validatingCache: Bool { get set }
}
@@ -105,24 +179,140 @@ internal extension FileProviderBasicRemote {
public protocol FileProviderOperations: FileProviderBasic {
var fileOperationDelegate : FileOperationDelegate? { get set }
/**
Creates a new directory at the specified path asynchronously.
This will create any necessary intermediate directories.
- Parameters:
- folder: Directory name.
- at: Parent path of new directory.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
*/
@discardableResult
func create(folder: String, at: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Creates an new file with data passed to method asynchronously.
Returns error via completionHandler if file is already exists.
- Parameters:
- file: New file name with extension separated by period.
- at: Parent path of new file.
- data: Data of new files. Pass nil or `Data()` to create empty file.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Moves a file or directory from `path` to designated path asynchronously.
When you want move a file, destination path should also consists of file name.
Either a new name or the old one. If file is already exist, an error will be returned via completionHandler.
- Parameters:
- path: original file or directory path.
- to: destination path of file or directory, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Moves a file or directory from `path` to designated path asynchronously.
When you want move a file, destination path should also consists of file name.
Either a new name or the old one.
- Parameters:
- path: original file or directory path.
- to: destination path of file or directory, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func moveItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Copies a file or directory from `path` to designated path asynchronously.
When want copy a file, destination path should also consists of file name.
Either a new name or the old one. If file is already exist, an error will be returned via completionHandler.
- Parameters:
- path: original file or directory path.
- to: destination path of file or directory, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Copies a file or directory from `path` to designated path asynchronously.
When want copy a file, destination path should also consists of file name.
Either a new name or the old one.
- Parameters:
- path: original file or directory path.
- to: destination path of file or directory, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Removes the file or directory at the specified path.
- Parameters:
- path: file or directory path.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Uploads a file from local file url to designated path asynchronously.
Method will fail if source is not a local url with `file://` scheme.
- Parameters:
- localFile: a file url to file.
- to: destination path of file, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress.
*/
@discardableResult
func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Uploads a file from local file url to designated path asynchronously.
Method will fail if source is not a local url with `file://` scheme.
- Parameters:
- localFile: a file url to file.
- to: destination path of file, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress.
*/
@discardableResult
func copyItem(localFile: URL, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Download a file from `path` to designated local file url asynchronously.
Method will fail if destination is not a local url with `file://` scheme.
- Parameters:
- path: original file or directory path.
- toLocalURL: destination local url of file, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle?
}
@@ -145,20 +335,104 @@ extension FileProviderOperations {
}
public protocol FileProviderReadWrite: FileProviderBasic {
/**
Retreives a `Data` object with the contents of the file asynchronously vis contents argument of completion handler.
If path specifies a directory, or if some other error occurs, data will be nil.
- Parameters:
- path: Path of file.
- completionHandler: a block with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle?
/**
Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler.
If path specifies a directory, or if some other error occurs, data will be nil.
- Parameters:
- path: Path of file.
- offset: First byte index which should be read. **Starts from 0.**
- length: Bytes count of data. Pass `-1` to read until the end of file.
- completionHandler: a block with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle?
/**
Write the contents of the `Data` to a location asynchronously.
It will return error if file is already exists.
Not attomically by default, unless the provider enforces it.
- Parameters:
- path: Path of target file.
- contents: Data to be written into file.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Write the contents of the `Data` to a location asynchronously.
It will return error if file is already exists.
- Parameters:
- path: Path of target file.
- contents: Data to be written into file.
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Write the contents of the `Data` to a location asynchronously.
Not attomically by default, unless the provider enforces it.
- Parameters:
- path: Path of target file.
- contents: Data to be written into file.
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Write the contents of the `Data` to a location asynchronously.
- Parameters:
- path: Path of target file.
- contents: Data to be written into file.
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Search files inside directory using query asynchronously.
- Note: For now only it's limited to file names. `query` parameter may take `NSPredicate` format in near future.
- Parameters:
- path: location of directory to start search
- recursive: Searching subdirectories of path
- query: Simple string of file name to be search (for now).
- foundItemHandler: Block which is called when a file is found
- completionHandler: Block which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
*/
func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void))
}
@@ -185,8 +459,33 @@ extension FileProviderReadWrite {
}
public protocol FileProviderMonitor: FileProviderBasic {
/**
Starts monitoring a path and its subpaths, including files and folders, for any change,
including copy, move/rename, content changes, etc.
To avoid thread congestion, `evetHandler` will be triggered with 0.2 seconds interval,
and has a 0.25 second delay, to ensure it's called after updates.
- Note: this functionality is available only in `LocalFileProvider` and `CloudFileProvider`.
- Note: `eventHandler` is not called on main thread, for updating UI. dispatch routine to main thread.
- Important: `eventHandler` may be called if file is changed in recursive subpaths of registered path.
This may cause negative impact on performance if a root path is being monitored.
- Parameters:
- path: path of directory.
- eventHandler: Block executed after change, on a secondary thread.
*/
func registerNotifcation(path: String, eventHandler: @escaping (() -> Void))
/// Stops monitoring the path.
///
/// - Parameter path: path of directory.
func unregisterNotifcation(path: String)
/// Investigate either the path is registered for change notification or not.
///
/// - Parameter path: path of directory.
/// - Returns: Directory is being monitored or not.
func isRegisteredForNotification(path: String) -> Bool
}
@@ -199,6 +498,7 @@ extension FileProviderBasic {
return Self.type
}
/// path without heading and trailing slash
public var bareCurrentPath: String {
return currentPath.trimmingCharacters(in: pathTrimSet)
}
@@ -248,6 +548,8 @@ extension FileProviderBasic {
return p
}
/// Returns a file name supposed to be unique with adding numbers to end of file.
/// - Important: It's a synchronous method. Don't use it on matin thread.
public func fileByUniqueName(_ filePath: String) -> String {
let fileUrl = URL(fileURLWithPath: filePath)
let dirPath = fileUrl.deletingLastPathComponent().path
@@ -299,44 +601,64 @@ extension FileProviderBasic {
internal func NotImplemented(_ fn: String = #function, file: StaticString = #file) {
assert(false, "\(fn) method is not yet implemented. \(file)")
}
internal func resolve(dateString: String) -> Date? {
let dateFor: DateFormatter = DateFormatter()
dateFor.locale = Locale(identifier: "en_US")
dateFor.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss zzz"
if let rfc1123 = dateFor.date(from: dateString) {
return rfc1123
}
dateFor.dateFormat = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
if let rfc850 = dateFor.date(from: dateString) {
return rfc850
}
dateFor.dateFormat = "EEE MMM d HH':'mm':'ss yyyy"
if let asctime = dateFor.date(from: dateString) {
return asctime
}
dateFor.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssz"
if let isotime = dateFor.date(from: dateString) {
return isotime
}
return nil
}
public func string(from date:Date) -> String {
let fm = DateFormatter()
fm.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
fm.timeZone = TimeZone(identifier:"UTC")
fm.locale = Locale(identifier:"en_US_POSIX")
return fm.string(from:date)
}
}
public protocol ExtendedFileProvider: FileProviderBasic {
/// Returuns true if thumbnail preview is supported by provider and file type accordingly.
///
/// - Parameter path: path of file.
/// - Returns: A `Bool` idicates path can have thumbnail.
func thumbnailOfFileSupported(path: String) -> Bool
/// Returns true if provider supports fetching properties of file like dimensions, duration, etc.
/// Usually media or document files support these meta-infotmations.
///
/// - Parameter path: path of file.
/// - Returns: A `Bool` idicates path can have properties.
func propertiesOfFileSupported(path: String) -> Bool
/**
Generates ans returns a thumbnail preview of document asynchronously. The defualt dimension of returned image is different
regarding provider type, usually 64x64 pixels.
- Parameters:
- path: path of file.
- completionHandler: a block with result of preview image or error.
`image`: `NSImage`/`UIImage` object contains preview.
`error`: Error returned by system.
*/
func thumbnailOfFile(path: String, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void))
/**
Generates ans returns a thumbnail preview of document asynchronously. The defualt dimension of returned image is different
regarding provider type, usually 64x64 pixels. Default value used when `dimenstion` is `nil`.
- Note: `LocalFileInformationGenerator` variables can be set to change default behavior of
thumbnail and properties generator of `LocalFileProvider`.
- Parameters:
- path: path of file.
- dimension: width and height of result preview image.
- completionHandler: a block with result of preview image or error.
`image`: `NSImage`/`UIImage` object contains preview.
`error`: Error returned by system.
*/
func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void))
/**
Fetching properties of file like dimensions, duration, etc. It's variant depending on file type.
Images, videos and audio files meta-information will be returned.
- Note: `LocalFileInformationGenerator` variables can be set to change default behavior of
thumbnail and properties generator of `LocalFileProvider`.
- Parameters:
- path: path of file.
- completionHandler: a block with result of preview image or error.
`propertiesDictionary`: A `Dictionary` of proprty keys and values.
`keys`: An `Array` contains ordering of keys.
`error`: Error returned by system.
*/
func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void))
}
@@ -463,13 +785,21 @@ extension ExtendedFileProvider {
}
}
/// Operation type description of file operation, included files path in associated values.
public enum FileOperationType: CustomStringConvertible {
/// Creating a file or directory in path.
case create (path: String)
/// Copying a file or directory from source to destination.
case copy (source: String, destination: String)
/// Moving a file or directory from source to destination.
case move (source: String, destination: String)
/// Modifying data of a file o in path by writing new data.
case modify (path: String)
/// Deleting file or directory in path.
case remove (path: String)
/// Creating a symbolic link or alias to target.
case link (link: String, target: String)
/// Fetching data in file located in path.
case fetch (path: String)
public var description: String {
@@ -484,16 +814,24 @@ public enum FileOperationType: CustomStringConvertible {
}
}
/// present participle of action, like 'Copying`.
public var actionDescription: String {
return description.trimmingCharacters(in: CharacterSet(charactersIn: "e")) + "ing"
}
/// Path of subjecting file.
public var source: String? {
guard let reflect = Mirror(reflecting: self).children.first?.value else { return nil }
let mirror = Mirror(reflecting: reflect)
return reflect as? String ?? mirror.children.first?.value as? String
}
/// Path of subjecting file.
public var path: String? {
return source
}
/// Path of destination file.
public var destination: String? {
guard let reflect = Mirror(reflecting: self).children.first?.value else { return nil }
let mirror = Mirror(reflecting: reflect)
@@ -510,11 +848,22 @@ public enum FileOperationType: CustomStringConvertible {
public protocol OperationHandle {
/// Operation supposed to be done on files. Contains file paths as associated value.
var operationType: FileOperationType { get }
/// Bytes written/read by operation so far.
var bytesSoFar: Int64 { get }
/// Total bytes of operation.
var totalBytes: Int64 { get }
/// Operation is progress or not, Returns false if operation is done or not initiated yet.
var inProgress: Bool { get }
/// Progress of operation, usually equals with `bytesSoFar/totalBytes`. or NaN if not available.
var progress: Float { get }
/// Cancels operation while in progress, or cancels data/download/upload url session task.
func cancel() -> Bool
}
@@ -527,8 +876,15 @@ public extension OperationHandle {
}
public protocol FileProviderDelegate: class {
/// fileproviderSucceed(_:operation:) gives delegate a notification when an operation finished with success.
/// This method is called in main thread to avoids UI bugs.
func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperationType)
/// fileproviderSucceed(_:operation:) gives delegate a notification when an operation finished with failure.
/// This method is called in main thread to avoids UI bugs.
func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperationType)
/// fileproviderSucceed(_:operation:) gives delegate a notification when an operation progess.
/// Supported by some providers, especially remote ones.
/// This method is called in main thread to avoids UI bugs.
func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperationType, progress: Float)
}
+24 -4
View File
@@ -359,8 +359,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
let url = self.url(of: path)
let operationHandler: (URL) -> Void = { url in
let data = self.fileManager.contents(atPath: url.path)
completionHandler(data, nil)
do {
let data = try Data(contentsOf: url)
completionHandler(data, nil)
} catch let e {
completionHandler(nil, e)
}
}
if isCoorinating {
@@ -382,7 +386,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
@discardableResult
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
if length < 0 {
if length == 0 {
return nil
}
if offset == 0 && length < 0 {
return self.contents(path: path, completionHandler: completionHandler)
}
@@ -391,18 +399,30 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
let operationHandler: (URL) -> Void = { url in
guard self.fileManager.fileExists(atPath: url.path) && !url.fileIsDirectory else {
completionHandler(nil, self.throwError(path, code: URLError.cannotOpenFile as FoundationErrorEnum))
completionHandler(nil, self.throwError(path, code: URLError.fileDoesNotExist as FoundationErrorEnum))
return
}
guard let handle = FileHandle(forReadingAtPath: url.path) else {
completionHandler(nil, self.throwError(path, code: URLError.cannotOpenFile as FoundationErrorEnum))
return
}
defer {
handle.closeFile()
}
handle.seek(toFileOffset: UInt64(offset))
guard Int64(handle.offsetInFile) == offset else {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadUnknown as FoundationErrorEnum))
return
}
let data = handle.readData(ofLength: length)
guard length > 0 && data.count == length else {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
return
}
completionHandler(data, nil)
}
+12 -6
View File
@@ -14,27 +14,33 @@ public final class LocalFileObject: FileObject {
}
public convenience init? (fileWithPath path: String, relativeTo relativeURL: URL?) {
let fileURL: URL
var rpath = path.replacingOccurrences(of: relativeURL?.absoluteString ?? "", with: "")
var fileURL: URL?
var rpath = path.replacingOccurrences(of: relativeURL?.absoluteString ?? "", with: "", options: .anchored)
if path.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
}
if rpath.isEmpty {
fileURL = relativeURL ?? URL(fileURLWithPath: path)
fileURL = relativeURL
} else {
if #available(iOS 9.0, macOS 10.11, tvOS 9.0, *) {
fileURL = URL(fileURLWithPath: rpath, relativeTo: relativeURL)
} else {
fileURL = relativeURL?.appendingPathComponent(path) ?? URL(fileURLWithPath: path)
fileURL = URL(string: rpath, relativeTo: relativeURL)
}
}
self.init(fileWithURL: fileURL)
if let fileURL = fileURL {
self.init(fileWithURL: fileURL)
} else {
return nil
}
}
public convenience init?(fileWithURL fileURL: URL) {
do {
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey, .generationIdentifierKey])
self.init(url: fileURL, name: values.name ?? fileURL.lastPathComponent, path: fileURL.relativePath)
let path = fileURL.relativePath.hasPrefix("/") ? fileURL.relativePath : "/" + fileURL.relativePath
self.init(url: fileURL, name: values.name ?? fileURL.lastPathComponent, path: path)
for (key, value) in values.allValues {
self.allValues[key.rawValue] = value
}
+5 -1
View File
@@ -87,7 +87,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
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) {
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: json) {
fileObject = file
}
}
@@ -222,6 +222,10 @@ extension OneDriveFileProvider: FileProviderOperations {
extension OneDriveFileProvider: FileProviderReadWrite {
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
if length == 0 {
return nil
}
let opType = FileOperationType.fetch(path: path)
let url = URL(string: escaped(path: path) + ":/content", relativeTo: driveURL)!
var request = URLRequest(url: url)
+20 -22
View File
@@ -28,6 +28,23 @@ public final class OneDriveFileObject: FileObject {
super.init(url: url, name: name, path: path)
}
internal convenience init? (baseURL: URL?, drive: String, jsonStr: String) {
guard let json = jsonToDictionary(jsonStr) else { return nil }
self.init(baseURL: baseURL, drive: drive, json: json)
}
internal convenience init? (baseURL: URL?, drive: String, json: [String: AnyObject]) {
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)
self.init(baseURL: baseURL, name: name, path: lPath)
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
self.modifiedDate = resolve(dateString: json["lastModifiedDateTime"] as? String ?? "")
self.creationDate = resolve(dateString: json["createdDateTime"] as? String ?? "")
self.type = (json["folder"] as? String) != nil ? .directory : .regular
self.id = json["id"] as? String
self.entryTag = json["eTag"] as? String
}
open internal(set) var id: String? {
get {
@@ -79,7 +96,7 @@ internal extension OneDriveFileProvider {
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) {
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: entry) {
files.append(file)
}
}
@@ -173,7 +190,7 @@ internal extension OneDriveFileProvider {
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) {
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: entry) {
foundItem(file)
}
}
@@ -195,25 +212,6 @@ internal extension OneDriveFileProvider {
// 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(baseURL: self.baseURL, name: name, path: lPath)
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()
@@ -254,7 +252,7 @@ internal extension OneDriveFileProvider {
keys.append("Duration")
dic["Duration"] = OneDriveFileProvider.formatshort(interval: TimeInterval(duration) / 1000)
}
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = self.resolve(dateString: timeTakenStr) {
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = resolve(dateString: timeTakenStr) {
keys.append("Date taken")
OneDriveFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dic["Date taken"] = OneDriveFileProvider.dateFormatter.string(from: timeTaken)
+133 -123
View File
@@ -8,30 +8,6 @@
import Foundation
public final class WebDavFileObject: FileObject {
internal override init(url: URL, name: String, path: String) {
super.init(url: url, name: name, path: path)
}
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
}
}
}
/// Because this class uses NSURLSession, it's necessary to disable App Transport Security
/// in case of using this class with unencrypted HTTP connection.
@@ -105,12 +81,12 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
}
var fileObjects = [WebDavFileObject]()
if let data = data {
let xresponse = self.parseXMLResponse(data)
for attr in xresponse {
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
for attr in xresponse where attr.href != url {
if attr.href.path == url.path {
continue
}
fileObjects.append(self.mapToFileObject(attr))
fileObjects.append(WebDavFileObject(attr))
}
}
completionHandler(fileObjects, responseError ?? error)
@@ -131,9 +107,9 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
responseError = FileProviderWebDavError(code: rCode, url: url)
}
if let data = data {
let xresponse = self.parseXMLResponse(data)
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
if let attr = xresponse.first {
completionHandler(self.mapToFileObject(attr), responseError ?? error)
completionHandler(WebDavFileObject(attr), responseError ?? error)
return
}
}
@@ -158,7 +134,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
var totalSize: Int64 = -1
var usedSize: Int64 = 0
if let data = data {
let xresponse = self.parseXMLResponse(data)
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
if let attr = xresponse.first {
totalSize = Int64(attr.prop["quota-available-bytes"] ?? "") ?? -1
usedSize = Int64(attr.prop["quota-used-bytes"] ?? "") ?? 0
@@ -270,7 +246,7 @@ extension WebDAVFileProvider: FileProviderOperations {
responseError = FileProviderWebDavError(code: code, url: sourceURL)
}
if code == .multiStatus, let data = data {
let xresponses = self.parseXMLResponse(data)
let xresponses = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
for xresponse in xresponses where (xresponse.status ?? 0) >= 300 {
completionHandler?(FileProviderWebDavError(code: code, url: sourceURL))
}
@@ -343,6 +319,10 @@ extension WebDAVFileProvider: FileProviderOperations {
extension WebDAVFileProvider: FileProviderReadWrite {
@discardableResult
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
if length == 0 {
return nil
}
let opType = FileOperationType.fetch(path: path)
let url = self.url(of: path)
var request = URLRequest(url: url)
@@ -411,14 +391,14 @@ extension WebDAVFileProvider: FileProviderReadWrite {
responseError = FileProviderWebDavError(code: rCode, url: url)
}
if let data = data {
let xresponse = self.parseXMLResponse(data)
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
var fileObjects = [WebDavFileObject]()
for attr in xresponse {
let path = attr.href.path
if !((path as NSString).lastPathComponent.contains(query)) {
continue
}
let fileObject = self.mapToFileObject(attr)
let fileObject = WebDavFileObject(attr)
fileObjects.append(fileObject)
foundItemHandler?(fileObject)
}
@@ -436,9 +416,10 @@ extension WebDAVFileProvider: FileProviderReadWrite {
* A messy approach is listing a directory with an interval period and compare
* with previous results
*/
NotImplemented()
}
fileprivate func unregisterNotifcation(path: String) {
NotImplemented()
}
// TODO: implements methods for lock mechanism
}
@@ -458,37 +439,34 @@ extension WebDAVFileProvider: FileProvider {
// MARK: WEBDAV XML response implementation
internal extension WebDAVFileProvider {
struct DavResponse {
let href: URL
let hrefString: String
let status: Int?
let prop: [String: String]
fileprivate 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)
}
})
}
}
struct DavResponse {
let href: URL
let hrefString: String
let status: Int?
let prop: [String: String]
fileprivate func parseXMLResponse(_ response: Data) -> [DavResponse] {
var result = [DavResponse]()
do {
let xml = try AEXMLDocument(xml: response)
var rootnode = xml.root
var responsetag = "response"
for node in rootnode.all ?? [] where node.name.lowercased().hasSuffix("multistatus") {
rootnode = node
init? (_ node: AEXMLElement, baseURL: URL?) {
func removeSlash(_ str: String) -> String {
if str.hasPrefix("/") {
return str.substring(from: str.index(after: str.startIndex))
} else {
return str
}
for node in rootnode.children where node.name.lowercased().hasSuffix("response") {
responsetag = node.name
break
}
for responseNode in rootnode[responsetag].all ?? [] {
if let davResponse = mapNodeToDavResponse(responseNode) {
result.append(davResponse)
}
}
} catch _ {
}
return result
}
fileprivate func mapNodeToDavResponse(_ node: AEXMLElement) -> DavResponse? {
// find node names with namespace
var hreftag = "href"
var statustag = "status"
var propstattag = "propstat"
@@ -503,75 +481,107 @@ internal extension WebDAVFileProvider {
propstattag = node.name
}
}
let href = node[hreftag].value
if let href = href {
let phrefURL: URL?
var relativePath = href.replacingOccurrences(of: self.baseURL?.absoluteString ?? "", with: "/")
if relativePath.hasPrefix("http://") || relativePath.hasPrefix("http://") {
phrefURL = URL(string: href)
} else {
if relativePath.hasPrefix("/") {
relativePath.remove(at: relativePath.startIndex)
}
phrefURL = URL(string: relativePath, relativeTo: self.baseURL) ?? self.baseURL
}
//let hrefURL = URL(string: href)
guard let hrefURL = phrefURL else { return nil }
var status: Int?
let statusDesc = (node[statustag].string).components(separatedBy: " ")
if statusDesc.count > 2 {
status = Int(statusDesc[1])
}
var propDic = [String: String]()
let propStatNode = node[propstattag]
for node in propStatNode.children where node.name.lowercased().hasSuffix("status"){
statustag = node.name
break
}
let statusDesc2 = (propStatNode[statustag].string).components(separatedBy: " ")
if statusDesc2.count > 2 {
status = Int(statusDesc2[1])
}
var proptag = "prop"
for tnode in propStatNode.children where tnode.name.lowercased().hasSuffix("prop") {
proptag = tnode.name
break
}
for propItemNode in propStatNode[proptag].children {
propDic[propItemNode.name.components(separatedBy: ":").last!.lowercased()] = propItemNode.value
if propItemNode.name.hasSuffix("resourcetype") && propItemNode.xml.contains("collection") {
propDic["getcontenttype"] = "httpd/unix-directory"
}
}
return DavResponse(href: hrefURL, hrefString: href, status: status, prop: propDic)
guard let hrefString = node[hreftag].value else { return nil }
// trying to figure out relative path out of href
let hrefAbsolute = URL(string: hrefString, relativeTo: baseURL)?.absoluteString ?? hrefString
let relativePath = hrefAbsolute.replacingOccurrences(of: baseURL?.absoluteString ?? "", with: "", options: .anchored, range: nil)
let hrefURL = URL(string: removeSlash(relativePath), relativeTo: baseURL) ?? baseURL
guard let href = hrefURL?.standardized else { return nil }
// reading status and properties
var status: Int?
let statusDesc = (node[statustag].string).components(separatedBy: " ")
if statusDesc.count > 2 {
status = Int(statusDesc[1])
}
return nil
var propDic = [String: String]()
let propStatNode = node[propstattag]
for node in propStatNode.children where node.name.lowercased().hasSuffix("status"){
statustag = node.name
break
}
let statusDesc2 = (propStatNode[statustag].string).components(separatedBy: " ")
if statusDesc2.count > 2 {
status = Int(statusDesc2[1])
}
var proptag = "prop"
for tnode in propStatNode.children where tnode.name.lowercased().hasSuffix("prop") {
proptag = tnode.name
break
}
for propItemNode in propStatNode[proptag].children {
propDic[propItemNode.name.components(separatedBy: ":").last!.lowercased()] = propItemNode.value
if propItemNode.name.hasSuffix("resourcetype") && propItemNode.xml.contains("collection") {
propDic["getcontenttype"] = "httpd/unix-directory"
}
}
self.href = href
self.hrefString = hrefString
self.status = status
self.prop = propDic
}
fileprivate func mapToFileObject(_ davResponse: DavResponse) -> WebDavFileObject {
var href = davResponse.href
if href.baseURL == nil {
href = url(of: href.path)
static func parse(xmlResponse: Data, baseURL: URL?) -> [DavResponse] {
var result = [DavResponse]()
do {
let xml = try AEXMLDocument(xml: xmlResponse)
var rootnode = xml.root
var responsetag = "response"
for node in rootnode.all ?? [] where node.name.lowercased().hasSuffix("multistatus") {
rootnode = node
}
for node in rootnode.children where node.name.lowercased().hasSuffix("response") {
responsetag = node.name
break
}
for responseNode in rootnode[responsetag].all ?? [] {
if let davResponse = DavResponse(responseNode, baseURL: baseURL) {
result.append(davResponse)
}
}
} catch _ {
}
return result
}
}
public final class WebDavFileObject: FileObject {
internal init(_ davResponse: DavResponse) {
let href = davResponse.href
let name = davResponse.prop["displayname"] ?? (davResponse.hrefString.removingPercentEncoding! as NSString).lastPathComponent
let fileObject = WebDavFileObject(url: href, name: name, path: href.path)
fileObject.size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown
fileObject.creationDate = self.resolve(dateString: davResponse.prop["creationdate"] ?? "")
fileObject.modifiedDate = self.resolve(dateString: davResponse.prop["getlastmodified"] ?? "")
fileObject.contentType = davResponse.prop["getcontenttype"] ?? "octet/stream"
fileObject.type = fileObject.contentType == "httpd/unix-directory" ? .directory : .regular
fileObject.entryTag = davResponse.prop["getetag"]
return fileObject
let relativePath = href.relativePath
let path = relativePath.hasPrefix("/") ? relativePath : ("/" + relativePath)
super.init(url: href, name: name, path: path)
self.size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown
self.creationDate = resolve(dateString: davResponse.prop["creationdate"] ?? "")
self.modifiedDate = resolve(dateString: davResponse.prop["getlastmodified"] ?? "")
self.contentType = davResponse.prop["getcontenttype"] ?? "octet/stream"
self.isHidden = (Int(davResponse.prop["ishidden"] ?? "0") ?? 0) > 0
self.type = self.contentType == "httpd/unix-directory" ? .directory : .regular
self.entryTag = davResponse.prop["getetag"]
}
fileprivate 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)
}
})
/// MIME type of the file
open internal(set) var contentType: String {
get {
return allValues["NSURLContentTypeKey"] as? String ?? ""
}
set {
allValues["NSURLContentTypeKey"] = newValue
}
}
/// HTTP E-Tag, can be used to mark changed files
open internal(set) var entryTag: String? {
get {
return allValues["NSURLEntryTagKey"] as? String
}
set {
allValues["NSURLEntryTagKey"] = newValue
}
}
}