Compare commits

...

43 Commits

Author SHA1 Message Date
Amir Abbas 435ed37d93 Updated pod version to 0.21 2017-10-27 09:21:17 +03:30
Amir Abbas 9e4a803345 Removed unnecessary properties from SessionDelegate 2017-10-26 14:07:57 +03:30
Amir Abbas 0e53036855 Added progressive downloading for HTTP-based providers
- Added HEIC/HEIF image thumbnail and properties
- Fixed bug: data task recieve data body called only once
2017-10-26 13:56:41 +03:30
Amir Abbas 87317c1d42 Fixed #61, OneDrive progress availibility 2017-10-13 08:55:55 +03:30
Amir Abbas 4c35e446a7 Fix provider base url (as directory), caused Cloud provider misbehaves 2017-10-13 02:31:31 +03:30
Amir Abbas 47516c63ad Refactored contentsOfDirectory to searchFiles where applicable 2017-10-12 18:30:28 +03:30
Amir Abbas 6cd5b7d8c5 Fixed error description in Dropbox (except uploads)
- Refactors in Dropbox request maker method
2017-10-11 01:14:02 +03:30
Amir Abbas 598444c90f Codebeat GPA improvements 2017-10-10 16:48:55 +03:30
Amir Abbas 370866442a Fixed Compilation error for upload size type 2017-10-10 11:40:32 +03:30
Amir Abbas 9cc9a6a96a Improving documentation
- Renaming OneDriveProvider.SubAddress to Route
- Refactors to improve codebeat score
2017-10-10 11:07:28 +03:30
Amir Abbas 2969af28b8 Small refactors 2017-10-09 13:32:46 +03:30
Amir Abbas d41e86b30a Fixed Swift 3 flatMap error 2017-10-09 11:31:02 +03:30
Amir Abbas aaa1def9e7 Refactored paginated listing in Dropbox and OneDrive
- Removed currentPath
2017-10-09 11:16:17 +03:30
Amir Abbas 024a3637b4 Fix compile time error 2017-10-04 16:32:54 +03:30
Amir Abbas b20fc3efe5 Fix compilation error on swift 3 2017-10-04 15:11:26 +03:30
Amir Abbas 682dd40072 File coordination enabled by default for copyItem(locaURL:) methods 2017-10-04 15:06:27 +03:30
Amir Abbas 3f35e600cd Removing deprecated String.characters usage
- Documentation for using file id in Dropbox & OneDrive
2017-10-04 15:04:23 +03:30
Amir Abbas 9cd2fa2e3a Compilation optimizations, refactoring 2017-10-01 02:04:29 +03:30
Amir Abbas 7be6985454 Updated OneDrive API to new version 2017-09-29 21:39:28 +03:30
Amir Abbas 04ac3e22e7 Refactored correctPath to specified providers 2017-09-29 20:51:09 +03:30
Amir Abbas c8de7fdb69 Fixed NSProgress parenting, iCloud KVO, crash in url(of) 2017-09-29 18:11:26 +03:30
Amir Abbas 42b879d4e2 Fix Swift 3 bug 2017-09-24 15:37:00 +03:30
Amir Abbas 5800c9a2ec Improved compile speed, Fixed Swift 4 warnings 2017-09-24 13:12:31 +03:30
Amir Abbas Mousavian 7c9b2bf09a Merge pull request #65 from yuyan7/fix_webdav
Fix HTTP providers not be overwritten
2017-09-16 20:31:59 +04:30
yuya-nakamura ba08cdda1d Fixed WebDAV not be overwritten 2017-09-16 23:01:57 +09:00
Amir Abbas b380685932 Added VolumeObject for storageProperties method
- Refined error handling in HTTP provider
- Added contentType and hash to OneDriveFileObject
2017-09-05 02:26:09 +04:30
Amir Abbas 5a5beb6891 Refined error handling 2017-09-04 01:22:44 +04:30
Amir Abbas c7201a8be7 Fixed currentPath docs, delegateNotify method default error arg 2017-08-29 01:40:15 +04:30
Amir Abbas d2a03369fd Fixed FTP crash on rucursive remove 2017-08-28 00:06:02 +04:30
Amir Abbas d08098008a Removed FoundationErrorEnum, Refactoed delegate progress routine 2017-08-27 13:48:29 +04:30
Amir Abbas 3c5303214e Fixed Array extension error in HashMAC 2017-08-24 15:08:22 +04:30
Amir Abbas ad8f333d16 Fixed swift 3 compile error on Array extenson 2017-08-24 08:36:11 +04:30
Amir Abbas 5e8f653c52 Fixed compile bug in Swift 3.0 for Array.Iterator.Element 2017-08-23 23:21:17 +04:30
Amir Abbas e4cd7ddf22 Fixed Quality<T> percision 2017-08-23 22:18:18 +04:30
Amir Abbas 1efb0e3fe5 Fixed Quality<T> 2017-08-23 22:15:45 +04:30
Amir Abbas 1292856646 Added convenience methods for URLRequest headers 2017-08-23 22:04:47 +04:30
Amir Abbas e39f9c29ec Merge commit '3d44de0b407c3cd604189cc161dc1fc039409763' 2017-08-23 10:54:59 +04:30
Amir Abbas f43199c22a fixed colon in url string problem across provider 2017-08-23 10:52:45 +04:30
Amir Abbas Mousavian 3d44de0b40 Added missed filePathAllowed const to CharacterSet 2017-08-23 01:22:19 +04:30
Amir Abbas 21b5214481 Deprecate currentPath property, returning error in provider delegate
- Using checkResourceIsReachable to check file exists
- fixed bug: downloading/uploading files with colon in name
2017-08-23 01:19:56 +04:30
Amir Abbas f0b4925db2 Consistancy to delegate call in all contents(of:) methods
- renamed opType instances to operation
- moved to v2 in Dropbox
- removed redundant CloudFileProvider methods
2017-08-20 22:41:53 +04:30
Amir Abbas 0da957d25f Making protocol method implementations overridable 2017-08-19 19:47:54 +04:30
Amir Abbas 0de558c160 Recursive search fixed in FTP, OneDrive, WebDAV 2017-08-19 06:49:15 +04:30
27 changed files with 3212 additions and 2280 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
#
s.name = "FilesProvider"
s.version = "0.20.0"
s.version = "0.21.0"
s.summary = "FileManager replacement for Local and Remote (WebDAV/FTP/Dropbox/OneDrive/SMB2) files on iOS and macOS."
# This description is used to generate tags and improve search results.
@@ -58,7 +58,7 @@ Pod::Spec.new do |s|
s.author = { "Amir Abbas Mousavian" => "a.mosavian@gmail.com" }
# Or just: s.author = "Amir Abbas Mousavian"
# s.authors = { "Amir Abbas Mousavian" => "a.mosavian@gmail.com" }
# s.social_media_url = "https://twitter.com/amosavian"
s.social_media_url = "https://twitter.com/amosavian"
# ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
+26 -10
View File
@@ -38,6 +38,12 @@
7936BC121E880F5700A6C81C /* FTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7936BC111E880F5700A6C81C /* FTPFileProvider.swift */; };
7936BC131E880F5700A6C81C /* FTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7936BC111E880F5700A6C81C /* FTPFileProvider.swift */; };
7936BC141E880F5700A6C81C /* FTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7936BC111E880F5700A6C81C /* FTPFileProvider.swift */; };
793CCE2A1F4B8C5C00BC8288 /* FoundationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793CCE291F4B8C5C00BC8288 /* FoundationExtensions.swift */; };
793CCE2B1F4B8C5C00BC8288 /* FoundationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793CCE291F4B8C5C00BC8288 /* FoundationExtensions.swift */; };
793CCE2C1F4B8C5C00BC8288 /* FoundationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793CCE291F4B8C5C00BC8288 /* FoundationExtensions.swift */; };
793CCE2E1F4B8C7B00BC8288 /* HashMAC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793CCE2D1F4B8C7B00BC8288 /* HashMAC.swift */; };
793CCE2F1F4B8C7B00BC8288 /* HashMAC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793CCE2D1F4B8C7B00BC8288 /* HashMAC.swift */; };
793CCE301F4B8C7B00BC8288 /* HashMAC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793CCE2D1F4B8C7B00BC8288 /* HashMAC.swift */; };
79480FF61E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; };
79480FF71E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; };
79480FF81E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; };
@@ -53,9 +59,6 @@
7958155A1F478ED9003344DD /* HTTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 795815591F478ED9003344DD /* HTTPFileProvider.swift */; };
7958155B1F478ED9003344DD /* HTTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 795815591F478ED9003344DD /* HTTPFileProvider.swift */; };
7958155C1F478ED9003344DD /* HTTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 795815591F478ED9003344DD /* HTTPFileProvider.swift */; };
796807551E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */; };
796807561E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */; };
796807571E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */; };
798654331E8874BC002FA550 /* FTPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798654321E8874BC002FA550 /* FTPHelper.swift */; };
799396AA1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; };
799396AB1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; };
@@ -143,12 +146,13 @@
7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FPSStreamTask.swift; sourceTree = "<group>"; };
792572401DF23BDA006A1526 /* LocalHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalHelper.swift; sourceTree = "<group>"; };
7936BC111E880F5700A6C81C /* FTPFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTPFileProvider.swift; sourceTree = "<group>"; };
793CCE291F4B8C5C00BC8288 /* FoundationExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationExtensions.swift; sourceTree = "<group>"; };
793CCE2D1F4B8C7B00BC8288 /* HashMAC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HashMAC.swift; sourceTree = "<group>"; };
79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudFileProvider.swift; sourceTree = "<group>"; };
794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropboxHelper.swift; sourceTree = "<group>"; };
794C22091D5893F800EC49B8 /* SMB2Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2Notification.swift; sourceTree = "<group>"; };
794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2QueryTypes.swift; sourceTree = "<group>"; };
795815591F478ED9003344DD /* HTTPFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPFileProvider.swift; sourceTree = "<group>"; };
796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileProviderExtensions.swift; sourceTree = "<group>"; };
798654321E8874BC002FA550 /* FTPHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTPHelper.swift; sourceTree = "<group>"; };
799396671D48B7F600086753 /* FilesProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FilesProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
799396751D48B80D00086753 /* FilesProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FilesProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -259,6 +263,15 @@
path = AEXML;
sourceTree = "<group>";
};
793CCE281F4B8C3600BC8288 /* Extensions */ = {
isa = PBXGroup;
children = (
793CCE291F4B8C5C00BC8288 /* FoundationExtensions.swift */,
793CCE2D1F4B8C7B00BC8288 /* HashMAC.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
7993965B1D48B7BF00086753 = {
isa = PBXGroup;
children = (
@@ -294,10 +307,10 @@
isa = PBXGroup;
children = (
7924B18B1D89DAE000589DB7 /* AEXML */,
793CCE281F4B8C3600BC8288 /* Extensions */,
799396991D48C02300086753 /* SMBTypes */,
799396941D48C02300086753 /* FileProvider.h */,
799396951D48C02300086753 /* FileProvider.swift */,
796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */,
79F5745A1DFDB10A00179ABF /* FileObject.swift */,
799396961D48C02300086753 /* LocalFileProvider.swift */,
792572401DF23BDA006A1526 /* LocalHelper.swift */,
@@ -495,7 +508,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
796807551E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */,
799396B31D48C02300086753 /* LocalFileProvider.swift in Sources */,
799396D41D48C02300086753 /* SMB2Tree.swift in Sources */,
7924B1A21D89DAE000589DB7 /* Options.swift in Sources */,
@@ -525,10 +537,12 @@
799396E01D48C02300086753 /* WebDAVFileProvider.swift in Sources */,
799396DA1D48C02300086753 /* SMBErrorType.swift in Sources */,
799396C21D48C02300086753 /* SMB2FileHandle.swift in Sources */,
793CCE2E1F4B8C7B00BC8288 /* HashMAC.swift in Sources */,
799396CB1D48C02300086753 /* SMB2Query.swift in Sources */,
799396AA1D48C02300086753 /* DropboxFileProvider.swift in Sources */,
79BD638C1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */,
7924B1B31D89FD6400589DB7 /* SMBClient.swift in Sources */,
793CCE2A1F4B8C5C00BC8288 /* FoundationExtensions.swift in Sources */,
7924B1A51D89DAE000589DB7 /* Parser.swift in Sources */,
799396B01D48C02300086753 /* FileProvider.swift in Sources */,
7924B19C1D89DAE000589DB7 /* Error.swift in Sources */,
@@ -539,7 +553,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
796807561E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */,
799396B41D48C02300086753 /* LocalFileProvider.swift in Sources */,
799396D51D48C02300086753 /* SMB2Tree.swift in Sources */,
7924B1A31D89DAE000589DB7 /* Options.swift in Sources */,
@@ -569,10 +582,12 @@
799396E11D48C02300086753 /* WebDAVFileProvider.swift in Sources */,
799396DB1D48C02300086753 /* SMBErrorType.swift in Sources */,
799396C31D48C02300086753 /* SMB2FileHandle.swift in Sources */,
793CCE2F1F4B8C7B00BC8288 /* HashMAC.swift in Sources */,
799396CC1D48C02300086753 /* SMB2Query.swift in Sources */,
7924B1AD1D89F7D800589DB7 /* SMBClient.swift in Sources */,
79BD638D1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */,
799396AB1D48C02300086753 /* DropboxFileProvider.swift in Sources */,
793CCE2B1F4B8C5C00BC8288 /* FoundationExtensions.swift in Sources */,
7924B1A61D89DAE000589DB7 /* Parser.swift in Sources */,
799396B11D48C02300086753 /* FileProvider.swift in Sources */,
7924B19D1D89DAE000589DB7 /* Error.swift in Sources */,
@@ -583,7 +598,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
796807571E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */,
799396B51D48C02300086753 /* LocalFileProvider.swift in Sources */,
799396D61D48C02300086753 /* SMB2Tree.swift in Sources */,
7924B1A41D89DAE000589DB7 /* Options.swift in Sources */,
@@ -613,10 +627,12 @@
799396E21D48C02300086753 /* WebDAVFileProvider.swift in Sources */,
799396DC1D48C02300086753 /* SMBErrorType.swift in Sources */,
799396C41D48C02300086753 /* SMB2FileHandle.swift in Sources */,
793CCE301F4B8C7B00BC8288 /* HashMAC.swift in Sources */,
799396CD1D48C02300086753 /* SMB2Query.swift in Sources */,
7924B1AE1D89F7D900589DB7 /* SMBClient.swift in Sources */,
79BD638E1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */,
799396AC1D48C02300086753 /* DropboxFileProvider.swift in Sources */,
793CCE2C1F4B8C5C00BC8288 /* FoundationExtensions.swift in Sources */,
7924B1A71D89DAE000589DB7 /* Parser.swift in Sources */,
799396B21D48C02300086753 /* FileProvider.swift in Sources */,
7924B19E1D89DAE000589DB7 /* Error.swift in Sources */,
@@ -629,7 +645,7 @@
799396601D48B7BF00086753 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.20.0;
BUNDLE_VERSION_STRING = 0.21.0;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -661,7 +677,7 @@
799396611D48B7BF00086753 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.20.0;
BUNDLE_VERSION_STRING = 0.21.0;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
+4 -4
View File
@@ -41,8 +41,8 @@ All functions do async calls and it wont block your main thread.
* For now it has limitation in uploading files up to 150MB.
- [x] **OneDriveFileProvider** A wrapper around OneDrive REST API, works with `onedrive.com` and compatible (business) servers.
* For now it has limitation in uploading files up to 100MB.
- [ ] **GoogleFileProvider** A wrapper around Goodle Drive REST API.
- [ ] **AmazonS3FileProvider** Amazon storage backend. Used by many sites.
- [ ] **GoogleFileProvider** A wrapper around Goodle Drive REST 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!*.
* SMBv1/CIFS is insecure, deprecated and kinda tricky to be implemented due to strict memory allignment in Swift.
@@ -155,7 +155,7 @@ let credential = URLCredential(user: "user", password: "pass", persistence: .per
let webdavProvider = WebDAVFileProvider(baseURL: URL(string: "http://www.example.com/dav")!, credential: credential)
```
* 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)
* In case you want to connect non-secure servers for WebDAV (http) or FTP in iOS 9+ / macOS 10.11+ you should disable App Transport Security (ATS) according to [this guide.](https://gist.github.com/mlynch/284699d676fe9ed0abfa)
* For Dropbox & OneDrive, user is clientID and password is Token which both must be retrieved via [OAuth2 API of Dropbox](https://www.dropbox.com/developers/reference/oauth-guide). There are libraries like [p2/OAuth2](https://github.com/p2/OAuth2) or [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) which can facilate the procedure to retrieve token. The latter is easier to use and prefered.
@@ -415,12 +415,12 @@ You can monitor updates in some file system (Local and SMB2), there is three met
```swift
// to register a new notification handler
documentsProvider.registerNotifcation(path: provider.currentPath) {
documentsProvider.registerNotifcation(path: "/") {
// calling functions to update UI
}
// To discontinue monitoring folders:
documentsProvider.unregisterNotifcation(path: provider.currentPath)
documentsProvider.unregisterNotifcation(path: "/")
```
* **Please note** in LocalFileProvider it will also monitor changes in subfolders. This behaviour can varies according to file system specification.
+262 -339
View File
@@ -26,8 +26,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
return true
}
set {
assert(true, "CloudFileProvider.isCoorinating can't be set")
return
assert(newValue, "CloudFileProvider.isCoorinating can't be set to false")
}
}
@@ -51,7 +50,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
If you specify nil for this parameter, this method uses the first container listed in the `com.apple.developer.ubiquity-container-identifiers` entitlement array.
- Parameter scope: Use `.documents` (default) to put documents that the user is allowed to access inside a Documents subdirectory. Otherwise use `.data` to store user-related data files that your app needs to share but that are not files you want the user to manipulate directly.
*/
public init? (containerId: String?, scope: UbiquitousScope = .documents) {
public convenience init? (containerId: String?, scope: UbiquitousScope = .documents) {
assert(!(CloudFileProvider.asserting && Thread.isMainThread), "CloudFileProvider.init(containerId:) is not recommended to be executed on Main Thread.")
guard FileManager.default.ubiquityIdentityToken != nil else {
return nil
@@ -59,8 +58,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
guard let ubiquityURL = FileManager.default.url(forUbiquityContainerIdentifier: containerId) else {
return nil
}
self.containerId = containerId
self.scope = scope
let baseURL: URL
if scope == .documents {
baseURL = ubiquityURL.appendingPathComponent("Documents/")
@@ -68,46 +66,54 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
baseURL = ubiquityURL
}
super.init(baseURL: baseURL)
self.isCoorinating = true
#if swift(>=3.1)
let queueLabel = "FileProvider.\(Swift.type(of: self).type)"
#else
let queueLabel = "FileProvider.\(type(of: self).type)"
#endif
dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent)
operation_queue = OperationQueue()
operation_queue.name = "\(queueLabel).Operation"
self.init(baseURL: baseURL)
self.containerId = containerId
self.scope = scope
// To prepare FileManager objects?!
fileManager.url(forUbiquityContainerIdentifier: containerId)
opFileManager.url(forUbiquityContainerIdentifier: containerId)
try? fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true)
}
public override init(baseURL: URL) {
self.scope = .data
super.init(baseURL: baseURL)
self.isCoorinating = true
#if swift(>=3.1)
let queueLabel = "FileProvider.\(Swift.type(of: self).type)"
#else
let queueLabel = "FileProvider.\(type(of: self).type)"
#endif
dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent)
operation_queue = OperationQueue()
operation_queue.name = "\(queueLabel).Operation"
}
public required convenience init?(coder aDecoder: NSCoder) {
guard let containerId = aDecoder.decodeObject(forKey: "containerId") as? String,
if let containerId = aDecoder.decodeObject(forKey: "containerId") as? String,
let scopeString = aDecoder.decodeObject(forKey: "scope") as? String,
let scope = UbiquitousScope(rawValue: scopeString) else {
let scope = UbiquitousScope(rawValue: scopeString) {
self.init(containerId: containerId, scope: scope)
} else if let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL {
self.init(baseURL: baseURL)
} else {
return nil
}
self.init(containerId: containerId, scope: scope)
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
self.isCoorinating = aDecoder.decodeBool(forKey: "isCoorinating")
}
open override func encode(with aCoder: NSCoder) {
super.encode(with: aCoder)
aCoder.encode(self.containerId, forKey: "containerId")
aCoder.encode(self.scope.rawValue, forKey: "scope")
aCoder.encode(self.currentPath, forKey: "currentPath")
aCoder.encode(self.isCoorinating, forKey: "isCoorinating")
}
open override func copy(with zone: NSZone? = nil) -> Any {
let copy = CloudFileProvider(containerId: self.containerId, scope: self.scope)
copy?.currentPath = self.currentPath
copy?.delegate = self.delegate
copy?.fileOperationDelegate = self.fileOperationDelegate
return copy as Any
@@ -118,67 +124,21 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
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 closure with result of directory entries or error.
`contents`: An array of `FileObject` identifying the the directory entries.
`error`: Error returned by system.
- Parameters:
- path: path to target directory. If empty, root will be iterated.
- completionHandler: a closure with result of directory entries or error.
- contents: An array of `FileObject` identifying the the directory entries.
- error: Error returned by system.
*/
open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
open override func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
// FIXME: create runloop for dispatch_queue, start query on it
dispatch_queue.async {
let pathURL = self.url(of: path)
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K BEGINSWITH %@", NSMetadataItemPathKey, pathURL.path)
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]
query.searchScopes = [self.scope.rawValue]
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
defer {
query.stop()
NotificationCenter.default.removeObserver(finishObserver!)
}
guard let results = query.results as? [NSMetadataItem] else {
return
}
query.disableUpdates()
var contents = [FileObject]()
for result in results {
guard let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
continue
}
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
continue
}
if let file = self.mapFileObject(attributes: attribs) {
contents.append(file)
}
}
query.stop()
self.dispatch_queue.async {
completionHandler(contents, nil)
}
})
DispatchQueue.main.async {
if !query.start() {
self.dispatch_queue.async {
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
}
}
}
}
let query = NSPredicate(format: "TRUEPREDICATE")
_ = searchFiles(path: path, recursive: false, query: query, foundItemHandler: nil, completionHandler: completionHandler)
}
/// Please don't rely this function to get iCloud drive total and remaining capacity
/// - Important: iCloud Storage size and free space is unavailable, it returns local space
open override func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) {
open override func storageProperties(completionHandler: @escaping (VolumeObject?) -> Void) {
super.storageProperties(completionHandler: completionHandler)
}
@@ -187,16 +147,17 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
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 closure with result of directory entries or error.
`attributes`: A `FileObject` containing the attributes of the item.
`error`: Error returned by system.
- Parameters:
- path: path to target directory. If empty, attributes of root will be returned.
- completionHandler: a closure with result of directory entries or error.
- attributes: A `FileObject` containing the attributes of the item.
- error: Error returned by system.
*/
open override func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
open override func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
dispatch_queue.async {
let pathURL = self.url(of: path)
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemPathKey, pathURL.path)
query.predicate = NSPredicate(format: "%K LIKE[CD] %@", NSMetadataItemPathKey, pathURL.path)
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]
query.searchScopes = [self.scope.rawValue]
var finishObserver: NSObjectProtocol?
@@ -209,7 +170,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
query.disableUpdates()
guard let result = (query.results as? [NSMetadataItem])?.first, let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
let error = self.throwError(path, code: CocoaError.fileNoSuchFile)
let error = self.cocoaError(path, code: .fileNoSuchFile)
self.dispatch_queue.async {
completionHandler(nil, error)
}
@@ -221,7 +182,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
completionHandler(file, nil)
}
} else {
let noFileError = self.throwError(path, code: CocoaError.fileNoSuchFile)
let noFileError = self.cocoaError(path, code: .fileNoSuchFile)
self.dispatch_queue.async {
completionHandler(nil, noFileError)
}
@@ -230,7 +191,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
DispatchQueue.main.async {
if !query.start() {
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadNoPermission))
completionHandler(nil, self.cocoaError(path, code: .fileReadNoPermission))
}
}
}
@@ -240,109 +201,51 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
/**
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.
Sample predicates:
```
NSPredicate(format: "(name CONTAINS[c] 'hello') && (filesize >= 10000)")
NSPredicate(format: "(modifiedDate >= %@)", Date())
NSPredicate(format: "(path BEGINSWITH %@)", "folder/child folder")
```
- Note: Don't pass Spotlight predicates to this method directly, use `FileProvider.convertSpotlightPredicateTo()` method to get usable predicate.
- Important: A file name criteria should be provided for Dropbox.
- 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: Closure which is called when a file is found
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
- path: location of directory to start search
- recursive: Searching subdirectories of path
- query: An `NSPredicate` object with keys like `FileObject` members, except `size` which becomes `filesize`.
- foundItemHandler: Closure which is called when a file is found
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
- files: all files meat the `query` criteria.
- error: `Error` returned by server if occured.
- Returns: An `Progress` to get progress or cancel progress. Use `completedUnitCount` to iterate count of found items.
*/
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: -1)
let mapDict: [String: String] = ["url": NSMetadataItemURLKey, "name": NSMetadataItemFSNameKey, "path": NSMetadataItemPathKey, "filesize": NSMetadataItemFSSizeKey, "modifiedDate": NSMetadataItemFSContentChangeDateKey, "creationDate": NSMetadataItemFSCreationDateKey, "contentType": NSMetadataItemContentTypeKey]
let pathURL = self.url(of: path)
progress.setUserInfoObject(pathURL, forKey: .fileURLKey)
let mdquery = NSMetadataQuery()
mdquery.predicate = NSPredicate(format: "(%K BEGINSWITH[CD] %@) && (\(updateQueryTypeKeys(query).predicateFormat))", NSMetadataItemPathKey, pathURL.path)
mdquery.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]
mdquery.searchScopes = [self.scope.rawValue]
func updateQueryKeys(_ queryComponent: NSPredicate) -> NSPredicate {
if let cQuery = queryComponent as? NSCompoundPredicate {
let newSub = cQuery.subpredicates.map { updateQueryKeys($0 as! NSPredicate) }
switch cQuery.compoundPredicateType {
case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub)
case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub.first!)
case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub)
}
} else if let cQuery = queryComponent as? NSComparisonPredicate {
var newLeft = cQuery.leftExpression
var newRight = cQuery.rightExpression
if newLeft.expressionType == .keyPath, let newKey = mapDict[newLeft.keyPath] {
newLeft = NSExpression(forKeyPath: newKey)
}
if newRight.expressionType == .keyPath, let newKey = mapDict[newRight.keyPath] {
newRight = NSExpression(forKeyPath: newKey)
}
if newLeft.expressionType == .keyPath, newLeft.keyPath == "type" {
newRight = NSExpression(forConstantValue: newRight.constantValue as? String == "directory" ? "public.directory": "public.data")
}
if newRight.expressionType == .keyPath, newRight.keyPath == "type" {
newLeft = NSExpression(forConstantValue: newLeft.constantValue as? String == "directory" ? "public.directory": "public.data")
}
return NSComparisonPredicate(leftExpression: newLeft, rightExpression: newRight, modifier: cQuery.comparisonPredicateModifier, type: cQuery.predicateOperatorType, options: cQuery.options)
} else {
return queryComponent
}
var lastReportedCount = 0
progress.cancellationHandler = { [weak mdquery] in
mdquery?.stop()
}
let progress = Progress(parent: nil, userInfo: nil)
dispatch_queue.async {
let pathURL = self.url(of: path)
progress.setUserInfoObject(pathURL, forKey: .fileURLKey)
let mdquery = NSMetadataQuery()
mdquery.predicate = NSPredicate(format: "(%K BEGINSWITH %@) && (\(updateQueryKeys(query).predicateFormat))", NSMetadataItemPathKey, pathURL.path)
mdquery.searchScopes = [self.scope.rawValue]
var lastReportedCount = 0
progress.cancellationHandler = { [weak mdquery] in
mdquery?.stop()
}
if let foundItemHandler = foundItemHandler {
var updateObserver: NSObjectProtocol?
// FIXME: Remove this section as it won't work as expected on iCloud
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryGatheringProgress, object: mdquery, queue: nil, using: { (notification) in
mdquery.disableUpdates()
guard mdquery.resultCount > lastReportedCount else { return }
for index in lastReportedCount..<mdquery.resultCount {
guard let attribs = (mdquery.result(at: index) as? NSMetadataItem)?.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
continue
}
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
continue
}
if let file = self.mapFileObject(attributes: attribs) {
foundItemHandler(file)
}
}
lastReportedCount = mdquery.resultCount
progress.totalUnitCount = Int64(lastReportedCount)
mdquery.enableUpdates()
})
}
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: mdquery, queue: nil, using: { (notification) in
defer {
mdquery.stop()
NotificationCenter.default.removeObserver(finishObserver!)
}
guard let results = mdquery.results as? [NSMetadataItem] else {
return
}
var updateObserver: NSObjectProtocol?
if let foundItemHandler = foundItemHandler {
// FIXME: Remove this section as it won't work as expected on iCloud
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryGatheringProgress, object: mdquery, queue: nil, using: { (notification) in
mdquery.disableUpdates()
var contents = [FileObject]()
for result in results {
guard let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
for index in lastReportedCount..<mdquery.resultCount {
guard let attribs = (mdquery.result(at: index) as? NSMetadataItem)?.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
continue
}
@@ -350,22 +253,58 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
continue
}
if let file = self.mapFileObject(attributes: attribs) {
contents.append(file)
if let file = self.mapFileObject(attributes: attribs), query.evaluate(with: file.mapPredicate()) {
foundItemHandler(file)
}
}
progress.completedUnitCount = Int64(contents.count)
self.dispatch_queue.async {
completionHandler(contents, nil)
}
lastReportedCount = mdquery.resultCount
progress.totalUnitCount = Int64(lastReportedCount)
mdquery.enableUpdates()
})
}
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: mdquery, queue: nil, using: { (notification) in
defer {
mdquery.stop()
finishObserver.flatMap(NotificationCenter.default.removeObserver)
finishObserver = nil
updateObserver.flatMap(NotificationCenter.default.removeObserver)
updateObserver = nil
}
DispatchQueue.main.async {
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
if !mdquery.start() {
self.dispatch_queue.async {
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
}
guard let results = mdquery.results as? [NSMetadataItem] else {
return
}
mdquery.disableUpdates()
var contents = [FileObject]()
for result in results {
guard let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
continue
}
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
continue
}
if let file = self.mapFileObject(attributes: attribs), query.evaluate(with: file.mapPredicate()) {
contents.append(file)
}
}
progress.completedUnitCount = Int64(contents.count)
self.dispatch_queue.async {
completionHandler(contents, nil)
}
})
DispatchQueue.main.async {
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
if !mdquery.start() {
self.dispatch_queue.async {
completionHandler([], self.cocoaError(path, code: .fileReadNoPermission))
}
}
}
@@ -379,55 +318,6 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
}
}
/**
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.
- Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
*/
@discardableResult
open override func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return super.create(folder: folderName, at: atPath, completionHandler: completionHandler)
}
/**
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: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
*/
@discardableResult
open override func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
return super.moveItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler)
}
/**
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: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
*/
@discardableResult
open override func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
return super.copyItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler)
}
/**
Removes the file or directory at the specified path.
@@ -459,14 +349,14 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
@discardableResult
open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
// TODO: Make use of overwrite parameter
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
let progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(localFile, forKey: .fileURLKey)
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
monitorFile(path: toPath, opType: opType, progress: progress)
monitorFile(path: toPath, operation: operation, progress: progress)
operation_queue.addOperation {
let tempFolder: URL
if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) {
@@ -477,21 +367,19 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
let tmpFile = tempFolder.appendingPathComponent(UUID().uuidString)
do {
progress.totalUnitCount = localFile.fileSize
try self.opFileManager.copyItem(at: localFile, to: tmpFile)
let toUrl = self.url(of: toPath)
try self.opFileManager.setUbiquitous(true, itemAt: tmpFile, destinationURL: toUrl)
self.monitorFile(path: toPath, operation: operation, progress: progress)
completionHandler?(nil)
DispatchQueue.main.async(execute: {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
} catch let e {
self.delegateNotify(operation)
} catch {
if self.opFileManager.fileExists(atPath: tmpFile.path) {
try? self.opFileManager.removeItem(at: tmpFile)
}
completionHandler?(e)
DispatchQueue.main.async(execute: {
self.delegate?.fileproviderFailed(self, operation: opType)
})
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
}
return progress
@@ -509,24 +397,16 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
*/
@discardableResult
open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey)
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
monitorFile(path: path, opType: opType, progress: progress)
let operation = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
let progress = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler)
monitorFile(path: path, operation: operation, progress: progress)
do {
try self.opFileManager.startDownloadingUbiquitousItem(at: self.url(of: path))
} catch let e {
completionHandler?(e)
DispatchQueue.main.async(execute: {
self.delegate?.fileproviderFailed(self, operation: opType)
})
} catch {
completionHandler?(error)
self.delegateNotify(operation, error: error)
return nil
}
let _ = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler)
return progress
}
@@ -544,13 +424,8 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
@discardableResult
open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
let operation = FileOperationType.fetch(path: path)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey)
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
monitorFile(path: path, opType: operation, progress: progress)
_ = super.contents(path: path, completionHandler: completionHandler)
let progress = super.contents(path: path, completionHandler: completionHandler)
monitorFile(path: path, operation: operation, progress: progress)
return progress
}
@@ -570,13 +445,8 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
@discardableResult
open override func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
let operation = FileOperationType.fetch(path: path)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey)
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
monitorFile(path: path, opType: operation, progress: progress)
_ = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler)
let progress = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler)
monitorFile(path: path, operation: operation, progress: progress)
return progress
}
@@ -594,12 +464,12 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
@discardableResult
open override func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.fetch(path: path)
let progress = Progress(parent: nil, userInfo: nil)
let progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey)
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
monitorFile(path: path, opType: operation, progress: progress)
monitorFile(path: path, operation: operation, progress: progress)
_ = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler)
return progress
}
@@ -666,13 +536,49 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
return monitors[path] != nil
}
fileprivate func updateQueryTypeKeys(_ queryComponent: NSPredicate) -> NSPredicate {
let mapDict: [String: String] = ["url": NSMetadataItemURLKey, "name": NSMetadataItemFSNameKey, "path": NSMetadataItemPathKey, "filesize": NSMetadataItemFSSizeKey, "modifiedDate": NSMetadataItemFSContentChangeDateKey, "creationDate": NSMetadataItemFSCreationDateKey, "contentType": NSMetadataItemContentTypeKey]
if let cQuery = queryComponent as? NSCompoundPredicate {
let newSub = cQuery.subpredicates.map { updateQueryTypeKeys($0 as! NSPredicate) }
switch cQuery.compoundPredicateType {
case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub)
case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub.first!)
case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub)
}
} else if let cQuery = queryComponent as? NSComparisonPredicate {
var newLeft = cQuery.leftExpression
var newRight = cQuery.rightExpression
if newLeft.expressionType == .keyPath, let newKey = mapDict[newLeft.keyPath] {
newLeft = NSExpression(forKeyPath: newKey)
}
if newRight.expressionType == .keyPath, let newKey = mapDict[newRight.keyPath] {
newRight = NSExpression(forKeyPath: newKey)
}
if newLeft.expressionType == .keyPath, newLeft.keyPath == "type" {
newRight = NSExpression(forConstantValue: newRight.constantValue as? String == "directory" ? "public.directory": "public.data")
}
if newRight.expressionType == .keyPath, newRight.keyPath == "type" {
newLeft = NSExpression(forConstantValue: newLeft.constantValue as? String == "directory" ? "public.directory": "public.data")
}
return NSComparisonPredicate(leftExpression: newLeft, rightExpression: newRight, modifier: cQuery.comparisonPredicateModifier, type: cQuery.predicateOperatorType, options: cQuery.options)
} else {
return queryComponent
}
}
fileprivate func mapFileObject(attributes attribs: [String: Any]) -> FileObject? {
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardizedFileURL, let name = attribs[NSMetadataItemFSNameKey] as? String else {
return nil
}
let path = self.relativePathOf(url: url)
#if swift(>=4.0)
let rpath = path.hasPrefix("/") ? String(path[path.index(after: path.startIndex)...]) : path
#else
let rpath = path.hasPrefix("/") ? path.substring(from: path.index(after: path.startIndex)) : path
#endif
let relativeUrl = URL(string: rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath, relativeTo: self.baseURL)
let file = FileObject(url: relativeUrl ?? url, name: name, path: path)
@@ -686,54 +592,20 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
return file
}
fileprivate func monitorFile(path: String, opType: FileOperationType, progress: Progress?) {
dispatch_queue.async {
let pathURL = self.url(of: path)
let size = pathURL.fileSize
progress?.totalUnitCount = size > 0 ? size : 0
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemPathKey, pathURL.path)
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataUbiquitousItemPercentDownloadedKey, NSMetadataUbiquitousItemPercentUploadedKey, NSMetadataItemFSSizeKey]
query.searchScopes = [self.scope.rawValue]
var updateObserver: NSObjectProtocol?
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: nil, using: { (notification) in
query.disableUpdates()
guard let item = (query.results as? [NSMetadataItem])?.first else {
return
}
if progress?.totalUnitCount == 0, let size = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 {
progress?.totalUnitCount = size
}
let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0
let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0
if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) {
progress?.completedUnitCount = Int64(uploaded / 100 * Double(progress?.totalUnitCount ?? 0))
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(uploaded / 100))
}
} else if (uploaded == 0 || uploaded == 100) && (downloaded > 0 && downloaded < 100) {
progress?.completedUnitCount = Int64(downloaded / 100 * Double(progress?.totalUnitCount ?? 0))
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(downloaded / 100))
}
} else if uploaded == 100 || downloaded == 100 {
progress?.completedUnitCount = progress?.totalUnitCount ?? 0
query.stop()
NotificationCenter.default.removeObserver(updateObserver!)
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
}
}
query.enableUpdates()
})
DispatchQueue.main.async {
progress?.setUserInfoObject(Date(), forKey: .startingTimeKey)
query.start()
}
lazy fileprivate var observer: KVOObserver = KVOObserver()
fileprivate func monitorFile(path: String, operation: FileOperationType, progress: Progress?) {
let pathURL = self.url(of: path).standardizedFileURL
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K LIKE[CD] %@", NSMetadataItemPathKey, pathURL.path)
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataUbiquitousItemPercentDownloadedKey, NSMetadataUbiquitousItemPercentUploadedKey, NSMetadataUbiquitousItemDownloadingStatusKey, NSMetadataItemFSSizeKey]
query.searchScopes = [self.scope.rawValue]
var context = QueryProgressWrapper(provider: self, progress: progress, operation: operation)
query.addObserver(self.observer, forKeyPath: "results", options: [.initial, .new, .old], context: &context)
DispatchQueue.main.async {
query.start()
progress?.setUserInfoObject(Date(), forKey: .startingTimeKey)
}
}
@@ -745,16 +617,13 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
self.dispatch_queue.async {
completionHandler(url, nil, expiration as Date?, nil)
}
} catch let e {
} catch {
self.dispatch_queue.async {
completionHandler(nil, nil, nil, e)
completionHandler(nil, nil, nil, error)
}
}
}
}
}
extension CloudFileProvider {
/// Removes local copy of file, but spares cloud copy.
/// - Parameter path: Path of file or directory to be removed from local
@@ -764,8 +633,8 @@ extension CloudFileProvider {
do {
try self.opFileManager.evictUbiquitousItem(at: self.url(of: path))
completionHandler?(nil)
} catch let e {
completionHandler?(e)
} catch {
completionHandler?(error)
}
}
}
@@ -791,9 +660,13 @@ public enum UbiquitousScope: RawRepresentable {
/// Search all files not in the Documents directories of the apps iCloud container directories.
/// Use this scope to store user-related data files that your app needs to share
/// but that are not files you want the user to manipulate directly.
///
/// Raw value is equivalent to `NSMetadataQueryUbiquitousDataScope`
case data
/// Search all files in the Documents directories of the apps iCloud container directories.
/// Put documents that the user is allowed to access inside a Documents subdirectory.
///
/// Raw value is equivalent to `NSMetadataQueryUbiquitousDocumentsScope`
case documents
public typealias RawValue = String
@@ -819,6 +692,56 @@ public enum UbiquitousScope: RawRepresentable {
}
}
struct QueryProgressWrapper {
weak var provider: CloudFileProvider?
weak var progress: Progress?
let operation: FileOperationType
}
fileprivate class KVOObserver: NSObject {
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let query = object as? NSMetadataQuery else {
return
}
guard let wrapper = context?.load(as: QueryProgressWrapper.self) else {
query.stop()
query.removeObserver(self, forKeyPath: "results")
return
}
let provider = wrapper.provider
let progress = wrapper.progress
let operation = wrapper.operation
guard let results = change?[.newKey], let item = (results as? [NSMetadataItem])?.first else {
return
}
query.disableUpdates()
var size = progress?.totalUnitCount ?? -1
if size < 0, let size_d = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 {
size = size_d
progress?.totalUnitCount = size
}
let downloadStatus = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? String ?? ""
let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0
let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0
if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) {
progress?.completedUnitCount = Int64(uploaded / 100 * Double(size))
provider?.delegateNotify(operation, progress: uploaded / 100)
} else if (uploaded == 0 || uploaded == 100) && downloadStatus != NSMetadataUbiquitousItemDownloadingStatusCurrent {
progress?.completedUnitCount = Int64(downloaded / 100 * Double(size))
provider?.delegateNotify(operation, progress: downloaded / 100)
} else if uploaded == 100 || downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent {
progress?.completedUnitCount = size
query.stop()
query.removeObserver(self, forKeyPath: "results")
provider?.delegateNotify(operation)
}
query.enableUpdates()
}
}
/*
func getMetadataItem(url: URL) -> NSMetadataItem? {
let query = NSMetadataQuery()
+164 -121
View File
@@ -14,9 +14,11 @@ import CoreGraphics
Allows accessing to Dropbox stored files. This provider doesn't cache or save files internally, however you can
set `useCache` and `cache` properties to use Foundation `NSURLCache` system.
- Note: You can pass file id or rev instead of file path, e.g `"id:1234abcd"` or `"rev:1234abcd"`, to point to a file or folder by ID.
- Note: Uploading files and data are limited to 150MB, for now.
*/
open class DropboxFileProvider: HTTPFileProvider {
open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
override open class var type: String { return "Dropbox" }
/// Dropbox RPC API URL, which is equal with [https://api.dropboxapi.com/2/](https://api.dropboxapi.com/2/)
@@ -28,8 +30,7 @@ open class DropboxFileProvider: HTTPFileProvider {
Initializer for Dropbox provider with given client ID and Token.
These parameters 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. Also you can use [auth0/Lock](https://github.com/auth0/Lock.iOS-OSX) which provides graphical user interface.
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.
- Parameter credential: a `URLCredential` object with Client ID set as `user` and Token set as `password`.
- Parameter cache: A URLCache to cache downloaded files and contents.
@@ -42,14 +43,12 @@ open class DropboxFileProvider: HTTPFileProvider {
public required convenience init?(coder aDecoder: NSCoder) {
self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
self.useCache = aDecoder.decodeBool(forKey: "useCache")
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
}
override open func copy(with zone: NSZone? = nil) -> Any {
let copy = DropboxFileProvider(credential: self.credential, cache: self.cache)
copy.currentPath = self.currentPath
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
copy.useCache = self.useCache
@@ -57,27 +56,47 @@ open class DropboxFileProvider: HTTPFileProvider {
return copy
}
open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
let progress = Progress(parent: nil, userInfo: nil)
list(path, progress: progress) { (contents, cursor, error) in
completionHandler(contents, error)
}
/**
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.
- Parameters:
- path: path to target directory. If empty, root will be iterated.
- completionHandler: a closure with result of directory entries or error.
- contents: An array of `FileObject` identifying the the directory entries.
- error: Error returned by system.
*/
open override func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
let query = NSPredicate(format: "TRUEPREDICATE")
_ = searchFiles(path: path, recursive: false, query: query, foundItemHandler: nil, completionHandler: completionHandler)
}
open override func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: 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`.
- Parameters:
- path: path to target directory. If empty, attributes of root will be returned.
- completionHandler: a closure with result of directory entries or error.
- attributes: A `FileObject` containing the attributes of the item.
- error: Error returned by system.
*/
open override func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
let url = URL(string: "files/get_metadata", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
request.set(httpContentType: .json)
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderDropboxError?
var serverError: FileProviderHTTPError?
var fileObject: DropboxFileObject?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
if let json = data?.deserializeJSON(), let file = DropboxFileObject(json: json) {
fileObject = file
}
@@ -87,121 +106,151 @@ open class DropboxFileProvider: HTTPFileProvider {
task.resume()
}
open override func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
/// Returns volume/provider information asynchronously.
/// - Parameter volumeInfo: Information of filesystem/Provider returned by system/server.
open override func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) {
let url = URL(string: "users/get_space_usage", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var totalSize: Int64 = -1
var usedSize: Int64 = 0
if let json = data?.deserializeJSON() {
totalSize = ((json["allocation"] as? NSDictionary)?["allocated"] as? NSNumber)?.int64Value ?? -1
usedSize = (json["used"] as? NSNumber)?.int64Value ?? 0
guard let json = data?.deserializeJSON() else {
completionHandler(nil)
return
}
completionHandler(totalSize, usedSize)
let volume = VolumeObject(allValues: [:])
volume.totalCapacity = ((json["allocation"] as? NSDictionary)?["allocated"] as? NSNumber)?.int64Value ?? -1
volume.usage = (json["used"] as? NSNumber)?.int64Value ?? 0
completionHandler(volume)
})
task.resume()
}
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
let progress = Progress(parent: nil, userInfo: nil)
var foundFiles = [DropboxFileObject]()
if let queryStr = query.findValue(forKey: "name", operator: .beginsWith) as? String {
// Dropbox only support searching for file names begin with query in non-enterprise accounts.
// We will use it if there is a `name BEGINSWITH[c] "query"` in predicate, then filter to form final result.
search(path, query: queryStr, progress: progress, foundItem: { (file) in
if query.evaluate(with: file.mapPredicate()) {
foundFiles.append(file)
foundItemHandler?(file)
}
}, completionHandler: { (error) in
completionHandler(foundFiles, error)
})
/**
Search files inside directory using query asynchronously.
Sample predicates:
```
NSPredicate(format: "(name CONTAINS[c] 'hello') && (filesize >= 10000)")
NSPredicate(format: "(modifiedDate >= %@)", Date())
NSPredicate(format: "(path BEGINSWITH %@)", "folder/child folder")
```
- Note: Don't pass Spotlight predicates to this method directly, use `FileProvider.convertSpotlightPredicateTo()` method to get usable predicate.
- Important: A file name criteria should be provided for Dropbox.
- Parameters:
- path: location of directory to start search
- recursive: Searching subdirectories of path
- query: An `NSPredicate` object with keys like `FileObject` members, except `size` which becomes `filesize`.
- foundItemHandler: Closure which is called when a file is found
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
- files: all files meat the `query` criteria.
- error: `Error` returned by server if occured.
- Returns: An `Progress` to get progress or cancel progress. Use `completedUnitCount` to iterate count of found items.
*/
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
let queryStr: String?
if query.predicateFormat == "TRUEPREDICATE" {
queryStr = nil
} else {
// Dropbox doesn't support searching attributes natively. The workaround is to fallback to listing all files
// and filter it locally. It may have a network burden in case there is many files in Dropbox, so please use it concisely.
list(path, recursive: true, progress: progress, progressHandler: { (files, _, error) in
for file in files where query.evaluate(with: file.mapPredicate()) {
queryStr = query.findValue(forKey: "name", operator: .beginsWith) as? String
}
let requestHandler = self.listRequest(path: path, queryStr: queryStr, recursive: recursive)
let queryIsTruePredicate = query.predicateFormat == "TRUEPREDICATE"
return paginated(path, requestHandler: requestHandler,
pageHandler: { [weak self] (data, progress) -> (files: [FileObject], error: Error?, newToken: String?) in
guard let json = data?.deserializeJSON(), let entries = (json["entries"] ?? json["matches"]) as? [AnyObject] else {
let err = self?.urlError(path, code: .badServerResponse)
return ([], err, nil)
}
var files = [FileObject]()
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry), queryIsTruePredicate || query.evaluate(with: file.mapPredicate()) {
files.append(file)
progress.completedUnitCount += 1
foundItemHandler?(file)
}
}, completionHandler: { (files, _, error) in
let predicatedFiles = files.filter { query.evaluate(with: $0.mapPredicate()) }
completionHandler(predicatedFiles, error)
})
}
return progress
}
let ncursor: String?
if let hasmore = (json["has_more"] as? NSNumber)?.boolValue, hasmore {
ncursor = json["cursor"] as? String
} else if let hasmore = (json["more"] as? NSNumber)?.boolValue, hasmore {
ncursor = (json["start"] as? Int).flatMap(String.init)
} else {
ncursor = nil
}
return (files, nil, ncursor)
}, completionHandler: completionHandler)
}
open override func isReachable(completionHandler: @escaping (Bool) -> Void) {
self.storageProperties { total, _ in
completionHandler(total > 0)
}
}
override func request(for operation: FileOperationType, overwrite: Bool, attributes: [URLResourceKey : Any]) -> URLRequest {
// content operations
var request: URLRequest
switch operation {
case .copy(source: let source, destination: let dest) where dest.lowercased().hasPrefix("file://"):
let url = URL(string: "files/download", relativeTo: contentURL)!
request = URLRequest(url: url)
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(dropboxArgKey: ["path": correctPath(source)! as NSString])
case .fetch(let path):
let url = URL(string: "files/download", relativeTo: contentURL)!
request = URLRequest(url: url)
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(dropboxArgKey: ["path": correctPath(path)! as NSString])
case .copy(source: let source, destination: let dest) where source.lowercased().hasPrefix("file://"):
var requestDictionary = [String: AnyObject]()
let url: URL = URL(string: "files/upload", relativeTo: contentURL)!
requestDictionary["path"] = correctPath(dest) as NSString?
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
requestDictionary["client_modified"] = (attributes[.contentModificationDateKey] as? Date)?.rfc3339utc() as NSString?
request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .stream)
request.set(dropboxArgKey: requestDictionary)
case .modify(let path):
override func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey : Any] = [:]) -> URLRequest {
func uploadRequest(to path: String) -> URLRequest {
var requestDictionary = [String: AnyObject]()
let url: URL = URL(string: "files/upload", relativeTo: contentURL)!
requestDictionary["path"] = correctPath(path) as NSString?
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
requestDictionary["client_modified"] = (attributes[.contentModificationDateKey] as? Date)?.rfc3339utc() as NSString?
request = URLRequest(url: url)
requestDictionary["client_modified"] = (attributes[.contentModificationDateKey] as? Date)?.format(with: .rfc3339) as NSString?
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .stream)
request.set(httpContentType: .stream)
request.set(dropboxArgKey: requestDictionary)
default: // modify, link, fetch
return self.apiRequest(for: operation)
return request
}
func downloadRequest(from path: String) -> URLRequest {
let url = URL(string: "files/download", relativeTo: contentURL)!
var request = URLRequest(url: url)
request = URLRequest(url: url)
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(dropboxArgKey: ["path": correctPath(path)! as NSString])
return request
}
// content operations
switch operation {
case .copy(source: let source, destination: let dest) where dest.lowercased().hasPrefix("file://"):
return downloadRequest(from: source)
case .fetch(let path):
return downloadRequest(from: path)
case .copy(source: let source, destination: let dest) where source.lowercased().hasPrefix("file://"):
return uploadRequest(to: dest)
case .modify(let path):
return uploadRequest(to: path)
default:
return self.apiRequest(for: operation, overwrite: overwrite)
}
return request
}
func apiRequest(for operation: FileOperationType) -> URLRequest {
func apiRequest(for operation: FileOperationType, overwrite: Bool = false) -> URLRequest {
let url: String
let sourcePath = operation.source
let destPath = operation.destination
var requestDictionary = [String: AnyObject]()
switch operation {
case .create:
url = "files/create_folder"
url = "files/create_folder_v2"
case .copy:
url = "files/copy"
url = "files/copy_v2"
requestDictionary["allow_shared_folder"] = NSNumber(value: true)
case .move:
url = "files/move"
url = "files/move_v2"
requestDictionary["allow_shared_folder"] = NSNumber(value: true)
case .remove:
url = "files/delete"
url = "files/delete_v2"
default: // modify, link, fetch
fatalError("Unimplemented operation \(operation.description) in \(#file)")
}
var request = URLRequest(url: URL(string: url, relativeTo: apiURL)!)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
var requestDictionary = [String: AnyObject]()
request.set(httpContentType: .json)
if let dest = correctPath(destPath) as NSString? {
requestDictionary["from_path"] = correctPath(sourcePath) as NSString?
requestDictionary["to_path"] = dest
@@ -213,23 +262,19 @@ open class DropboxFileProvider: HTTPFileProvider {
}
override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
return FileProviderDropboxError(code: code, path: path ?? "", errorDescription: data.flatMap({ String(data: $0, encoding: .utf8) }))
let errorDesc: String?
if let response = data?.deserializeJSON() {
errorDesc = (response["user_message"] as? String) ?? (response["error"]?["tag"] as? String)
} else {
errorDesc = data.flatMap({ String(data: $0, encoding: .utf8) })
}
return FileProviderDropboxError(code: code, path: path ?? "", errorDescription: errorDesc)
}
override func upload_simple(_ targetPath: String, request: URLRequest, data: Data?, localFile: URL?, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
let size = data?.count ?? Int((try? localFile?.resourceValues(forKeys: [.fileSizeKey]))??.fileSize ?? -1)
if size > 150 * 1024 * 1024 {
let error = FileProviderDropboxError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
completionHandler?(error)
self.delegateNotify(operation, error: error)
return nil
}
return super.upload_simple(targetPath, request: request, data: data, localFile: localFile, operation: operation, completionHandler: completionHandler)
override var maxUploadSimpleSupported: Int64 {
return 157_286_400 // 150MB
}
}
extension DropboxFileProvider {
/*
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
/* There is two ways to monitor folders changing in Dropbox. Either using webooks
@@ -245,24 +290,22 @@ extension DropboxFileProvider {
}
*/
// TODO: Implement /get_account & /get_current_account
}
extension DropboxFileProvider: FileProviderSharing {
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
let url = URL(string: "files/get_temporary_link", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
request.set(httpContentType: .json)
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderDropboxError?
var serverError: FileProviderHTTPError?
var link: URL?
var fileObject: DropboxFileObject?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
if let json = data?.deserializeJSON() {
if let linkStr = json["link"] as? String {
link = URL(string: linkStr)
@@ -292,23 +335,23 @@ extension DropboxFileProvider: FileProviderSharing {
*/
open func copyItem(remoteURL: URL, to toPath: String, completionHandler: @escaping ((_ jobId: String?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
if remoteURL.isFileURL {
completionHandler(nil, nil, self.throwError(remoteURL.path, code: URLError.badURL))
completionHandler(nil, nil, self.urlError(remoteURL.path, code: .badURL))
return
}
let url = URL(string: "files/save_url", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
request.set(httpContentType: .json)
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "url" : remoteURL.absoluteString as NSString]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderDropboxError?
var serverError: FileProviderHTTPError?
var jobId: String?
var fileObject: DropboxFileObject?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderDropboxError(code: code!, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
serverError = code.flatMap { self.serverError(with: $0, path: toPath, data: data) }
if let json = data?.deserializeJSON() {
jobId = json["async_job_id"] as? String
if let attribDic = json["metadata"] as? [String: AnyObject] {
@@ -334,14 +377,14 @@ extension DropboxFileProvider: FileProviderSharing {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
request.set(httpContentType: .json)
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "copy_reference" : reference as NSString]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderDropboxError?
var serverError: FileProviderHTTPError?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderDropboxError(code: code!, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
serverError = code.flatMap { self.serverError(with: $0, path: toPath, data: data) }
}
completionHandler?(serverError ?? error)
})
@@ -417,7 +460,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
var image: ImageClass? = nil
if let r = response as? HTTPURLResponse, let result = r.allHeaderFields["Dropbox-API-Result"] as? String, let jsonResult = result.deserializeJSON() {
if jsonResult["error"] != nil {
completionHandler(nil, self.throwError(path, code: URLError.cannotDecodeRawData as FoundationErrorEnum))
completionHandler(nil, self.urlError(path, code: .cannotDecodeRawData))
}
}
if let data = data {
@@ -443,16 +486,16 @@ extension DropboxFileProvider: ExtendedFileProvider {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
request.set(httpContentType: .json)
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderDropboxError?
var serverError: FileProviderHTTPError?
var dic = [String: Any]()
var keys = [String]()
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
if let json = data?.deserializeJSON(), let properties = (json["media_info"] as? [String: Any])?["metadata"] as? [String: Any] {
(dic, keys) = self.mapMediaInfo(properties)
}
+48 -97
View File
@@ -23,12 +23,16 @@ public final class DropboxFileObject: FileObject {
}
internal init? (json: [String: AnyObject]) {
var json = json
if json["name"] == nil, let metadata = json["metadata"] as? [String: AnyObject] {
json = metadata
}
guard let name = json["name"] as? String else { return nil }
guard let path = json["path_display"] as? String else { return nil }
super.init(url: nil, name: name, path: path)
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
self.serverTime = Date(rfcString: json["server_modified"] as? String ?? "")
self.modifiedDate = Date(rfcString: json["client_modified"] as? String ?? "")
self.serverTime = (json["server_modified"] as? String).flatMap(Date.init(rfcString:))
self.modifiedDate = (json["client_modified"] as? String).flatMap(Date.init(rfcString:))
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
@@ -47,13 +51,12 @@ public final class DropboxFileObject: FileObject {
/// The document identifier is a value assigned by the Dropbox to a file.
/// This value is used to identify the document regardless of where it is moved on a volume.
/// The identifier persists across system restarts.
open internal(set) var id: String? {
get {
return allValues[.documentIdentifierKey] as? String
return allValues[.fileResourceIdentifierKey] as? String
}
set {
allValues[.documentIdentifierKey] = newValue
allValues[.fileResourceIdentifierKey] = newValue
}
}
@@ -69,109 +72,57 @@ public final class DropboxFileObject: FileObject {
}
}
// codebeat:disable[ARITY]
internal extension DropboxFileProvider {
func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, session: URLSession? = nil, progress: Progress, progressHandler: ((_ contents: [FileObject], _ nextCursor: String?, _ error: Error?) -> Void)? = nil, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
if progress.isCancelled { return }
var requestDictionary = [String: AnyObject]()
let url: URL
if let cursor = cursor {
url = URL(string: "files/list_folder/continue", relativeTo: apiURL)!
requestDictionary["cursor"] = cursor as NSString?
} else {
url = URL(string: "files/list_folder", relativeTo: apiURL)!
requestDictionary["path"] = correctPath(path) as NSString?
requestDictionary["recursive"] = recursive as NSNumber?
internal func correctPath(_ path: String?) -> String? {
guard let path = path else { return nil }
if path.hasPrefix("id:") || path.hasPrefix("rev:") {
return path
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = (session ?? self.session).dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderDropboxError?
var files = [DropboxFileObject]()
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderDropboxError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
if let json = data?.deserializeJSON() {
if let entries = json["entries"] as? [AnyObject] , entries.count > 0 {
files.reserveCapacity(entries.count)
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
files.append(file)
progress.totalUnitCount = Int64(files.count)
}
}
let ncursor = json["cursor"] as? String
let hasmore = (json["has_more"] as? NSNumber)?.boolValue ?? false
if hasmore && !progress.isCancelled {
progressHandler?(files, ncursor, responseError ?? error)
self.list(path, cursor: ncursor, prevContents: prevContents + files, progress: progress, completionHandler: completionHandler)
return
}
}
}
progressHandler?(files, nil, responseError ?? error)
completionHandler(prevContents + files, nil, responseError ?? error)
})
progress.cancellationHandler = { [weak task] in
task?.cancel()
var p = path.hasPrefix("/") ? path : "/" + path
if p.hasSuffix("/") {
p.remove(at: p.index(before:p.endIndex))
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.taskDescription = FileOperationType.fetch(path: path).json
task.resume()
return p
}
func search(_ startPath: String = "", query: String, start: Int = 0, maxResultPerPage: Int = 25, maxResults: Int = -1, progress: Progress, foundItem:@escaping ((_ file: DropboxFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
if progress.isCancelled { return }
let url = URL(string: "files/search", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
var requestDictionary: [String: AnyObject] = ["path": startPath as NSString]
requestDictionary["query"] = query as NSString
requestDictionary["start"] = start as NSNumber
requestDictionary["max_results"] = maxResultPerPage as NSNumber
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderDropboxError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderDropboxError(code: rCode, path: startPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
internal func listRequest(path: String, queryStr: String? = nil, recursive: Bool = false) -> ((_ token: String?) -> URLRequest?) {
if let queryStr = queryStr {
return { [weak self] (token) -> URLRequest? in
guard let `self` = self else { return nil }
let url = URL(string: "files/search", relativeTo: self.apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: self.credential, with: .oAuth2)
request.set(httpContentType: .json)
var requestDictionary: [String: AnyObject] = ["path": self.correctPath(path) as NSString!]
requestDictionary["query"] = queryStr as NSString
requestDictionary["start"] = NSNumber(value: (token.flatMap( { Int($0) } ) ?? 0))
request.httpBody = Data(jsonDictionary: requestDictionary)
return request
}
if let json = data?.deserializeJSON() {
if let entries = json["matches"] as? [AnyObject] , entries.count > 0 {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
foundItem(file)
progress.completedUnitCount += 1
}
}
let rstart = json["start"] as? Int
let hasmore = (json["more"] as? NSNumber)?.boolValue ?? false
if hasmore && !progress.isCancelled, let rstart = rstart {
self.search(startPath, query: query, start: rstart + entries.count, maxResultPerPage: maxResultPerPage, progress: progress, foundItem: foundItem, completionHandler: completionHandler)
} else {
completionHandler(responseError ?? error)
}
return
} else {
return { [weak self] (token) -> URLRequest? in
guard let `self` = self else { return nil }
var requestDictionary = [String: AnyObject]()
let url: URL
if let token = token {
url = URL(string: "files/list_folder/continue", relativeTo: self.apiURL)!
requestDictionary["cursor"] = token as NSString?
} else {
url = URL(string: "files/list_folder", relativeTo: self.apiURL)!
requestDictionary["path"] = self.correctPath(path) as NSString?
requestDictionary["recursive"] = NSNumber(value: recursive)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: self.credential, with: .oAuth2)
request.set(httpContentType: .json)
request.httpBody = Data(jsonDictionary: requestDictionary)
return request
}
completionHandler(responseError ?? error)
})
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
}
}
// codebeat:enable[ARITY]
internal extension DropboxFileProvider {
static let dateFormatter = DateFormatter()
+38 -45
View File
@@ -14,17 +14,17 @@ import AVFoundation
extension LocalFileProvider: ExtendedFileProvider {
open func thumbnailOfFileSupported(path: String) -> Bool {
switch (path as NSString).pathExtension.lowercased() {
case LocalFileInformationGenerator.imageThumbnailExtensions:
case LocalFileInformationGenerator.imageThumbnailExtensions.contains:
return true
case LocalFileInformationGenerator.audioThumbnailExtensions:
case LocalFileInformationGenerator.audioThumbnailExtensions.contains:
return true
case LocalFileInformationGenerator.videoThumbnailExtensions:
case LocalFileInformationGenerator.videoThumbnailExtensions.contains:
return true
case LocalFileInformationGenerator.pdfThumbnailExtensions:
case LocalFileInformationGenerator.pdfThumbnailExtensions.contains:
return true
case LocalFileInformationGenerator.officeThumbnailExtensions:
case LocalFileInformationGenerator.officeThumbnailExtensions.contains:
return true
case LocalFileInformationGenerator.customThumbnailExtensions:
case LocalFileInformationGenerator.customThumbnailExtensions.contains:
return true
default:
return false
@@ -34,19 +34,19 @@ extension LocalFileProvider: ExtendedFileProvider {
open func propertiesOfFileSupported(path: String) -> Bool {
let fileExt = (path as NSString).pathExtension.lowercased()
switch fileExt {
case LocalFileInformationGenerator.imagePropertiesExtensions:
case LocalFileInformationGenerator.imagePropertiesExtensions.contains:
return LocalFileInformationGenerator.imageProperties != nil
case LocalFileInformationGenerator.audioPropertiesExtensions:
case LocalFileInformationGenerator.audioPropertiesExtensions.contains:
return LocalFileInformationGenerator.audioProperties != nil
case LocalFileInformationGenerator.videoPropertiesExtensions:
case LocalFileInformationGenerator.videoPropertiesExtensions.contains:
return LocalFileInformationGenerator.videoProperties != nil
case LocalFileInformationGenerator.pdfPropertiesExtensions:
case LocalFileInformationGenerator.pdfPropertiesExtensions.contains:
return LocalFileInformationGenerator.pdfProperties != nil
case LocalFileInformationGenerator.archivePropertiesExtensions:
case LocalFileInformationGenerator.archivePropertiesExtensions.contains:
return LocalFileInformationGenerator.archiveProperties != nil
case LocalFileInformationGenerator.officePropertiesExtensions:
case LocalFileInformationGenerator.officePropertiesExtensions.contains:
return LocalFileInformationGenerator.officeProperties != nil
case LocalFileInformationGenerator.customPropertiesExtensions:
case LocalFileInformationGenerator.customPropertiesExtensions.contains:
return LocalFileInformationGenerator.customProperties != nil
default:
@@ -62,17 +62,17 @@ extension LocalFileProvider: ExtendedFileProvider {
let fileURL = self.url(of: path)
// Create Thumbnail and cache
switch fileURL.pathExtension.lowercased() {
case LocalFileInformationGenerator.videoThumbnailExtensions:
case LocalFileInformationGenerator.videoThumbnailExtensions.contains:
thumbnailImage = LocalFileInformationGenerator.videoThumbnail(fileURL)
case LocalFileInformationGenerator.audioThumbnailExtensions:
case LocalFileInformationGenerator.audioThumbnailExtensions.contains:
thumbnailImage = LocalFileInformationGenerator.audioThumbnail(fileURL)
case LocalFileInformationGenerator.imageThumbnailExtensions:
case LocalFileInformationGenerator.imageThumbnailExtensions.contains:
thumbnailImage = LocalFileInformationGenerator.imageThumbnail(fileURL)
case LocalFileInformationGenerator.pdfThumbnailExtensions:
case LocalFileInformationGenerator.pdfThumbnailExtensions.contains:
thumbnailImage = LocalFileInformationGenerator.pdfThumbnail(fileURL)
case LocalFileInformationGenerator.officeThumbnailExtensions:
case LocalFileInformationGenerator.officeThumbnailExtensions.contains:
thumbnailImage = LocalFileInformationGenerator.officeThumbnail(fileURL)
case LocalFileInformationGenerator.customThumbnailExtensions:
case LocalFileInformationGenerator.customThumbnailExtensions.contains:
thumbnailImage = LocalFileInformationGenerator.customThumbnail(fileURL)
default:
completionHandler(nil, nil)
@@ -91,19 +91,19 @@ extension LocalFileProvider: ExtendedFileProvider {
let fileExt = (path as NSString).pathExtension.lowercased()
var getter: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))?
switch fileExt {
case LocalFileInformationGenerator.imagePropertiesExtensions:
case LocalFileInformationGenerator.imagePropertiesExtensions.contains:
getter = LocalFileInformationGenerator.imageProperties
case LocalFileInformationGenerator.audioPropertiesExtensions:
case LocalFileInformationGenerator.audioPropertiesExtensions.contains:
getter = LocalFileInformationGenerator.audioProperties
case LocalFileInformationGenerator.videoPropertiesExtensions:
case LocalFileInformationGenerator.videoPropertiesExtensions.contains:
getter = LocalFileInformationGenerator.videoProperties
case LocalFileInformationGenerator.pdfPropertiesExtensions:
case LocalFileInformationGenerator.pdfPropertiesExtensions.contains:
getter = LocalFileInformationGenerator.pdfProperties
case LocalFileInformationGenerator.archivePropertiesExtensions:
case LocalFileInformationGenerator.archivePropertiesExtensions.contains:
getter = LocalFileInformationGenerator.archiveProperties
case LocalFileInformationGenerator.officePropertiesExtensions:
case LocalFileInformationGenerator.officePropertiesExtensions.contains:
getter = LocalFileInformationGenerator.officeProperties
case LocalFileInformationGenerator.customPropertiesExtensions:
case LocalFileInformationGenerator.customPropertiesExtensions.contains:
getter = LocalFileInformationGenerator.customProperties
default:
break
@@ -125,18 +125,18 @@ public struct LocalFileInformationGenerator {
/// Image extensions supportes for thumbnail.
///
/// Default: `["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]`
static public var imageThumbnailExtensions: [String] = ["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]
static public var imageThumbnailExtensions: [String] = ["heic", "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]
/// Audio and music extensions supportes for thumbnail.
///
/// Default: `["mp3", "aac", "m4a"]`
static public var audioThumbnailExtensions: [String] = ["mp3", "aac", "m4a"]
/// Default: `["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]`
static public var audioThumbnailExtensions: [String] = ["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]
/// Video extensions supportes for thumbnail.
///
/// Default: `["mov", "mp4", "m4v", "mpg", "mpeg"]`
static public var videoThumbnailExtensions: [String] = ["mov", "mp4", "m4v", "mpg", "mpeg"]
/// Default: `["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]`
static public var videoThumbnailExtensions: [String] = ["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]
/// Portable document file extensions supportes for thumbnail.
///
/// Default: `["pdf"]`
@@ -156,17 +156,17 @@ public struct LocalFileInformationGenerator {
/// Image extensions supportes for properties.
///
/// Default: `["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff"]`
static public var imagePropertiesExtensions: [String] = ["jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff"]
static public var imagePropertiesExtensions: [String] = ["heic", "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff"]
/// Audio and music extensions supportes for properties.
///
/// Default: `["mp3", "aac", "m4a", "caf"]`
static public var audioPropertiesExtensions: [String] = ["mp3", "aac", "m4a", "caf"]
/// Default: `["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]`
static public var audioPropertiesExtensions: [String] = ["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]
/// Video extensions supportes for properties.
///
/// Default: `["mp4", "mpg", "3gp", "mov", "avi"]`
static public var videoPropertiesExtensions: [String] = ["mp4", "mpg", "3gp", "mov", "avi"]
/// Default: `["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]`
static public var videoPropertiesExtensions: [String] = ["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]
/// Portable document file extensions supportes for properties.
///
@@ -442,10 +442,7 @@ public struct LocalFileInformationGenerator {
func convertDate(_ date: String?) -> Date? {
guard let date = date else { return nil }
var dateStr = date.replacingOccurrences(of: "'", with: "")
if dateStr.hasPrefix("D:") {
dateStr.characters.removeFirst(2)
}
let dateStr = date.replacingOccurrences(of: "'", with: "").replacingOccurrences(of: "D:", with: "", options: .anchored)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMddHHmmssTZ"
if let result = dateFormatter.date(from: dateStr) {
@@ -507,7 +504,3 @@ public struct LocalFileInformationGenerator {
/// - Note: No default implementation is avaiable
static public var customProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
}
fileprivate func ~=<T : Equatable>(array: [T], value: T) -> Bool {
return array.contains(value)
}
+502
View File
@@ -0,0 +1,502 @@
//
// FileProviderExtensions.swift
// FileProvider
//
// Created by Amir Abbas on 12/27/1395 AP.
//
//
import Foundation
public extension Array where Element: FileObject {
/// Returns a sorted array of `FileObject`s by criterias set in attributes.
public func sort(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 attributes.
public mutating func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) {
self = self.sort(by: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
}
}
public extension Sequence where Iterator.Element == UInt8 {
func hexString() -> String {
return self.map{String(format: "%02X", $0)}.joined()
}
}
public extension URLFileResourceType {
/// **FileProvider** returns corresponding `URLFileResourceType` of a `FileAttributeType` value
public init(fileTypeValue: FileAttributeType) {
switch fileTypeValue {
case FileAttributeType.typeCharacterSpecial: self = .characterSpecial
case FileAttributeType.typeDirectory: self = .directory
case FileAttributeType.typeBlockSpecial: self = .blockSpecial
case FileAttributeType.typeRegular: self = .regular
case FileAttributeType.typeSymbolicLink: self = .symbolicLink
case FileAttributeType.typeSocket: self = .socket
case FileAttributeType.typeUnknown: self = .unknown
default: self = .unknown
}
}
}
public extension URLResourceKey {
/// **FileProvider** returns url of file object.
public static let fileURLKey = URLResourceKey(rawValue: "NSURLFileURLKey")
/// **FileProvider** returns modification date of file in server
public static let serverDateKey = URLResourceKey(rawValue: "NSURLServerDateKey")
/// **FileProvider** returns HTTP ETag string of remote resource
public static let entryTagKey = URLResourceKey(rawValue: "NSURLEntryTagKey")
/// **FileProvider** returns MIME type of file, if returned by server
public static let mimeTypeKey = URLResourceKey(rawValue: "NSURLMIMETypeIdentifierKey")
/// **FileProvider** returns either file is encrypted or not
public static let isEncryptedKey = URLResourceKey(rawValue: "NSURLIsEncryptedKey")
/// **FileProvider** count of items in directory
public static let childrensCount = URLResourceKey(rawValue: "MFPURLChildrensCount")
}
public extension ProgressUserInfoKey {
/// **FileProvider** returns associated `FileProviderOperationType`
public static let fileProvderOperationTypeKey = ProgressUserInfoKey("MFPOperationTypeKey")
/// **FileProvider** returns start date/time of operation
public static let startingTimeKey = ProgressUserInfoKey("NSProgressStartingTimeKey")
}
internal extension URL {
var uw_scheme: String {
return self.scheme ?? ""
}
var fileIsDirectory: Bool {
return (try? self.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
}
var fileSize: Int64 {
return Int64((try? self.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1)
}
var fileExists: Bool {
return self.isFileURL && FileManager.default.fileExists(atPath: self.path)
}
}
public extension URLRequest {
/// Defines HTTP Authentication method required to access
public enum AuthenticationType {
/// Basic method for authentication
case basic
/// Digest method for authentication
case digest
/// OAuth 1.0 method for authentication (OAuth)
case oAuth1
/// OAuth 2.0 method for authentication (Bearer)
case oAuth2
}
}
struct Quality<T> {
let value: T
let quality: Float
var stringifed: String {
var representaion: String = String(describing: value)
let quality: Float = min(1, max(self.quality, 0))
if let value = value as? Locale {
representaion = value.identifier.replacingOccurrences(of: "_", with: "-")
}
if let value = value as? String.Encoding {
let cfEncoding = CFStringConvertNSStringEncodingToEncoding(value.rawValue)
representaion = CFStringConvertEncodingToIANACharSetName(cfEncoding) as String? ?? "*"
}
let qualityDesc = String(format: "%.1f", quality)
return "\(representaion); q=\(qualityDesc)"
}
}
internal extension URLRequest {
mutating func set(httpAuthentication credential: URLCredential?, with type: AuthenticationType) {
func base64(_ str: String) -> String {
let plainData = str.data(using: .utf8)
let base64String = plainData!.base64EncodedString(options: [])
return base64String
}
guard let credential = credential else { return }
switch type {
case .basic:
let user = credential.user?.replacingOccurrences(of: ":", with: "") ?? ""
let pass = credential.password ?? ""
let authStr = "\(user):\(pass)"
if let base64Auth = authStr.data(using: .utf8)?.base64EncodedString() {
self.setValue("Basic \(base64Auth)", forHTTPHeaderField: "Authorization")
}
case .digest:
// handled by RemoteSessionDelegate
break
case .oAuth1:
if let oauth = credential.password {
self.setValue("OAuth \(oauth)", forHTTPHeaderField: "Authorization")
}
case .oAuth2:
if let bearer = credential.password {
self.setValue("Bearer \(bearer)", forHTTPHeaderField: "Authorization")
}
}
}
mutating func set(httpAcceptCharset acceptCharset: String.Encoding) {
let cfEncoding = CFStringConvertNSStringEncodingToEncoding(acceptCharset.rawValue)
if let charsetString = CFStringConvertEncodingToIANACharSetName(cfEncoding) as String? {
self.addValue(charsetString, forHTTPHeaderField: "Accept-Charset")
}
}
mutating func set(httpAcceptCharset acceptCharset: Quality<String.Encoding>) {
self.addValue(acceptCharset.stringifed, forHTTPHeaderField: "Accept-Charset")
}
mutating func set(httpAcceptCharsets acceptCharsets: [String.Encoding]) {
self.setValue(nil, forHTTPHeaderField: "Accept-Charset")
for charset in acceptCharsets {
let cfEncoding = CFStringConvertNSStringEncodingToEncoding(charset.rawValue)
if let charsetString = CFStringConvertEncodingToIANACharSetName(cfEncoding) as String? {
self.addValue(charsetString, forHTTPHeaderField: "Accept-Charset")
}
}
}
mutating func set(httpAcceptCharsets acceptCharsets: [Quality<String.Encoding>]) {
self.setValue(nil, forHTTPHeaderField: "Accept-Charset")
for charset in acceptCharsets.sorted(by: { $0.quality > $1.quality }) {
self.addValue(charset.stringifed, forHTTPHeaderField: "Accept-Charset")
}
}
enum Encoding: String {
case all = "*"
case identity
case gzip
case deflate
}
mutating func set(httpAcceptEncoding acceptEncoding: Encoding) {
self.addValue(acceptEncoding.rawValue, forHTTPHeaderField: "Accept-Encoding")
}
mutating func set(httpAcceptEncoding acceptEncoding: Quality<Encoding>) {
self.addValue(acceptEncoding.stringifed, forHTTPHeaderField: "Accept-Encoding")
}
mutating func set(httpAcceptEncodings acceptEncodings: [Encoding]) {
self.setValue(nil, forHTTPHeaderField: "Accept-Encoding")
for encoding in acceptEncodings {
self.addValue(encoding.rawValue, forHTTPHeaderField: "Accept-Encoding")
}
}
mutating func set(httpAcceptEncodings acceptEncodings: [Quality<Encoding>]) {
self.setValue(nil, forHTTPHeaderField: "Accept-Encoding")
for encoding in acceptEncodings.sorted(by: { $0.quality > $1.quality }) {
self.addValue(encoding.stringifed, forHTTPHeaderField: "Accept-Encoding")
}
}
mutating func set(httpAcceptLanguage acceptLanguage: Locale) {
let langCode = acceptLanguage.identifier.replacingOccurrences(of: "_", with: "-")
self.addValue(langCode, forHTTPHeaderField: "Accept-Language")
}
mutating func set(httpAcceptLanguage acceptLanguage: Quality<Locale>) {
self.addValue(acceptLanguage.stringifed, forHTTPHeaderField: "Accept-Language")
}
mutating func set(httpAcceptLanguages acceptLanguages: [Locale]) {
self.setValue(nil, forHTTPHeaderField: "Accept-Language")
for lang in acceptLanguages {
let langCode = lang.identifier.replacingOccurrences(of: "_", with: "-")
self.addValue(langCode, forHTTPHeaderField: "Accept-Language")
}
}
mutating func set(httpAcceptLanguages acceptLanguages: [Quality<Locale>]) {
self.setValue(nil, forHTTPHeaderField: "Accept-Language")
for lang in acceptLanguages.sorted(by: { $0.quality > $1.quality} ) {
self.addValue(lang.stringifed, forHTTPHeaderField: "Accept-Language")
}
}
mutating func set(httpRangeWithOffset offset: Int64, length: Int) {
if length > 0 {
self.setValue("bytes=\(offset)-\(offset + Int64(length) - 1)", forHTTPHeaderField: "Range")
} else if offset > 0 && length < 0 {
self.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
}
}
mutating func set(httpRange range: Range<Int>) {
let range = max(0, range.lowerBound)..<range.upperBound
if range.upperBound < Int.max && range.count > 0 {
self.setValue("bytes=\(range.lowerBound)-\(range.upperBound - 1)", forHTTPHeaderField: "Range")
} else if range.lowerBound > 0 {
self.setValue("bytes=\(range.lowerBound)-", forHTTPHeaderField: "Range")
}
}
struct ContentMIMEType: RawRepresentable {
public var rawValue: String
public typealias RawValue = String
public init(rawValue: String) {
self.rawValue = rawValue
}
static let javascript = ContentMIMEType(rawValue: "application/javascript")
static let json = ContentMIMEType(rawValue: "application/json")
static let pdf = ContentMIMEType(rawValue: "application/pdf")
static let stream = ContentMIMEType(rawValue: "application/octet-stream")
static let zip = ContentMIMEType(rawValue: "application/zip")
// Texts
static let css = ContentMIMEType(rawValue: "text/css")
static let html = ContentMIMEType(rawValue: "text/html")
static let plainText = ContentMIMEType(rawValue: "text/plain")
static let xml = ContentMIMEType(rawValue: "text/xml")
// Images
static let gif = ContentMIMEType(rawValue: "image/gif")
static let jpeg = ContentMIMEType(rawValue: "image/jpeg")
static let png = ContentMIMEType(rawValue: "image/png")
}
mutating func set(httpContentType contentType: ContentMIMEType, charset: String.Encoding? = nil) {
var parameter = ""
if let charset = charset {
let cfEncoding = CFStringConvertNSStringEncodingToEncoding(charset.rawValue)
if let charsetString = CFStringConvertEncodingToIANACharSetName(cfEncoding) as String? {
parameter = ";charset=" + charsetString
}
}
self.setValue(contentType.rawValue + parameter, forHTTPHeaderField: "Content-Type")
}
mutating func set(dropboxArgKey requestDictionary: [String: AnyObject]) {
guard let jsonData = try? JSONSerialization.data(withJSONObject: requestDictionary, options: []) else {
return
}
guard var jsonString = String(data: jsonData, encoding: .utf8) else { return }
jsonString = jsonString.asciiEscaped().replacingOccurrences(of: "\\/", with: "/")
self.setValue(jsonString, forHTTPHeaderField: "Dropbox-API-Arg")
}
}
internal extension CharacterSet {
static let filePathAllowed = CharacterSet.urlPathAllowed.subtracting(CharacterSet(charactersIn: ":"))
}
internal extension Data {
internal var isPDF: Bool {
return self.count > 4 && self.scanString(length: 4, using: .ascii) == "%PDF"
}
init? (jsonDictionary dictionary: [String: AnyObject]) {
guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else {
return nil
}
self = data
}
func deserializeJSON() -> [String: AnyObject]? {
if let dic = try? JSONSerialization.jsonObject(with: self, options: []) as? [String: AnyObject] {
return dic
}
return nil
}
init<T>(value: T) {
var value = value
self = Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
}
func scanValue<T>() -> T? {
guard MemoryLayout<T>.size <= self.count else { return nil }
return self.withUnsafeBytes { $0.pointee }
}
func scanValue<T>(start: Int) -> T? {
let length = MemoryLayout<T>.size
guard self.count >= start + length else { return nil }
return self.subdata(in: start..<start+length).withUnsafeBytes { $0.pointee }
}
func scanString(start: Int = 0, length: Int, using encoding: String.Encoding = .utf8) -> String? {
guard self.count >= start + length else { return nil }
return String(data: self.subdata(in: start..<start+length), encoding: encoding)
}
static func mapMemory<T, U>(from: T) -> U? {
guard MemoryLayout<T>.size >= MemoryLayout<U>.size else { return nil }
let data = Data(value: from)
return data.scanValue()
}
}
internal extension String {
init? (jsonDictionary: [String: AnyObject]) {
guard let data = Data(jsonDictionary: jsonDictionary) else {
return nil
}
self.init(data: data, encoding: .utf8)
}
func deserializeJSON(using encoding: String.Encoding = .utf8) -> [String: AnyObject]? {
guard let data = self.data(using: encoding) else {
return nil
}
return data.deserializeJSON()
}
func asciiEscaped() -> String {
var res = ""
for char in self.unicodeScalars {
let substring = String(char)
if substring.canBeConverted(to: .ascii) {
res.append(substring)
} else {
res = res.appendingFormat("\\u%04x", char.value)
}
}
return res
}
}
#if swift(>=4.0)
#else
extension String {
var count: Int {
return self.characters.count
}
}
#endif
internal extension TimeInterval {
internal var formatshort: String {
var result = "0:00"
if self < TimeInterval(Int32.max) {
result = ""
var time = DateComponents()
time.hour = Int(self / 3600)
time.minute = Int((self.truncatingRemainder(dividingBy: 3600)) / 60)
time.second = Int(self.truncatingRemainder(dividingBy: 60))
let formatter = NumberFormatter()
formatter.paddingCharacter = "0"
formatter.minimumIntegerDigits = 2
formatter.maximumFractionDigits = 0
let formatterFirst = NumberFormatter()
formatterFirst.maximumFractionDigits = 0
if time.hour! > 0 {
result = "\(formatterFirst.string(from: NSNumber(value: time.hour!))!):\(formatter.string(from: NSNumber(value: time.minute!))!):\(formatter.string(from: NSNumber(value: time.second!))!)"
} else {
result = "\(formatterFirst.string(from: NSNumber(value: time.minute!))!):\(formatter.string(from: NSNumber(value: time.second!))!)"
}
}
result = result.trimmingCharacters(in: CharacterSet(charactersIn: ": "))
return result
}
}
public extension Date {
/// Date formats used commonly in internet messaging defined by various RFCs.
public enum RFCStandards: String {
/// Date format defined by usenet, commonly used in old implementations.
case rfc850 = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
/// Date format defined by RFC 1132 for http.
case rfc1123 = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss z"
/// Date format defined by ISO 8601, also defined in RFC 3339. Used by Dropbox.
case iso8601 = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"
/// Date string returned by asctime() function.
case asctime = "EEE MMM d HH':'mm':'ss yyyy"
/// Equivalent to and defined by RFC 1123.
public static let http = RFCStandards.rfc1123
/// Equivalent to and defined by ISO 8610.
public static let rfc3339 = RFCStandards.iso8601
/// Equivalent to and defined by RFC 850.
public static let usenet = RFCStandards.rfc850
// Sorted by commonness
fileprivate static let allValues: [RFCStandards] = [.rfc1123, .rfc850, .iso8601, .asctime]
}
/// Checks date string against various RFC standards and returns `Date`.
public init?(rfcString: String) {
let dateFor: DateFormatter = DateFormatter()
dateFor.locale = Locale(identifier: "en_US")
for standard in RFCStandards.allValues {
dateFor.dateFormat = standard.rawValue
if let date = dateFor.date(from: rfcString) {
self = date
return
}
}
return nil
}
/// Formats date according to RFCs standard.
public func format(with standard: RFCStandards, locale: Locale? = nil, timeZone: TimeZone? = nil) -> String {
let fm = DateFormatter()
fm.dateFormat = standard.rawValue
fm.timeZone = timeZone ?? TimeZone(identifier: "UTC")
fm.locale = locale ?? Locale(identifier: "en_US_POSIX")
return fm.string(from: self)
}
}
internal extension NSPredicate {
func findValue(forKey key: String?, operator op: NSComparisonPredicate.Operator? = nil) -> Any? {
let val = findAllValues(forKey: key).lazy.filter { (op == nil || $0.operator == op!) && !$0.not }
return val.first?.value
}
func findAllValues(forKey key: String?) -> [(value: Any, operator: NSComparisonPredicate.Operator, not: Bool)] {
if let cQuery = self as? NSCompoundPredicate {
let find = cQuery.subpredicates.flatMap { ($0 as! NSPredicate).findAllValues(forKey: key) }
if cQuery.compoundPredicateType == .not {
return find.map { return ($0.value, $0.operator, !$0.not) }
}
return find
} else if let cQuery = self as? NSComparisonPredicate {
if cQuery.leftExpression.expressionType == .keyPath, key == nil || cQuery.leftExpression.keyPath == key!, let const = cQuery.rightExpression.constantValue {
return [(value: const, operator: cQuery.predicateOperatorType, false)]
}
if cQuery.rightExpression.expressionType == .keyPath, key == nil || cQuery.rightExpression.keyPath == key!, let const = cQuery.leftExpression.constantValue {
return [(value: const, operator: cQuery.predicateOperatorType, false)]
}
return []
} else {
return []
}
}
}
func ~=<T>(pattern: (T) -> Bool, value: T) -> Bool {
return pattern(value)
}
func hasPrefix(_ prefix: String) -> (_ value: String) -> Bool {
return { (value: String) -> Bool in
value.hasPrefix(prefix)
}
}
func hasSuffix(_ suffix: String) -> (_ value: String) -> Bool {
return { (value: String) -> Bool in
value.hasSuffix(suffix)
}
}
+445
View File
@@ -0,0 +1,445 @@
// Originally based on CryptoSwift by Marcin Krzyżanowski <marcin.krzyzanowski@gmail.com>
// Copyright (C) 2014 Marcin Krzyżanowski <marcin.krzyzanowski@gmail.com>
// This software is provided 'as-is', without any express or implied warranty.
//
// In no event will the authors be held liable for any damages arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
//
// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
// - This notice may not be removed or altered from any source or binary distribution.
import Foundation
protocol SHA2Variant {
static var size: Int { get }
static var h: [UInt64] { get }
static var k: [UInt64] { get }
static func resultingArray<T>(_ hh:[T]) -> ArraySlice<T>
static func calculate(_ message: [UInt8]) -> [UInt8]
}
protocol SHA2Variant32: SHA2Variant { }
protocol SHA2Variant64: SHA2Variant { }
extension SHA2Variant32 {
static var size: Int {
return 64
}
static var k: [UInt64] {
return [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]
}
// codebeat:disable[ABC]
static func calculate(_ message: [UInt8]) -> [UInt8] {
var tmpMessage = message
let len = Self.size
// Step 1. Append Padding Bits
tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message
// append "0" bit until message length in bits 448 (mod 512)
var msgLength = tmpMessage.count
var counter = 0
while msgLength % len != (len - 8) {
counter += 1
msgLength += 1
}
tmpMessage.append(contentsOf: [UInt8](repeating: 0, count: counter))
// hash values
var hh: [UInt32] = Self.h.map { UInt32($0) }
let k = Self.k
// append message length, in a 64-bit big-endian integer. So now the message length is a multiple of 512 bits.
tmpMessage.append(contentsOf: arrayOfBytes(message.count * 8, length: 64 / 8))
// Process the message in successive 512-bit chunks:
let chunkSizeBytes = 512 / 8 // 64
for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) {
// break chunk into sixteen 32-bit words M[j], 0 j 15, big-endian
// Extend the sixteen 32-bit words into sixty-four 32-bit words:
var M:[UInt32] = [UInt32](repeating: 0, count: k.count)
for x in 0..<M.count {
switch (x) {
case 0...15:
let start: Int = chunk.startIndex + (x * MemoryLayout.size(ofValue: M[x]))
let end: Int = start + MemoryLayout.size(ofValue: M[x])
let le = toUInt32Array(chunk[start..<end])[0]
M[x] = le.bigEndian
break
default:
let s0 = rotateRight(M[x-15], n: 7) ^ rotateRight(M[x-15], n: 18) ^ (M[x-15] >> 3) //FIXME: n
let s1 = rotateRight(M[x-2], n: 17) ^ rotateRight(M[x-2], n: 19) ^ (M[x-2] >> 10)
let s2 = M[x-16]
let s3 = M[x-7]
M[x] = s2 &+ s0 &+ s3 &+ s1
break
}
}
var A = hh[0], B = hh[1], C = hh[2], D = hh[3], E = hh[4], F = hh[5], G = hh[6], H = hh[7]
// Main loop
for j in 0..<k.count {
let s0 = rotateRight(A,n: 2) ^ rotateRight(A,n: 13) ^ rotateRight(A,n: 22)
let maj = (A & B) ^ (A & C) ^ (B & C)
let t2 = s0 &+ maj
let s1 = rotateRight(E,n: 6) ^ rotateRight(E,n: 11) ^ rotateRight(E,n: 25)
let ch = (E & F) ^ ((~E) & G)
let t1 = H &+ s1 &+ ch &+ UInt32(k[j]) &+ M[j]
H = G; G = F; F = E; E = D &+ t1
D = C; C = B; B = A; A = t1 &+ t2
}
hh[0] = (hh[0] &+ A)
hh[1] = (hh[1] &+ B)
hh[2] = (hh[2] &+ C)
hh[3] = (hh[3] &+ D)
hh[4] = (hh[4] &+ E)
hh[5] = (hh[5] &+ F)
hh[6] = (hh[6] &+ G)
hh[7] = (hh[7] &+ H)
}
// Produce the final hash value (big-endian) as a 160 bit number:
var result = [UInt8]()
result.reserveCapacity(hh.count / 4)
Self.resultingArray(hh).forEach {
let item = $0.bigEndian
#if swift(>=4.0)
result.append(UInt8(truncatingIfNeeded: item))
result.append(UInt8(truncatingIfNeeded: item >> 8))
result.append(UInt8(truncatingIfNeeded: item >> 16))
result.append(UInt8(truncatingIfNeeded: item >> 24))
#else
result.append(UInt8(item))
result.append(UInt8((item >> 8) & 0xFF))
result.append(UInt8((item >> 16) & 0xFF))
result.append(UInt8((item >> 24) & 0xFF))
#endif
}
return result
}
// codebeat:enable[ABC]
}
extension SHA2Variant64 {
static var size: Int {
return 128
}
static var k: [UInt64] {
return [0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, 0x3956c25bf348b538,
0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, 0xd807aa98a3030242, 0x12835b0145706fbe,
0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235,
0xc19bf174cf692694, 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65,
0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, 0x983e5152ee66dfab,
0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, 0xc6e00bf33da88fc2, 0xd5a79147930aa725,
0x06ca6351e003826f, 0x142929670a0e6e70, 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed,
0x53380d139d95b3df, 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b,
0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, 0xd192e819d6ef5218,
0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, 0x19a4c116b8d2d0c8, 0x1e376c085141ab53,
0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373,
0x682e6ff3d6b2b8a3, 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec,
0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, 0xca273eceea26619c,
0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, 0x06f067aa72176fba, 0x0a637dc5a2c898a6,
0x113f9804bef90dae, 0x1b710b35131c471b, 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc,
0x431d67c49c100d4c, 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817]
}
// codebeat:disable[ABC]
static func calculate(_ message: [UInt8]) -> [UInt8] {
var tmpMessage = message
let len = Self.size
// Step 1. Append Padding Bits
tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message
// append "0" bit until message length in bits 448 (mod 512)
var msgLength = tmpMessage.count
var counter = 0
while msgLength % len != (len - 8) {
counter += 1
msgLength += 1
}
tmpMessage += [UInt8](repeating: 0, count: counter)
// hash values
var hh: [UInt64] = Self.h
let k = Self.k
// append message length, in a 64-bit big-endian integer. So now the message length is a multiple of 512 bits.
tmpMessage += arrayOfBytes(message.count * 8, length: 64 / 8)
// Process the message in successive 1024-bit chunks:
let chunkSizeBytes = 1024 / 8 // 128
for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) {
// break chunk into sixteen 64-bit words M[j], 0 j 15, big-endian
// Extend the sixteen 64-bit words into eighty 64-bit words:
var M = [UInt64](repeating: 0, count: k.count)
for x in 0..<M.count {
switch (x) {
case 0...15:
let start = chunk.startIndex + (x * MemoryLayout.size(ofValue: M[x]))
let end = start + MemoryLayout.size(ofValue: M[x])
let le = toUInt64Array(chunk[start..<end])[0]
M[x] = le.bigEndian
break
default:
let s0 = rotateRight(M[x-15], n: 1) ^ rotateRight(M[x-15], n: 8) ^ (M[x-15] >> 7)
let s1 = rotateRight(M[x-2], n: 19) ^ rotateRight(M[x-2], n: 61) ^ (M[x-2] >> 6)
let s2 = M[x-16]
let s3 = M[x-7]
M[x] = s2 &+ s0 &+ s3 &+ s1
break
}
}
var A = hh[0], B = hh[1], C = hh[2], D = hh[3], E = hh[4], F = hh[5], G = hh[6], H = hh[7]
// Main loop
for j in 0..<k.count {
let s0 = rotateRight(A,n: 28) ^ rotateRight(A,n: 34) ^ rotateRight(A,n: 39) //FIXME: n:
let maj = (A & B) ^ (A & C) ^ (B & C)
let t2 = s0 &+ maj
let s1 = rotateRight(E,n: 14) ^ rotateRight(E,n: 18) ^ rotateRight(E,n: 41)
let ch = (E & F) ^ ((~E) & G)
let t1 = H &+ s1 &+ ch &+ k[j] &+ UInt64(M[j])
H = G; G = F; F = E; E = D &+ t1
D = C; C = B; B = A; A = t1 &+ t2
}
hh[0] = (hh[0] &+ A)
hh[1] = (hh[1] &+ B)
hh[2] = (hh[2] &+ C)
hh[3] = (hh[3] &+ D)
hh[4] = (hh[4] &+ E)
hh[5] = (hh[5] &+ F)
hh[6] = (hh[6] &+ G)
hh[7] = (hh[7] &+ H)
}
// Produce the final hash value (big-endian)
var result = [UInt8]()
result.reserveCapacity(hh.count / 4)
Self.resultingArray(hh).forEach {
let item = $0.bigEndian
var partialResult = [UInt8]()
partialResult.reserveCapacity(8)
for i in 0..<8 {
let shift = UInt64(8 * i)
partialResult.append(UInt8((item >> shift) & 0xff))
}
result += partialResult
}
return result
}
// codebeat:enable[ABC]
}
final class SHA256 : SHA2Variant32 {
static let h: [UInt64] = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]
static func resultingArray<T>(_ hh: [T]) -> ArraySlice<T> {
return ArraySlice(hh)
}
}
final class SHA384 : SHA2Variant64 {
static let h: [UInt64] = [0xcbbb9d5dc1059ed8, 0x629a292a367cd507, 0x9159015a3070dd17, 0x152fecd8f70e5939, 0x67332667ffc00b31, 0x8eb44a8768581511, 0xdb0c2e0d64f98fa7, 0x47b5481dbefa4fa4]
public static func resultingArray<T>(_ hh: [T]) -> ArraySlice<T> {
return hh[0..<6]
}
}
final class SHA512 : SHA2Variant64 {
static let h: [UInt64] = [0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179]
static func resultingArray<T>(_ hh: [T]) -> ArraySlice<T> {
return ArraySlice(hh)
}
}
final class SHA2<Variant: SHA2Variant> {
static var size: Int {
return Variant.size
}
static func calculate(_ message: [UInt8]) -> [UInt8] {
return Variant.calculate(message)
}
}
final class HMAC<Variant: SHA2Variant> {
public static func authenticate(message:[UInt8], withKey key: [UInt8]) -> [UInt8] {
var key = key
if (key.count > Variant.size) {
key = Variant.calculate(key)
}
if (key.count < Variant.size) { // keys shorter than blocksize are zero-padded
key = key + [UInt8](repeating: 0, count: Variant.size - key.count)
}
var opad = [UInt8](repeating: 0x5c, count: Variant.size)
for (idx, _) in key.enumerated() {
opad[idx] = key[idx] ^ opad[idx]
}
var ipad = [UInt8](repeating: 0x36, count: Variant.size)
for (idx, _) in key.enumerated() {
ipad[idx] = key[idx] ^ ipad[idx]
}
let ipadAndMessageHash = Variant.calculate(ipad + message)
let finalHash = Variant.calculate(opad + ipadAndMessageHash);
return finalHash
}
static func authenticate(message: String, withKey key: [UInt8]) -> [UInt8] {
return authenticate(message: [UInt8](message.utf8), withKey: key)
}
static func authenticate(message: Data, withKey key: Data) -> Data {
return Data(bytes: authenticate(message: Array(message), withKey: Array(key)))
}
static func authenticate(message: String, withKey key: Data) -> Data {
return Data(bytes: authenticate(message: [UInt8](message.utf8), withKey: Array(key)))
}
}
fileprivate struct BytesSequence: Sequence {
let chunkSize: Int
let data: [UInt8]
init(chunkSize: Int, data: [UInt8]) {
self.chunkSize = chunkSize
self.data = data
}
func makeIterator() -> AnyIterator<ArraySlice<UInt8>> {
var offset:Int = 0
return AnyIterator {
let end = Swift.min(self.chunkSize, self.data.count - offset)
let result = self.data[offset..<offset + end]
offset += result.count
return result.count > 0 ? result : nil
}
}
}
fileprivate func rotateRight(_ x:UInt16, n:UInt16) -> UInt16 {
return (x >> n) | (x << (16 - n))
}
fileprivate func rotateRight(_ x:UInt32, n:UInt32) -> UInt32 {
return (x >> n) | (x << (32 - n))
}
fileprivate func rotateRight(_ x:UInt64, n:UInt64) -> UInt64 {
return ((x >> n) | (x << (64 - n)))
}
fileprivate func toUInt32Array(_ slice: ArraySlice<UInt8>) -> Array<UInt32> {
var result = Array<UInt32>()
result.reserveCapacity(16)
for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout<UInt32>.size) {
let val1:UInt32 = (UInt32(slice[idx.advanced(by: 3)]) << 24)
let val2:UInt32 = (UInt32(slice[idx.advanced(by: 2)]) << 16)
let val3:UInt32 = (UInt32(slice[idx.advanced(by: 1)]) << 8)
let val4:UInt32 = UInt32(slice[idx])
let val:UInt32 = val1 | val2 | val3 | val4
result.append(val)
}
return result
}
fileprivate func toUInt64Array(_ slice: ArraySlice<UInt8>) -> Array<UInt64> {
var result = Array<UInt64>()
result.reserveCapacity(32)
for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout<UInt64>.size) {
var val:UInt64 = 0
val |= UInt64(slice[idx.advanced(by: 7)]) << 56
val |= UInt64(slice[idx.advanced(by: 6)]) << 48
val |= UInt64(slice[idx.advanced(by: 5)]) << 40
val |= UInt64(slice[idx.advanced(by: 4)]) << 32
val |= UInt64(slice[idx.advanced(by: 3)]) << 24
val |= UInt64(slice[idx.advanced(by: 2)]) << 16
val |= UInt64(slice[idx.advanced(by: 1)]) << 8
val |= UInt64(slice[idx.advanced(by: 0)]) << 0
result.append(val)
}
return result
}
fileprivate func arrayOfBytes<T>(_ value:T, length:Int? = nil) -> [UInt8] {
let totalBytes = length ?? MemoryLayout<T>.size
let valuePointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
valuePointer.pointee = value
let bytesPointer = UnsafeMutableRawPointer(valuePointer).assumingMemoryBound(to: UInt8.self)
var bytes = [UInt8](repeating: 0, count: totalBytes)
for j in 0..<min(MemoryLayout<T>.size,totalBytes) {
bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee
}
valuePointer.deinitialize()
valuePointer.deallocate(capacity: 1)
return bytes
}
public extension String {
public func fp_sha256() -> [UInt8] {
return SHA2<SHA256>.calculate([UInt8](self.utf8))
}
public func fp_sha384() -> [UInt8] {
return SHA2<SHA384>.calculate([UInt8](self.utf8))
}
public func fp_sha512() -> [UInt8] {
return SHA2<SHA512>.calculate([UInt8](self.utf8))
}
}
public extension Data {
public func fp_sha256() -> [UInt8] {
return SHA2<SHA256>.calculate(Array(self))
}
public func fp_sha384() -> [UInt8] {
return SHA2<SHA384>.calculate(Array(self))
}
public func fp_sha512() -> [UInt8] {
return SHA2<SHA512>.calculate(Array(self))
}
}
+31 -31
View File
@@ -10,7 +10,7 @@ import Foundation
private var lasttaskIdAssociated = 1_000_000_000
// codebeat:disable[TOTAL_LOC,TOO_MANY_IVARS]
/// This class is a replica of NSURLSessionStreamTask with same api for iOS 7/8
/// while it can actually fallback to NSURLSessionStreamTask in iOS 9.
public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
@@ -27,10 +27,10 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
/// Force using `URLSessionStreamTask` for iOS 9 and later
public let useURLSession: Bool
@available(iOS 9.0, OSX 10.11, *)
@available(iOS 9.0, macOS 10.11, *)
fileprivate static var streamTasks = [Int: URLSessionStreamTask]()
@available(iOS 9.0, OSX 10.11, *)
@available(iOS 9.0, macOS 10.11, *)
internal var _underlyingTask: URLSessionStreamTask? {
return FileProviderStreamTask.streamTasks[_taskIdentifier]
}
@@ -42,7 +42,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* tasks in other sessions may have the same `taskIdentifier` value.
*/
open override var taskIdentifier: Int {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
return _underlyingTask!.taskIdentifier
}
@@ -57,7 +57,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
/// then display to the user as part of your apps user interface.
open override var taskDescription: String? {
get {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
return _underlyingTask!.taskDescription
}
@@ -67,7 +67,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
}
@objc(setTaskDescription:)
set {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
_underlyingTask!.taskDescription = newValue
return
@@ -84,7 +84,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* of being canceled, or completed.
*/
override open var state: URLSessionTask.State {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
return _underlyingTask!.state
}
@@ -99,7 +99,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* except when the server has responded to the initial request with a redirect to a different URL.
*/
override open var originalRequest: URLRequest? {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
return _underlyingTask!.originalRequest
}
@@ -114,7 +114,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* except when the server has responded to the initial request with a redirect to a different URL.
*/
override open var currentRequest: URLRequest? {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
return _underlyingTask!.currentRequest
} else {
return nil
@@ -172,7 +172,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* or the `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)` method (for download tasks).
*/
override open var countOfBytesReceived: Int64 {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
return _underlyingTask!.countOfBytesReceived
}
@@ -192,7 +192,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* Otherwise, the value is `NSURLSessionTransferSizeUnknown` (`-1`) if you provided a stream or body data object, or zero (`0`) if you did not.
*/
override open var countOfBytesExpectedToSend: Int64 {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
return _underlyingTask!.countOfBytesExpectedToSend
} else {
return Int64(dataToBeSent.count)
@@ -206,7 +206,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* If that header is absent, the value is `NSURLSessionTransferSizeUnknown`.
*/
override open var countOfBytesExpectedToReceive: Int64 {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
return _underlyingTask!.countOfBytesExpectedToReceive
}
@@ -268,7 +268,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
internal init(session: URLSession, host: String, port: Int, useURLSession: Bool = true) {
self._underlyingSession = session
self.useURLSession = useURLSession
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if useURLSession {
let task = session.streamTask(withHostName: host, port: port)
self._taskIdentifier = task.taskIdentifier
@@ -288,7 +288,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
internal init(session: URLSession, netService: NetService, useURLSession: Bool = true) {
self._underlyingSession = session
self.useURLSession = useURLSession
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if useURLSession {
let task = session.streamTask(with: netService)
self._taskIdentifier = task.taskIdentifier
@@ -316,7 +316,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* This method may be called on a task that is suspended.
*/
override open func cancel() {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
_underlyingTask!.cancel()
return
@@ -352,7 +352,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* This value is `NULL` if the task is still active or if the transfer completed successfully.
*/
override open var error: Error? {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if useURLSession {
return _underlyingTask!.error
}
@@ -369,7 +369,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* All other tasks must start over when resumed.
*/
override open func suspend() {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
_underlyingTask!.suspend()
return
@@ -382,7 +382,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
// Resumes the task, if it is suspended.
override open func resume() {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
_underlyingTask!.resume()
return
@@ -439,12 +439,12 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* the read is canceled and the completionHandler is called with an error. Pass `0` to prevent a read from timing out.
* - Parameter completionHandler: The completion handler to call when all bytes are read, or an error occurs.
* This handler is executed on the delegate queue. This completion handler takes the following parameters:
* - data: The data read from the stream.
* - atEOF: Whether or not the stream reached end-of-file (`EOF`), such that no more data can be read.
* - error: An error object that indicates why the read failed, or `nil` if the read was successful.
* - Parameter data: The data read from the stream.
* - Parameter atEOF: Whether or not the stream reached end-of-file (`EOF`), such that no more data can be read.
* - Parameter error: An error object that indicates why the read failed, or `nil` if the read was successful.
*/
open func readData(ofMinLength minBytes: Int, maxLength maxBytes: Int, timeout: TimeInterval, completionHandler: @escaping (_ data: Data?, _ atEOF: Bool, _ error :Error?) -> Void) {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
_underlyingTask!.readData(ofMinLength: minBytes, maxLength: maxBytes, timeout: timeout, completionHandler: completionHandler)
return
@@ -491,10 +491,10 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* - Parameter completionHandler: The completion handler to call when all bytes are written, or an error occurs.
* This handler is executed on the delegate queue.
* This completion handler takes the following parameter:
* - error: An error object that indicates why the write failed, or nil if the write was successful.
* - Parameter error: An error object that indicates why the write failed, or `nil` if the write was successful.
*/
open func write(_ data: Data, timeout: TimeInterval, completionHandler: @escaping (_ error: Error?) -> Void) {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
_underlyingTask!.write(data, timeout: timeout, completionHandler: completionHandler)
return
@@ -505,12 +505,11 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
return
}
operation_queue.addOperation {
self.dataToBeSent.append(data)
let result = self.write(timeout: timeout, close: false)
if result < 0 {
let error = self.outputStream?.streamError ?? NSError(domain: URLError.errorDomain, code: URLError.cannotWriteToFile.rawValue, userInfo: nil)
let error = self.outputStream?.streamError ?? URLError(.cannotWriteToFile)
completionHandler(error)
} else {
completionHandler(nil)
@@ -523,7 +522,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* `urlSession(_:streamTask:didBecome:outputStream:)` delegate message.
*/
open func captureStreams() {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
_underlyingTask!.captureStreams()
return
@@ -553,7 +552,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* you continue reading until the stream reaches end-of-file (EOF).
*/
open func closeWrite() {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
_underlyingTask!.closeWrite()
return
@@ -605,7 +604,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* after calling this method will result in an error.
*/
open func closeRead() {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
_underlyingTask!.closeRead()
return
@@ -631,7 +630,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* `urlSession(_:task:didReceive:completionHandler:)` method.
*/
open func startSecureConnection() {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
_underlyingTask!.startSecureConnection()
return
@@ -648,7 +647,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* Completes any enqueued reads and writes, and closes the secure connection.
*/
open func stopSecureConnection() {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
_underlyingTask!.stopSecureConnection()
return
@@ -736,6 +735,7 @@ internal protocol FPSStreamDelegate : URLSessionTaskDelegate {
*/
@objc optional func urlSession(_ session: URLSession, streamTask: FileProviderStreamTask, didBecome inputStream: InputStream, outputStream: OutputStream)
}
// codebeat:enable[TOTAL_LOC,TOO_MANY_IVARS]
private let ports: [String: Int] = ["http": 80, "https": 443, "smb": 445,"ftp": 21,
"telnet": 23, "pop": 110, "smtp": 25, "imap": 143]
+323 -343
View File
@@ -12,10 +12,12 @@ import Foundation
Allows accessing to FTP files and directories. This provider doesn't cache or save files internally.
It's a complete reimplementation and doesn't use CFNetwork deprecated API.
*/
open class FTPFileProvider: FileProviderBasicRemote {
open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite {
open class var type: String { return "FTP" }
open let baseURL: URL?
open var currentPath: String
/// **OBSOLETED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method.
@available(*, obsoleted: 0.22, message: "This property is redundant with almost no use internally.")
open var currentPath: String = ""
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue {
@@ -40,30 +42,28 @@ open class FTPFileProvider: FileProviderBasicRemote {
/// Force to use URLSessionDownloadTask/URLSessionDataTask when possible
public var useAppleImplementation = true
fileprivate var _session: URLSession?
fileprivate var _session: URLSession!
internal var sessionDelegate: SessionDelegate?
public var session: URLSession {
get {
if _session == nil {
self.sessionDelegate = SessionDelegate(fileProvider: self)
let config = URLSessionConfiguration.default
config.urlCache = cache
config.requestCachePolicy = .returnCacheDataElseLoad
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: self.operation_queue)
_session?.sessionDescription = UUID().uuidString
initEmptySessionHandler(_session!.sessionDescription!)
_session.sessionDescription = UUID().uuidString
initEmptySessionHandler(_session.sessionDescription!)
}
return _session!
return _session
}
set {
assert(newValue.delegate is SessionDelegate, "session instances should have a SessionDelegate instance as delegate.")
_session = newValue
if session.sessionDescription?.isEmpty ?? true {
_session?.sessionDescription = UUID().uuidString
if _session.sessionDescription?.isEmpty ?? true {
_session.sessionDescription = UUID().uuidString
}
self.sessionDelegate = newValue.delegate as? SessionDelegate
initEmptySessionHandler(_session!.sessionDescription!)
initEmptySessionHandler(_session.sessionDescription!)
}
}
@@ -85,10 +85,10 @@ open class FTPFileProvider: FileProviderBasicRemote {
let defaultPort: Int = baseURL.scheme == "ftps" ? 990 : 21
urlComponents.port = urlComponents.port ?? defaultPort
urlComponents.scheme = urlComponents.scheme ?? "ftp"
urlComponents.path = urlComponents.path.hasSuffix("/") ? urlComponents.path : urlComponents.path + "/"
self.baseURL = (urlComponents.url!.path.hasSuffix("/") ? urlComponents.url! : urlComponents.url!.appendingPathComponent("")).absoluteURL
self.baseURL = urlComponents.url!.absoluteURL
self.passiveMode = passive
self.currentPath = ""
self.useCache = false
self.validatingCache = true
self.cache = cache
@@ -107,7 +107,6 @@ open class FTPFileProvider: FileProviderBasicRemote {
public required convenience init?(coder aDecoder: NSCoder) {
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else { return nil }
self.init(baseURL: baseURL, passive: aDecoder.decodeBool(forKey: "passiveMode"), credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
self.useCache = aDecoder.decodeBool(forKey: "useCache")
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
self.useAppleImplementation = aDecoder.decodeBool(forKey: "useAppleImplementation")
@@ -116,7 +115,6 @@ open class FTPFileProvider: FileProviderBasicRemote {
public func encode(with aCoder: NSCoder) {
aCoder.encode(self.baseURL, forKey: "baseURL")
aCoder.encode(self.credential, forKey: "credential")
aCoder.encode(self.currentPath, forKey: "currentPath")
aCoder.encode(self.useCache, forKey: "useCache")
aCoder.encode(self.validatingCache, forKey: "validatingCache")
aCoder.encode(self.useAppleImplementation, forKey: "useAppleImplementation")
@@ -129,7 +127,6 @@ open class FTPFileProvider: FileProviderBasicRemote {
open func copy(with zone: NSZone? = nil) -> Any {
let copy = FTPFileProvider(baseURL: self.baseURL!, credential: self.credential, cache: self.cache)!
copy.currentPath = self.currentPath
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
copy.useCache = self.useCache
@@ -152,7 +149,7 @@ open class FTPFileProvider: FileProviderBasicRemote {
internal var serverSupportsRFC3659: Bool = true
open func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) {
open func contentsOfDirectory(path: String, completionHandler: @escaping ([FileObject], Error?) -> Void) {
self.contentsOfDirectory(path: path, rfc3659enabled: serverSupportsRFC3659, completionHandler: completionHandler)
}
@@ -161,13 +158,13 @@ open class FTPFileProvider: FileProviderBasicRemote {
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 path: path to target directory. If empty, root will be iterated.
- Parameter rfc3659enabled: uses MLST command instead of old LIST to get files attributes, default is `true`.
- Parameter completionHandler: a closure with result of directory entries or error.
`contents`: An array of `FileObject` identifying the the directory entries.
`error`: Error returned by system.
- Parameter contents: An array of `FileObject` identifying the the directory entries.
- Parameter error: Error returned by system.
*/
open func contentsOfDirectory(path apath: String, rfc3659enabled: Bool , completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
open func contentsOfDirectory(path apath: String, rfc3659enabled: Bool , completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
let path = ftpPath(apath)
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
@@ -207,7 +204,7 @@ open class FTPFileProvider: FileProviderBasicRemote {
}
}
open func attributesOfItem(path: String, completionHandler: @escaping ((FileObject?, Error?) -> Void)) {
open func attributesOfItem(path: String, completionHandler: @escaping (FileObject?, Error?) -> Void) {
self.attributesOfItem(path: path, rfc3659enabled: serverSupportsRFC3659, completionHandler: completionHandler)
}
@@ -216,13 +213,13 @@ open class FTPFileProvider: FileProviderBasicRemote {
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 path: path to target directory. If empty, attributes of root will be returned.
- Parameter rfc3659enabled: uses MLST command instead of old LIST to get files attributes, default is true.
- Parameter completionHandler: a closure with result of directory entries or error.
`attributes`: A `FileObject` containing the attributes of the item.
`error`: Error returned by system.
*/
open func attributesOfItem(path apath: String, rfc3659enabled: Bool, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
open func attributesOfItem(path apath: String, rfc3659enabled: Bool, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
let path = ftpPath(apath)
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
@@ -239,69 +236,83 @@ open class FTPFileProvider: FileProviderBasicRemote {
defer {
self.ftpQuit(task)
}
if let error = error {
do {
if let error = error {
throw error
}
guard let response = response, response.hasPrefix("250") || (response.hasPrefix("50") && rfc3659enabled) else {
throw self.urlError(path, code: .badServerResponse)
}
if response.hasPrefix("500") {
self.serverSupportsRFC3659 = false
self.attributesOfItem(path: path, rfc3659enabled: false, completionHandler: completionHandler)
}
let lines = response.components(separatedBy: "\n").flatMap { $0.isEmpty ? nil : $0.trimmingCharacters(in: .whitespacesAndNewlines) }
guard lines.count > 2 else {
throw self.urlError(path, code: .badServerResponse)
}
let file: FileObject? = rfc3659enabled ? self.parseMLST(lines[1], in: path) : self.parseUnixList(lines[1], in: path)
self.dispatch_queue.async {
completionHandler(file, nil)
}
} catch {
self.dispatch_queue.async {
completionHandler(nil, error)
}
return
}
guard let response = response, response.hasPrefix("250") || (response.hasPrefix("50") && rfc3659enabled) else {
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: URLError.badServerResponse))
}
return
}
if response.hasPrefix("500") {
self.serverSupportsRFC3659 = false
self.attributesOfItem(path: path, rfc3659enabled: false, completionHandler: completionHandler)
}
let lines = response.components(separatedBy: "\n").flatMap { $0.isEmpty ? nil : $0.trimmingCharacters(in: .whitespacesAndNewlines) }
guard lines.count > 2 else {
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: URLError.badServerResponse))
}
return
}
let file = rfc3659enabled ? self.parseMLST(lines[1], in: path) : self.parseUnixList(lines[1], in: path)
self.dispatch_queue.async {
completionHandler(file, nil)
}
})
}
}
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
open func storageProperties(completionHandler: @escaping (_ volume: VolumeObject?) -> Void) {
dispatch_queue.async {
completionHandler(-1, 0)
completionHandler(nil)
}
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
let progress = Progress(parent: nil, userInfo: nil)
_ = self.recursiveList(path: path, useMLST: true, foundItemsHandler: { items in
if let foundItemHandler = foundItemHandler {
for item in items where query.evaluate(with: item.mapPredicate()) {
foundItemHandler(item)
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: -1)
if recursive {
return self.recursiveList(path: path, useMLST: true, foundItemsHandler: { items in
if let foundItemHandler = foundItemHandler {
for item in items where query.evaluate(with: item.mapPredicate()) {
foundItemHandler(item)
}
progress.totalUnitCount = Int64(items.count)
}
progress.totalUnitCount = Int64(items.count)
}
}, completionHandler: {files, error in
if let error = error {
completionHandler([], error)
return
}
let foundFiles = files.filter { query.evaluate(with: $0.mapPredicate()) }
completionHandler(foundFiles, nil)
})
}, completionHandler: {files, error in
if let error = error {
completionHandler([], error)
return
}
let foundFiles = files.filter { query.evaluate(with: $0.mapPredicate()) }
completionHandler(foundFiles, nil)
})
} else {
self.contentsOfDirectory(path: path, completionHandler: { (items, error) in
if let error = error {
completionHandler([], error)
return
}
var result = [FileObject]()
for item in items where query.evaluate(with: item.mapPredicate()) {
foundItemHandler?(item)
result.append(item)
}
completionHandler(result, nil)
})
}
return progress
}
public func url(of path: String?) -> URL {
let path = (path ?? self.currentPath).trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? (path ?? self.currentPath)
open func url(of path: String?) -> URL {
let path = path?.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? (path ?? "")
var baseUrlComponent = URLComponents(url: self.baseURL!, resolvingAgainstBaseURL: true)
baseUrlComponent?.user = credential?.user
@@ -309,7 +320,7 @@ open class FTPFileProvider: FileProviderBasicRemote {
return URL(string: path, relativeTo: baseUrlComponent?.url ?? baseURL) ?? baseUrlComponent?.url ?? baseURL!
}
public func relativePathOf(url: URL) -> String {
open func relativePathOf(url: URL) -> String {
// check if url derieved from current base url
let relativePath = url.relativePath
if !relativePath.isEmpty, url.baseURL == self.baseURL {
@@ -330,9 +341,7 @@ open class FTPFileProvider: FileProviderBasicRemote {
}
open weak var fileOperationDelegate: FileOperationDelegate?
}
extension FTPFileProvider: FileProviderOperations {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
return doOperation(.create(path: path), completionHandler: completionHandler)
@@ -350,226 +359,22 @@ extension FTPFileProvider: FileProviderOperations {
return doOperation(.remove(path: path), completionHandler: completionHandler)
}
fileprivate func doOperation(_ opType: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let sourcePath = opType.source
let destPath = opType.destination
let command: String
switch opType {
case .create:
command = "MKD \(ftpPath(sourcePath))"
case .copy:
command = "SITE CPFR \(ftpPath(sourcePath))\r\nSITE CPTO \(ftpPath(destPath!))"
case .move:
command = "RNFR \(ftpPath(sourcePath))\r\nRNTO \(ftpPath(destPath!))"
case .remove:
command = "DELE \(ftpPath(sourcePath))"
case .link:
command = "SITE SYMLINK \(ftpPath(sourcePath)) \(ftpPath(destPath!))"
default: // modify, fetch
return nil
}
let progress = Progress(totalUnitCount: 1)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
self.ftpLogin(task) { (error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
return
}
self.execute(command: command, on: task, completionHandler: { (response, error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
return
}
guard let response = response else {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: self.throwError(sourcePath, code: URLError.badServerResponse))
}
return
}
let codes: [Int] = response.components(separatedBy: .newlines).flatMap({ $0.isEmpty ? nil : $0})
.flatMap {
let code = $0.components(separatedBy: .whitespaces).flatMap({ $0.isEmpty ? nil : $0}).first
return code != nil ? Int(code!) : nil
}
if codes.filter({ (450..<560).contains($0) }).count > 0 {
let errorCode: URLError.Code
switch opType {
case .create:
errorCode = URLError.cannotCreateFile
case .modify:
errorCode = URLError.cannotWriteToFile
case .copy:
self.fallbackCopy(opType, progress: progress, completionHandler: completionHandler)
return
case .move:
errorCode = URLError.cannotMoveFile
case .remove:
self.fallbackRemove(opType, progress: progress, on: task, completionHandler: completionHandler)
return
case .link:
errorCode = URLError.cannotWriteToFile
default:
errorCode = URLError.cannotOpenFile
}
let error = self.throwError(sourcePath, code: errorCode)
progress.cancel()
self.dispatch_queue.async {
completionHandler?(error)
}
self.delegateNotify(opType, error: error)
return
}
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
completionHandler?(nil)
}
self.delegateNotify(opType, error: nil)
})
}
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
return progress
}
private func fallbackCopy(_ opType: FileOperationType, progress: Progress, completionHandler: SimpleCompletionHandler) {
let sourcePath = opType.source
guard let destPath = opType.destination else { return }
let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).appendingPathExtension("tmp")
progress.becomeCurrent(withPendingUnitCount: 1)
_ = self.copyItem(path: sourcePath, toLocalURL: localURL) { (error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
return
}
progress.becomeCurrent(withPendingUnitCount: 1)
_ = self.copyItem(localFile: localURL, to: destPath) { error in
completionHandler?(nil)
self.delegateNotify(opType, error: nil)
}
progress.resignCurrent()
}
progress.resignCurrent()
return
}
private func fallbackRemove(_ opType: FileOperationType, progress: Progress, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
let sourcePath = opType.source
self.execute(command: "SITE RMDIR \(ftpPath(sourcePath))", on: task) { (response, error) in
if let error = error {
progress.cancel()
self.dispatch_queue.async {
completionHandler?(error)
}
self.delegateNotify(opType, error: error)
return
}
guard let response = response else {
progress.cancel()
let error = self.throwError(sourcePath, code: URLError.badServerResponse)
self.dispatch_queue.async {
completionHandler?(error)
}
self.delegateNotify(opType, error: error)
return
}
if response.hasPrefix("50") {
self.fallbackRecursiveRemove(opType, progress: progress, on: task, completionHandler: completionHandler)
return
}
var error: Error?
if !response.hasPrefix("2") {
error = self.throwError(sourcePath, code: URLError.cannotRemoveFile)
}
self.dispatch_queue.async {
completionHandler?(error)
}
self.delegateNotify(opType, error: error)
}
}
private func fallbackRecursiveRemove(_ opType: FileOperationType, progress: Progress, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
let sourcePath = opType.source
_ = self.recursiveList(path: sourcePath, useMLST: true, completionHandler: { (contents, error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
return
}
let recursiveProgress = Progress(parent: progress, userInfo: nil)
recursiveProgress.totalUnitCount = Int64(contents.count)
let sortedContents = contents.sorted(by: {
$0.path.localizedStandardCompare($1.path) == .orderedDescending
})
var command = ""
for file in sortedContents {
command += (file.isDirectory ? "RMD \(self.ftpPath(file.path))" : "DELE \(self.ftpPath(file.path))") + "\r\n"
}
command += "RMD \(self.ftpPath(sourcePath))"
self.execute(command: command, on: task, completionHandler: { (response, error) in
recursiveProgress.completedUnitCount += 1
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
// TODO: Digest response
})
})
}
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
// check file is not a folder
guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else {
dispatch_queue.async {
completionHandler?(self.throwError(localFile.path, code: URLError.fileIsDirectory))
completionHandler?(self.urlError(localFile.path, code: .fileIsDirectory))
}
return nil
}
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let progress = Progress(totalUnitCount: 0)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
@@ -578,7 +383,7 @@ extension FTPFileProvider: FileProviderOperations {
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
self.delegateNotify(operation, error: error)
}
return
}
@@ -591,9 +396,7 @@ extension FTPFileProvider: FileProviderOperations {
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { bytesSent, totalSent, expectedBytes in
progress.completedUnitCount = totalSent
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress.fractionCompleted))
}
self.delegateNotify(operation, progress: progress.fractionCompleted)
}, completionHandler: { (error) in
if error != nil {
progress.cancel()
@@ -601,7 +404,7 @@ extension FTPFileProvider: FileProviderOperations {
self.ftpQuit(task)
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
self.delegateNotify(operation, error: error)
}
})
}
@@ -610,30 +413,29 @@ extension FTPFileProvider: FileProviderOperations {
}
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
let operation = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
var progress = Progress(totalUnitCount: 0)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
if self.useAppleImplementation {
self.attributesOfItem(path: path, completionHandler: { (file, error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
do {
if let error = error {
throw error
}
return
}
if file?.isDirectory ?? false {
if file?.isDirectory ?? false {
throw self.urlError(path, code: .fileIsDirectory)
}
} catch {
self.dispatch_queue.async {
let error = self.throwError(path, code: URLError.fileIsDirectory)
completionHandler?(error)
self.delegateNotify(opType, error: error)
self.delegateNotify(operation, error: error)
}
return
}
@@ -643,14 +445,20 @@ extension FTPFileProvider: FileProviderOperations {
let task = self.session.downloadTask(with: self.url(of: path))
completionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = completionHandler
downloadCompletionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
do {
try FileManager.default.moveItem(at: tempURL, to: destURL)
completionHandler?(nil)
} catch let e {
completionHandler?(e)
var error: NSError?
NSFileCoordinator().coordinate(writingItemAt: tempURL, options: .forMoving, writingItemAt: destURL, options: .forReplacing, error: &error, byAccessor: { (tempURL, destURL) in
do {
try FileManager.default.moveItem(at: tempURL, to: destURL)
completionHandler?(nil)
} catch {
completionHandler?(error)
}
})
if let error = error {
completionHandler?(error)
}
}
task.taskDescription = opType.json
task.taskDescription = operation.json
task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
@@ -678,15 +486,13 @@ extension FTPFileProvider: FileProviderOperations {
}, onProgress: { recevied, totalReceived, totalSize in
progress.totalUnitCount = totalSize
progress.completedUnitCount = totalReceived
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress.fractionCompleted))
}
self.delegateNotify(operation, progress: progress.fractionCompleted)
}) { (tmpurl, error) in
if let error = error {
progress.cancel()
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
self.delegateNotify(operation, error: error)
}
return
}
@@ -695,7 +501,7 @@ extension FTPFileProvider: FileProviderOperations {
try? FileManager.default.moveItem(at: tmpurl, to: destURL)
self.dispatch_queue.async {
completionHandler?(nil)
self.delegateNotify(opType, error: nil)
self.delegateNotify(operation)
}
}
}
@@ -703,18 +509,16 @@ extension FTPFileProvider: FileProviderOperations {
}
return progress
}
}
extension FTPFileProvider: FileProviderReadWrite {
open func contents(path: String, completionHandler: @escaping ((Data?, Error?) -> Void)) -> Progress? {
let opType = FileOperationType.fetch(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
let operation = FileOperationType.fetch(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
if self.useAppleImplementation {
var progress = Progress(totalUnitCount: 0)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
@@ -729,11 +533,11 @@ extension FTPFileProvider: FileProviderReadWrite {
do {
let data = try Data(contentsOf: tempURL)
completionHandler(data, nil)
} catch let e {
completionHandler(nil, e)
} catch {
completionHandler(nil, error)
}
}
task.taskDescription = opType.json
task.taskDescription = operation.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
@@ -748,16 +552,16 @@ extension FTPFileProvider: FileProviderReadWrite {
}
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
let opType = FileOperationType.fetch(path: path)
let operation = FileOperationType.fetch(path: path)
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
self.delegateNotify(opType, error: nil)
self.delegateNotify(operation)
}
return nil
}
let progress = Progress(totalUnitCount: 0)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
@@ -779,15 +583,13 @@ extension FTPFileProvider: FileProviderReadWrite {
}, onProgress: { recevied, totalReceived, totalSize in
progress.totalUnitCount = totalSize
progress.completedUnitCount = totalReceived
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress.fractionCompleted))
}
self.delegateNotify(operation, progress: progress.fractionCompleted)
}) { (data, error) in
if let error = error {
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, error)
self.delegateNotify(opType, error: error)
self.delegateNotify(operation, error: error)
}
return
}
@@ -795,7 +597,7 @@ extension FTPFileProvider: FileProviderReadWrite {
if let data = data {
self.dispatch_queue.async {
completionHandler(data, nil)
self.delegateNotify(opType, error: nil)
self.delegateNotify(operation)
}
}
}
@@ -805,13 +607,13 @@ extension FTPFileProvider: FileProviderReadWrite {
}
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
let operation = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let progress = Progress(totalUnitCount: Int64(data?.count ?? 0))
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
@@ -820,7 +622,7 @@ extension FTPFileProvider: FileProviderReadWrite {
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
self.delegateNotify(operation, error: error)
}
return
}
@@ -834,9 +636,7 @@ extension FTPFileProvider: FileProviderReadWrite {
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { bytesSent, totalSent, expectedBytes in
progress.completedUnitCount = totalSent
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress.fractionCompleted))
}
self.delegateNotify(operation, progress: progress.fractionCompleted)
}, completionHandler: { (error) in
if error != nil {
progress.cancel()
@@ -844,7 +644,7 @@ extension FTPFileProvider: FileProviderReadWrite {
self.ftpQuit(task)
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
self.delegateNotify(operation, error: error)
}
})
}
@@ -862,9 +662,7 @@ extension FTPFileProvider: FileProviderReadWrite {
return progress
}
}
public extension FTPFileProvider {
/**
Creates a symbolic link at the specified path that points to an item at the given path.
This method does not traverse symbolic links contained in destination path, making it possible
@@ -878,9 +676,191 @@ public extension FTPFileProvider {
- withDestinationPath: The path that contains the item to be pointed to by the link. In other words, this is the destination of the link.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
*/
public func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) {
let opType = FileOperationType.link(link: path, target: destPath)
_=self.doOperation(opType, completionHandler: completionHandler)
open func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) {
let operation = FileOperationType.link(link: path, target: destPath)
_=self.doOperation(operation, completionHandler: completionHandler)
}
}
extension FTPFileProvider {
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let sourcePath = operation.source
let destPath = operation.destination
let command: String
switch operation {
case .create: command = "MKD \(ftpPath(sourcePath))"
case .copy: command = "SITE CPFR \(ftpPath(sourcePath))\r\nSITE CPTO \(ftpPath(destPath!))"
case .move: command = "RNFR \(ftpPath(sourcePath))\r\nRNTO \(ftpPath(destPath!))"
case .remove: command = "DELE \(ftpPath(sourcePath))"
case .link: command = "SITE SYMLINK \(ftpPath(sourcePath)) \(ftpPath(destPath!))"
default: return nil // modify, fetch
}
let progress = Progress(totalUnitCount: 1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
self.ftpLogin(task) { (error) in
if let error = error {
completionHandler?(error)
self.delegateNotify(operation, error: error)
return
}
self.execute(command: command, on: task, completionHandler: { (response, error) in
if let error = error {
completionHandler?(error)
self.delegateNotify(operation, error: error)
return
}
guard let response = response else {
completionHandler?(error)
self.delegateNotify(operation, error: self.urlError(sourcePath, code: .badServerResponse))
return
}
let codes: [Int] = response.components(separatedBy: .newlines).flatMap({ $0.isEmpty ? nil : $0})
.flatMap {
let code = $0.components(separatedBy: .whitespaces).flatMap({ $0.isEmpty ? nil : $0}).first
return code != nil ? Int(code!) : nil
}
if codes.filter({ (450..<560).contains($0) }).count > 0 {
let errorCode: URLError.Code
switch operation {
case .create: errorCode = .cannotCreateFile
case .modify: errorCode = .cannotWriteToFile
case .copy:
self.fallbackCopy(operation, progress: progress, completionHandler: completionHandler)
return
case .move: errorCode = .cannotMoveFile
case .remove:
self.fallbackRemove(operation, progress: progress, on: task, completionHandler: completionHandler)
return
case .link: errorCode = .cannotWriteToFile
default: errorCode = .cannotOpenFile
}
let error = self.urlError(sourcePath, code: errorCode)
progress.cancel()
completionHandler?(error)
self.delegateNotify(operation, error: error)
return
}
progress.completedUnitCount = progress.totalUnitCount
completionHandler?(nil)
self.delegateNotify(operation)
})
}
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
return progress
}
private func fallbackCopy(_ operation: FileOperationType, progress: Progress, completionHandler: SimpleCompletionHandler) {
let sourcePath = operation.source
guard let destPath = operation.destination else { return }
let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).appendingPathExtension("tmp")
progress.becomeCurrent(withPendingUnitCount: 1)
_ = self.copyItem(path: sourcePath, toLocalURL: localURL) { (error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
return
}
progress.becomeCurrent(withPendingUnitCount: 1)
_ = self.copyItem(localFile: localURL, to: destPath) { error in
completionHandler?(nil)
self.delegateNotify(operation)
}
progress.resignCurrent()
}
progress.resignCurrent()
return
}
private func fallbackRemove(_ operation: FileOperationType, progress: Progress, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
let sourcePath = operation.source
self.execute(command: "SITE RMDIR \(ftpPath(sourcePath))", on: task) { (response, error) in
do {
if let error = error {
throw error
}
guard let response = response else {
throw self.urlError(sourcePath, code: .badServerResponse)
}
if response.hasPrefix("50") {
self.fallbackRecursiveRemove(operation, progress: progress, on: task, completionHandler: completionHandler)
return
}
if !response.hasPrefix("2") {
throw self.urlError(sourcePath, code: .cannotRemoveFile)
}
self.dispatch_queue.async {
completionHandler?(nil)
}
self.delegateNotify(operation)
} catch {
progress.cancel()
self.dispatch_queue.async {
completionHandler?(error)
}
self.delegateNotify(operation, error: error)
}
}
}
private func fallbackRecursiveRemove(_ operation: FileOperationType, progress: Progress, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
let sourcePath = operation.source
_ = self.recursiveList(path: sourcePath, useMLST: true, completionHandler: { (contents, error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
return
}
progress.becomeCurrent(withPendingUnitCount: 1)
let recursiveProgress = Progress(totalUnitCount: Int64(contents.count))
let sortedContents = contents.sorted(by: {
$0.path.localizedStandardCompare($1.path) == .orderedDescending
})
progress.resignCurrent()
var command = ""
for file in sortedContents {
command += (file.isDirectory ? "RMD \(self.ftpPath(file.path))" : "DELE \(self.ftpPath(file.path))") + "\r\n"
}
command += "RMD \(self.ftpPath(sourcePath))"
self.execute(command: command, on: task, completionHandler: { (response, error) in
recursiveProgress.completedUnitCount += 1
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
// TODO: Digest response
})
})
}
}
+234 -242
View File
@@ -9,16 +9,6 @@
import Foundation
internal extension FTPFileProvider {
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)
}
})
}
func readDataUntilEOF(of task: FileProviderStreamTask, minLength: Int, receivedData: Data? = nil, timeout: TimeInterval, completionHandler: @escaping (_ data: Data?, _ errror:Error?) -> Void) {
task.readData(ofMinLength: minLength, maxLength: 65535, timeout: timeout) { (data, eof, error) in
if let error = error {
@@ -67,7 +57,7 @@ internal extension FTPFileProvider {
if let data = data, let response = String(data: data, encoding: .utf8) {
completionHandler(response.trimmingCharacters(in: .whitespacesAndNewlines), nil)
} else {
completionHandler(nil, self.throwError("", code: URLError.cannotParseResponse))
completionHandler(nil, self.urlError("", code: .cannotParseResponse))
return
}
}
@@ -90,18 +80,19 @@ internal extension FTPFileProvider {
let credential = self.credential
task.readData(ofMinLength: 4, maxLength: 2048, timeout: timeout) { (data, eof, error) in
if let error = error {
completionHandler(error)
return
}
guard let data = data, let response = String(data: data, encoding: .utf8) else {
completionHandler(self.throwError("", code: URLError.cannotParseResponse))
return
}
guard response.hasPrefix("22") else {
let error = FileProviderFTPError(message: response)
do {
if let error = error {
throw error
}
guard let data = data, let response = String(data: data, encoding: .utf8) else {
throw self.urlError("", code: .cannotParseResponse)
}
guard response.hasPrefix("22") else {
throw FileProviderFTPError(message: response)
}
} catch {
completionHandler(error)
return
}
@@ -114,7 +105,7 @@ internal extension FTPFileProvider {
}
guard let response = response else {
completionHandler(self.throwError("", code: URLError.badServerResponse))
completionHandler(self.urlError("", code: .badServerResponse))
return
}
@@ -130,7 +121,7 @@ internal extension FTPFileProvider {
if response?.hasPrefix("23") ?? false {
completionHandler(nil)
} else {
completionHandler(self.throwError("", code: URLError.userAuthenticationRequired))
completionHandler(self.urlError("", code: .userAuthenticationRequired))
}
}
return
@@ -138,7 +129,6 @@ internal extension FTPFileProvider {
let error = FileProviderFTPError(message: response)
completionHandler(error)
return
}
}
@@ -180,66 +170,70 @@ internal extension FTPFileProvider {
func ftpCwd(_ task: FileProviderStreamTask, to path: String, completionHandler: @escaping (_ error: Error?) -> Void) {
self.execute(command: "CWD \(path)", on: task) { (response, error) in
if let error = error {
do {
if let error = error {
throw error
}
guard let response = response else {
throw self.urlError(path, code: .badServerResponse)
}
// successfully logged in
if response.hasPrefix("25") {
completionHandler(nil)
}
// not logged in
else if response.hasPrefix("55") {
throw FileProviderFTPError(message: response)
}
} catch {
completionHandler(error)
return
}
guard let response = response else {
completionHandler(self.throwError(path, code: URLError.badServerResponse))
return
}
// successfully logged in
if response.hasPrefix("25") {
completionHandler(nil)
}
// not logged in
else if response.hasPrefix("55") {
let error = FileProviderFTPError(message: response)
completionHandler(error)
return
}
}
}
func ftpPassive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
func trimmedNumber(_ s : String) -> String {
let characterSet = Set("+*#0123456789".characters)
return String(s.characters.lazy.filter(characterSet.contains))
return s.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
}
self.execute(command: "PASV", on: task) { (response, error) in
if let error = error {
do {
if let error = error {
throw error
}
guard let response = response, let destString = response.components(separatedBy: " ").flatMap({ $0 }).last.flatMap(String.init) else {
throw self.urlError("", code: .badServerResponse)
}
let destArray = destString.components(separatedBy: ",").flatMap({ UInt32(trimmedNumber($0)) })
guard destArray.count == 6 else {
throw self.urlError("", code: .badServerResponse)
}
// first 4 elements are ip, 2 next are port, as byte
var host = destArray.prefix(4).flatMap(String.init).joined(separator: ".")
let portHi = Int(destArray[4]) << 8
let portLo = Int(destArray[5])
let port = portHi + portLo
// IPv6 workaround
if host == "127.555.555.555" {
host = self.baseURL!.host!
}
let passiveTask = self.session.fpstreamTask(withHostName: host, port: port)
passiveTask.resume()
if self.baseURL?.scheme == "ftps" || self.baseURL?.scheme == "ftpes" || self.baseURL?.port == 990 {
passiveTask.startSecureConnection()
}
completionHandler(passiveTask, nil)
} catch {
completionHandler(nil, error)
return
}
guard let response = response, let destString = response.components(separatedBy: " ").flatMap({ $0 }).last.flatMap({ String($0) }) else {
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
return
}
let destArray = destString.components(separatedBy: ",").flatMap({ UInt32(trimmedNumber($0)) })
guard destArray.count == 6 else {
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
return
}
// first 4 elements are ip, 2 next are port, as byte
var host = destArray.prefix(4).flatMap({ String($0) }).joined(separator: ".")
let port = Int(destArray[4] << 8 + destArray[5])
// IPv6 workaround
if host == "127.555.555.555" {
host = self.baseURL!.host!
}
let passiveTask = self.session.fpstreamTask(withHostName: host, port: port)
passiveTask.resume()
if self.baseURL?.scheme == "ftps" || self.baseURL?.scheme == "ftpes" || self.baseURL?.port == 990 {
passiveTask.startSecureConnection()
}
completionHandler(passiveTask, nil)
}
}
@@ -257,25 +251,24 @@ internal extension FTPFileProvider {
}
self.execute(command: "PORT \(service.port)", on: task) { (response, error) in
if let error = error {
do {
if let error = error {
throw error
}
guard let response = response else {
throw self.urlError("", code: .badServerResponse)
}
guard !response.hasPrefix("5") else {
throw self.urlError("", code: .cannotConnectToHost)
}
completionHandler(activeTask, nil)
} catch {
activeTask.cancel()
completionHandler(nil, error)
return
}
guard let response = response else {
activeTask.cancel()
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
return
}
guard !response.hasPrefix("5") else {
activeTask.cancel()
completionHandler(nil, self.throwError("", code: URLError.cannotConnectToHost))
return
}
completionHandler(activeTask, nil)
}
}
@@ -291,24 +284,25 @@ internal extension FTPFileProvider {
func ftpRest(_ task: FileProviderStreamTask, startPosition: Int64, completionHandler: @escaping (_ error: Error?) -> Void) {
self.execute(command: "REST \(startPosition)", on: task) { (response, error) in
if let error = error {
do {
if let error = error {
throw error
}
// Successful
guard let response = response else {
throw self.urlError("", code: .badServerResponse)
}
if response.hasPrefix("35") {
completionHandler(nil)
} else {
throw FileProviderFTPError(message: response, path: "")
}
} catch {
completionHandler(error)
return
}
// Successful
guard let response = response else {
completionHandler(self.throwError("", code: URLError.badServerResponse))
return
}
if response.hasPrefix("35") {
completionHandler(nil)
} else {
let error = FileProviderFTPError(message: response, path: "")
completionHandler(error)
return
}
}
}
@@ -320,7 +314,7 @@ internal extension FTPFileProvider {
}
guard let dataTask = dataTask else {
completionHandler([], self.throwError(path, code: URLError.badServerResponse))
completionHandler([], self.urlError(path, code: .badServerResponse))
return
}
@@ -334,73 +328,74 @@ internal extension FTPFileProvider {
var finalData = Data()
var eof = false
var error: Error?
while !eof {
let group = DispatchGroup()
group.enter()
dataTask.readData(ofMinLength: 1, maxLength: 65535, timeout: timeout, completionHandler: { (data, seof, serror) in
if let data = data {
finalData.append(data)
do {
while !eof {
let group = DispatchGroup()
group.enter()
dataTask.readData(ofMinLength: 1, maxLength: 65535, timeout: timeout, completionHandler: { (data, seof, serror) in
if let data = data {
finalData.append(data)
}
eof = seof
error = serror
group.leave()
})
let waitResult = group.wait(timeout: .now() + timeout)
if let error = error {
if !((error as NSError).domain == URLError.errorDomain && (error as NSError).code == URLError.cancelled.rawValue) {
throw error
}
return
}
eof = seof
error = serror
group.leave()
})
let waitResult = group.wait(timeout: .now() + timeout)
if let error = error {
if !((error as NSError).domain == URLError.errorDomain && (error as NSError).code == URLError.cancelled.rawValue) {
completionHandler([], error)
if waitResult == .timedOut {
throw self.urlError(path, code: .timedOut)
}
return
}
if waitResult == .timedOut {
completionHandler([], self.throwError(path, code: URLError.timedOut))
return
guard let response = String(data: finalData, encoding: .utf8) else {
throw self.urlError(path, code: .badServerResponse)
}
let contents: [String] = response.components(separatedBy: "\n").flatMap({ $0.trimmingCharacters(in: .whitespacesAndNewlines) })
success = true
completionHandler(contents, nil)
} catch {
completionHandler([], error)
}
guard let response = String(data: finalData, encoding: .utf8) else {
completionHandler([], self.throwError(path, code: URLError.badServerResponse))
return
}
let contents: [String] = response.components(separatedBy: "\n").flatMap({ $0.trimmingCharacters(in: .whitespacesAndNewlines) })
success = true
completionHandler(contents, nil)
return
}
}) { (response, error) in
if let error = error {
completionHandler([], error)
return
}
guard let response = response else {
completionHandler([], self.throwError(path, code: URLError.cannotParseResponse))
return
}
if response.hasPrefix("500") && useMLST {
dataTask.cancel()
self.serverSupportsRFC3659 = false
completionHandler([], self.throwError(path, code: URLError.unsupportedURL))
return
}
if !success && !(response.hasPrefix("25") || response.hasPrefix("15")) {
let error = FileProviderFTPError(message: response, path: path)
do {
if let error = error {
throw error
}
guard let response = response else {
throw self.urlError(path, code: .cannotParseResponse)
}
if response.hasPrefix("500") && useMLST {
dataTask.cancel()
self.serverSupportsRFC3659 = false
throw self.urlError(path, code: .unsupportedURL)
}
if !success && !(response.hasPrefix("25") || response.hasPrefix("15")) {
throw FileProviderFTPError(message: response, path: path)
}
} catch {
self.dispatch_queue.async {
completionHandler([], error)
}
return
}
}
}
}
func recursiveList(path: String, useMLST: Bool, foundItemsHandler: ((_ contents: [FileObject]) -> Void)? = nil, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: 0)
let progress = Progress(totalUnitCount: -1)
let queue = DispatchQueue(label: "\(self.type).recursiveList")
queue.async {
let group = DispatchGroup()
@@ -454,7 +449,7 @@ internal extension FTPFileProvider {
func ftpRetrieveData(_ task: FileProviderStreamTask, filePath: String, from position: Int64 = 0, length: Int = -1, onTask: ((_ task: FileProviderStreamTask) -> Void)?, onProgress: ((_ bytesReceived: Int64, _ totalReceived: Int64, _ expectedBytes: Int64) -> Void)?, completionHandler: @escaping (_ data: Data?, _ error: Error?) -> Void) {
// Check cache
if useCache, let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL, let cachedResponse = self.cache?.cachedResponse(for: URLRequest(url: url)), cachedResponse.data.count > 0 {
if useCache, let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL, let cachedResponse = self.cache?.cachedResponse(for: URLRequest(url: url)), cachedResponse.data.count > 0 {
dispatch_queue.async {
completionHandler(cachedResponse.data, nil)
}
@@ -471,12 +466,12 @@ internal extension FTPFileProvider {
}
guard let dataTask = dataTask else {
completionHandler(nil, self.throwError(filePath, code: URLError.badServerResponse))
completionHandler(nil, self.urlError(filePath, code: .badServerResponse))
return
}
// Send retreive command
let len = 19 /* TYPE response */ + 65 + String(position).characters.count /* REST Response */ + 53 + filePath.characters.count + String(totalSize).characters.count /* RETR open response */ + 26 /* RETR Transfer complete message. */
let len = 19 /* TYPE response */ + 65 + String(position).count /* REST Response */ + 53 + filePath.count + String(totalSize).count /* RETR open response */ + 26 /* RETR Transfer complete message. */
self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "RETR \(filePath)", on: task, minLength: len, afterSend: { error in
// starting passive task
onTask?(dataTask)
@@ -509,12 +504,12 @@ internal extension FTPFileProvider {
}
if waitResult == .timedOut {
completionHandler(nil, self.throwError(filePath, code: URLError.timedOut))
completionHandler(nil, self.urlError(filePath, code: .timedOut))
return
}
}
if let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL {
if let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL {
let urlresponse = URLResponse(url: url, mimeType: nil, expectedContentLength: finalData.count, textEncodingName: nil)
let cachedResponse = CachedURLResponse(response: urlresponse, data: finalData)
let request = URLRequest(url: url)
@@ -525,23 +520,22 @@ internal extension FTPFileProvider {
return
}
}) { (response, error) in
if let error = error {
completionHandler(nil, error)
return
}
guard let response = response else {
completionHandler(nil, self.throwError(filePath, code: URLError.cannotParseResponse))
return
}
if !(response.hasPrefix("1") || !response.hasPrefix("2")) {
let error = FileProviderFTPError(message: response)
do {
if let error = error {
throw error
}
guard let response = response else {
throw self.urlError(filePath, code: .cannotParseResponse)
}
if !(response.hasPrefix("1") || !response.hasPrefix("2")) {
throw FileProviderFTPError(message: response)
}
} catch {
self.dispatch_queue.async {
completionHandler(nil, error)
}
return
}
}
}
@@ -552,7 +546,7 @@ internal extension FTPFileProvider {
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).appendingPathExtension("tmp")
// Check cache
if useCache, let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL, let cachedResponse = self.cache?.cachedResponse(for: URLRequest(url: url)), cachedResponse.data.count > 0 {
if useCache, let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL, let cachedResponse = self.cache?.cachedResponse(for: URLRequest(url: url)), cachedResponse.data.count > 0 {
dispatch_queue.async {
do {
try cachedResponse.data.write(to: tempURL)
@@ -575,12 +569,12 @@ internal extension FTPFileProvider {
}
guard let dataTask = dataTask else {
completionHandler(nil, self.throwError(filePath, code: URLError.badServerResponse))
completionHandler(nil, self.urlError(filePath, code: .badServerResponse))
return
}
// Send retreive command
let len = 19 /* TYPE response */ + 65 + String(position).characters.count /* REST Response */ + 53 + filePath.characters.count + String(totalSize).characters.count /* RETR open response */ + 26 /* RETR Transfer complete message. */
let len = 19 /* TYPE response */ + 65 + String(position).count /* REST Response */ + 53 + filePath.count + String(totalSize).count /* RETR open response */ + 26 /* RETR Transfer complete message. */
self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "RETR \(filePath)", on: task, minLength: len, afterSend: { error in
// starting passive task
onTask?(dataTask)
@@ -613,13 +607,13 @@ internal extension FTPFileProvider {
}
if waitResult == .timedOut {
error = self.throwError("", code: URLError.timedOut)
error = self.urlError("", code: .timedOut)
completionHandler(nil, error)
return
}
}
if let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL {
if let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL {
let urlresponse = URLResponse(url: url, mimeType: nil, expectedContentLength: finalData.count, textEncodingName: nil)
let cachedResponse = CachedURLResponse(response: urlresponse, data: finalData)
let request = URLRequest(url: url)
@@ -630,31 +624,32 @@ internal extension FTPFileProvider {
do {
try finalData.write(to: tempURL)
completionHandler(tempURL, nil)
// Removing temporary file after coordinating
NSFileCoordinator().coordinate(writingItemAt: tempURL, options: .forDeleting, error: nil, byAccessor: { (tempURL) in
try? FileManager.default.removeItem(at: tempURL)
})
} catch {
completionHandler(nil, error)
}
try? FileManager.default.removeItem(at: tempURL)
}
return
}
}) { (response, error) in
if let error = error {
completionHandler(nil, error)
return
}
guard let response = response else {
completionHandler(nil, self.throwError(filePath, code: URLError.cannotParseResponse))
return
}
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
let error = FileProviderFTPError(message: response)
do {
if let error = error {
throw error
}
guard let response = response else {
throw self.urlError(filePath, code: .cannotParseResponse)
}
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
throw FileProviderFTPError(message: response)
}
} catch {
self.dispatch_queue.async {
completionHandler(nil, error)
}
return
}
}
}
@@ -669,11 +664,11 @@ internal extension FTPFileProvider {
var error: Error?
let chunkSize: Int
switch size {
case 0..<262_144: chunkSize = 32_768 // 0KB To 256KB, page size is 32KB
case 262_144..<1_048_576: chunkSize = 65_536 // 256KB To 1MB, page size is 64KB
case 1_048_576..<10_485_760: chunkSize = 131_072 // 1MB To 10MB, page size is 128KB
case 10_048_576..<33_554_432: chunkSize = 262_144 // 1MB To 10MB, page size is 256KB
default: chunkSize = 524_288 // Larger than 32MB, page size is 512KB
case 0..<262_144: chunkSize = 32_768 // 0KB To 256KB, chunk size is 32KB
case 262_144..<1_048_576: chunkSize = 65_536 // 256KB To 1MB, chunk size is 64KB
case 1_048_576..<10_485_760: chunkSize = 131_072 // 1MB To 10MB, chunk size is 128KB
case 10_048_576..<33_554_432: chunkSize = 262_144 // 10MB To 32MB, chunk size is 256KB
default: chunkSize = 524_288 // Larger than 32MB, chunk size is 512KB
}
var fileHandle: FileHandle?
@@ -711,12 +706,13 @@ internal extension FTPFileProvider {
})
let waitResult = group.wait(timeout: .now() + timeout)
if waitResult == .timedOut {
error = self.throwError(filePath, code: URLError.timedOut)
if let error = error {
completionHandler(error)
return
}
if let error = error {
if waitResult == .timedOut {
error = self.urlError(filePath, code: .timedOut)
completionHandler(error)
return
}
@@ -733,19 +729,16 @@ internal extension FTPFileProvider {
}
guard let dataTask = dataTask else {
completionHandler(self.throwError(filePath, code: URLError.badServerResponse))
completionHandler(self.urlError(filePath, code: .badServerResponse))
return
}
// Send retreive command
var success = false
let len = 19 /* TYPE response */ + 65 + String(position).characters.count /* REST Response */ + 44 + filePath.characters.count /* STOR open response */ + 10 /* RETR Transfer complete message. */
let len = 19 /* TYPE response */ + 65 + String(position).count /* REST Response */ + 44 + filePath.count /* STOR open response */ + 10 /* RETR Transfer complete message. */
self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "STOR \(filePath)", on: task, minLength: len, afterSend: { error in
// starting passive task
let timeout = self.session.configuration.timeoutIntervalForRequest
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
task.startSecureConnection()
}
onTask?(dataTask)
if data.count == 0 { return }
@@ -763,26 +756,25 @@ internal extension FTPFileProvider {
}) { (response, error) in
guard success else { return }
if let error = error {
completionHandler(error)
return
}
guard let response = response else {
completionHandler(self.throwError(filePath, code: URLError.cannotParseResponse))
return
}
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
let error = FileProviderFTPError(message: response)
do {
if let error = error {
throw error
}
guard let response = response else {
throw self.urlError(filePath, code: .cannotParseResponse)
}
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
throw FileProviderFTPError(message: response)
}
completionHandler(nil)
} catch {
self.dispatch_queue.async {
completionHandler(error)
}
return
}
completionHandler(nil)
}
}
}
@@ -795,22 +787,13 @@ internal extension FTPFileProvider {
}
func ftpPath(_ apath: String) -> String {
var path = apath.isEmpty ? self.currentPath : apath
// path of base url should be concreted into file path!
path = baseURL!.appendingPathComponent(path).path
// path of base url should be concreted into file path! And remove final slash
var path = baseURL!.appendingPathComponent(apath).path.replacingOccurrences(of: "/", with: "", options: [.anchored, .backwards])
// Fixing slashes
if !path.hasPrefix("/") {
path = "/" + path
}
if path.hasSuffix("/"){
path.characters.removeLast()
}
if path.isEmpty {
path = "/"
}
return path
}
@@ -838,13 +821,15 @@ internal extension FTPFileProvider {
let name = components[8..<components.count].joined(separator: " ")
guard name != "." && name != ".." else { return nil }
var path = (path as NSString).appendingPathComponent(name)
if path.hasPrefix("/") {
path.characters.removeFirst()
}
let path = (path as NSString).appendingPathComponent(name).replacingOccurrences(of: "/", with: "", options: .anchored)
let file = FileObject(url: url(of: path), name: name, path: path)
switch String(posixPermission.characters.first!) {
#if swift(>=4.0)
let typeChar = posixPermission.first ?? Character(" ")
#else
let typeChar = posixPermission.characters.first ?? Character(" ")
#endif
switch String(typeChar) {
case "d": file.type = .directory
case "l": file.type = .symbolicLink
default: file.type = .regular
@@ -880,9 +865,7 @@ internal extension FTPFileProvider {
name = nameOrPath
correctedPath = (path as NSString).appendingPathComponent(nameOrPath)
}
if correctedPath.hasPrefix("/") {
correctedPath.characters.removeFirst()
}
correctedPath = correctedPath.replacingOccurrences(of: "/", with: "", options: .anchored)
var attributes = [String: String]()
for component in components {
@@ -953,11 +936,20 @@ public struct FileProviderFTPError: Error {
init(message response: String, path: String = "") {
let message = response.components(separatedBy: .newlines).last ?? "No Response"
let spaceIndex = message.characters.index(of: "-") ?? message.characters.index(of: " ") ?? message.startIndex
self.code = Int(message.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
#if swift(>=4.0)
let startIndex = (message.index(of: "-") ?? message.index(of: " ")) ?? message.startIndex
self.code = Int(message[..<startIndex].trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
#else
let startIndex = (message.characters.index(of: "-") ?? message.characters.index(of: " ")) ?? message.startIndex
self.code = Int(message.substring(to: startIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
#endif
self.path = path
if code > 0 {
self.errorDescription = message.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
#if swift(>=4.0)
self.errorDescription = message[startIndex...].trimmingCharacters(in: .whitespacesAndNewlines)
#else
self.errorDescription = message.substring(from: startIndex).trimmingCharacters(in: .whitespacesAndNewlines)
#endif
} else {
self.errorDescription = message
}
+105 -4
View File
@@ -33,7 +33,7 @@ open class FileObject: Equatable {
if let url = allValues[.fileURLKey] as? URL {
return url
} else {
let path = self.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self.path
let path = self.path.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? self.path
return URL(string: path) ?? URL(string: "/")!
}
}
@@ -72,6 +72,16 @@ open class FileObject: Equatable {
}
}
/// Count of children items of a driectory.
open internal(set) var childrensCount: Int? {
get {
return allValues[.childrensCount] as? Int
}
set {
allValues[.childrensCount] = newValue
}
}
/// The time contents of file has been created, returns nil if not set
open internal(set) var creationDate: Date? {
get {
@@ -93,9 +103,9 @@ open class FileObject: Equatable {
}
/// return resource type of file, usually directory, regular or symLink
open internal(set) var type: URLFileResourceType? {
open internal(set) var type: URLFileResourceType {
get {
return allValues[.fileResourceTypeKey] as? URLFileResourceType
return allValues[.fileResourceTypeKey] as? URLFileResourceType ?? .unknown
}
set {
allValues[.fileResourceTypeKey] = newValue
@@ -174,7 +184,7 @@ open class FileObject: Equatable {
result["isDirectory"] = self.isDirectory
result["isRegularFile"] = self.isRegularFile
result["isSymLink"] = self.isSymLink
result["type"] = typeDict[self.type ?? .unknown] ?? "unknown"
result["type"] = typeDict[self.type] ?? "unknown"
return result
}
@@ -207,6 +217,97 @@ open class FileObject: Equatable {
}
}
/// Containts attributes of a provider.
open class VolumeObject {
/// A `Dictionary` contains volume information, using `URLResourceKey` keys.
open internal(set) var allValues: [URLResourceKey: Any]
public init(allValues: [URLResourceKey: Any]) {
self.allValues = allValues
}
/// The root directory of the resources volume, returned as an `URL` object.
open internal(set) var url: URL? {
get {
return allValues[.volumeURLKey] as? URL
}
set {
allValues[.volumeURLKey] = newValue
}
}
/// The name of the volume.
open internal(set) var name: String? {
get {
return allValues[.volumeNameKey] as? String
}
set {
allValues[.volumeNameKey] = newValue
}
}
/// the volumes capacity in bytes, return -1 if is undetermined.
open internal(set) var totalCapacity: Int64 {
get {
return allValues[.volumeTotalCapacityKey] as? Int64 ?? -1
}
set {
allValues[.volumeTotalCapacityKey] = newValue
}
}
/// The volumes available capacity in bytes.
open internal(set) var availableCapacity: Int64 {
get {
return allValues[.volumeAvailableCapacityKey] as? Int64 ?? 0
}
set {
allValues[.volumeAvailableCapacityKey] = newValue
}
}
open internal(set) var usage: Int64 {
get {
return totalCapacity >= 0 ? totalCapacity - availableCapacity : -availableCapacity
}
set {
availableCapacity = totalCapacity >= 0 ? totalCapacity - newValue : -newValue
}
}
/// the volumes creation date, returned as an `Date` object, or NULL if it cannot be determined
open internal(set) var creationDate: Date? {
get {
return allValues[.volumeCreationDateKey] as? Date
}
set {
allValues[.volumeCreationDateKey] = newValue
}
}
/// Determining whether the volume is read-only
open internal(set) var isReadOnly: Bool {
get {
return allValues[.volumeIsReadOnlyKey] as? Bool ?? false
}
set {
allValues[.volumeIsReadOnlyKey] = newValue
}
}
@available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
open internal(set) var isEncrypted: Bool {
get {
return allValues[.volumeIsEncryptedKey] as? Bool ?? false
}
set {
allValues[.volumeIsEncryptedKey] = !newValue
}
}
}
/// Sorting FileObject array by given criteria, **not thread-safe**
public struct FileObjectSorting {
+98 -99
View File
@@ -29,9 +29,6 @@ public protocol FileProviderBasic: class, NSSecureCoding {
/// 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.
@@ -64,28 +61,31 @@ public protocol FileProviderBasic: class, NSSecureCoding {
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 closure with result of directory entries or error.
- `contents`: An array of `FileObject` identifying the the directory entries.
- `error`: Error returned by system.
- Parameters:
- path: path to target directory. If empty, root will be iterated.
- completionHandler: a closure 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))
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 closure with result of directory entries or error.
- `attributes`: A `FileObject` containing the attributes of the item.
- `error`: Error returned by system.
- Parameters:
- path: path to target directory. If empty, attributes of root will be returned.
- completionHandler: a closure 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))
func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void)
/// Returns total and used capacity in provider container asynchronously.
func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void))
/// Returns volume/provider information asynchronously.
/// - Parameter volumeInfo: Information of filesystem/Provider returned by system/server.
func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void)
/**
Search files inside directory using query asynchronously.
@@ -98,9 +98,11 @@ public protocol FileProviderBasic: class, NSSecureCoding {
- query: Simple string that file name begins with to be search, case-insensitive.
- foundItemHandler: Closure which is called when a file is found
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
- files: all files meat the `query` criteria.
- error: `Error` returned by server if occured.
*/
@discardableResult
func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress?
func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress?
/**
Search files inside directory using query asynchronously.
@@ -122,10 +124,12 @@ public protocol FileProviderBasic: class, NSSecureCoding {
- query: An `NSPredicate` object with keys like `FileObject` members, except `size` which becomes `filesize`.
- foundItemHandler: Closure which is called when a file is found
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
- Returns: An `Progress` to get progress or cancel progress.
- files: all files meat the `query` criteria.
- error: `Error` returned by server if occured.
- Returns: An `Progress` to get progress or cancel progress. Use `completedUnitCount` to iterate count of found items.
*/
@discardableResult
func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress?
func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress?
/**
Returns an independent url to access the file. Some providers like `Dropbox` due to their nature.
@@ -133,7 +137,7 @@ public protocol FileProviderBasic: class, NSSecureCoding {
- 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
func url(of path: String) -> URL
/// Returns the relative path of url, wothout percent encoding. Even if url is absolute or
@@ -145,11 +149,15 @@ public protocol FileProviderBasic: class, NSSecureCoding {
func relativePathOf(url: URL) -> String
/// Checks the connection to server or permission on local
///
/// - Note: To prevent race condition, use this method wisely and avoid it as far possible.
///
/// - Parameter success: indicated server is reachable or not.
func isReachable(completionHandler: @escaping(_ success: Bool) -> Void)
}
extension FileProviderBasic {
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
let predicate = NSPredicate(format: "name BEGINSWITH[c] %@", query)
return self.searchFiles(path: path, recursive: recursive, query: predicate, foundItemHandler: foundItemHandler, completionHandler: completionHandler)
}
@@ -165,6 +173,14 @@ extension FileProviderBasic {
operation_queue.maxConcurrentOperationCount = newValue
}
}
/// Returns total and used capacity in provider container asynchronously.
@available(*, deprecated, message: "Use storageProperties which returns VolumeObject")
func storageProperties(completionHandler: @escaping (_ total: Int64, _ used: Int64) -> Void) {
self.storageProperties { (info) in
completionHandler(info?.totalCapacity ?? -1, info?.usage ?? 0)
}
}
}
/// Checking equality of two file provider, regardless of current path queues and delegates.
@@ -418,6 +434,24 @@ public extension FileProviderOperations {
}
}
internal extension FileProviderOperations {
internal func delegateNotify(_ operation: FileOperationType, error: Error? = nil) {
DispatchQueue.main.async(execute: {
if let error = error {
self.delegate?.fileproviderFailed(self, operation: operation, error: error)
} else {
self.delegate?.fileproviderSucceed(self, operation: operation)
}
})
}
internal func delegateNotify(_ operation: FileOperationType, progress: Double) {
DispatchQueue.main.async(execute: {
self.delegate?.fileproviderProgress(self, operation: operation, progress: Float(progress))
})
}
}
/// Defines method for fetching and modifying file contents
public protocol FileProviderReadWrite: FileProviderBasic {
/**
@@ -427,12 +461,12 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- Parameters:
- path: Path of file.
- completionHandler: a closure with result of file contents or error.
- `contents`: contents of file in a `Data` object.
- `error`: Error returned by system.
- contents: contents of file in a `Data` object.
- error: `Error` returned by system if occured.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress?
func contents(path: String, completionHandler: @escaping (_ contents: Data?, _ error: Error?) -> Void) -> Progress?
/**
Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler.
@@ -443,12 +477,12 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- 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 closure with result of file contents or error.
- `contents`: contents of file in a `Data` object.
- `error`: Error returned by system.
- contents: contents of file in a `Data` object.
- error: Error returned by system if occured.
- Returns: An `Progress` 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)) -> Progress?
func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping (_ contents: Data?, _ error: Error?) -> Void) -> Progress?
/**
Write the contents of the `Data` to a location asynchronously.
@@ -547,7 +581,7 @@ public protocol FileProviderMonitor: FileProviderBasic {
- path: path of directory.
- eventHandler: Closure executed after change, on a secondary thread.
*/
func registerNotifcation(path: String, eventHandler: @escaping (() -> Void))
func registerNotifcation(path: String, eventHandler: @escaping () -> Void)
/// Stops monitoring the path.
///
@@ -570,8 +604,10 @@ public protocol FileProvideUndoable: FileProviderOperations {
var undoManager: UndoManager? { get set }
/// UndoManager supports undoing this file operation
/// - Parameter handle: determines wheither this progress can be rolled back or not.
func canUndo(handle: Progress) -> Bool
/// UndoManager supports undoing this operation
/// - Parameter operation: determines wheither this operation can be rolled back or not.
func canUndo(operation: FileOperationType) -> Bool
}
@@ -587,6 +623,7 @@ public extension FileProvideUndoable {
return false
}
/// Reuturns roll back operation for provided `operation`.
internal func undoOperation(for operation: FileOperationType) -> FileOperationType? {
switch operation {
case .create(path: let path):
@@ -625,12 +662,12 @@ public protocol FileProviderSharing {
- Parameters:
- to: path of file, including file/directory name.
- completionHandler: a closure with result of directory entries or error.
- `link`: a url returned by Dropbox to share.
- `attribute`: a `FileObject` containing the attributes of the item.
- `expiration`: a `Date` object, determines when the public url will expires.
- `error`: Error returned by server.
- link: a url returned by Dropbox to share.
- attribute: a `FileObject` containing the attributes of the item.
- expiration: a `Date` object, determines when the public url will expires.
- error: Error returned by server.
*/
func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void))
func publicLink(to path: String, completionHandler: @escaping (_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)
}
/// Defines protocol for provider allows all common operations.
@@ -647,16 +684,16 @@ extension FileProviderBasic {
#endif
}
public func url(of path: String? = nil) -> URL {
var rpath: String = path ?? self.currentPath
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath
public func url(of path: String) -> URL {
var rpath: String = path
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? rpath
if let baseURL = baseURL {
if rpath.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
}
return URL(string: rpath, relativeTo: baseURL) ?? baseURL
} else {
return URL(string: rpath)!
return URL(string: rpath) ?? URL(string: "/")!
}
}
@@ -685,18 +722,11 @@ extension FileProviderBasic {
}
}
internal func correctPath(_ path: String?) -> String? {
guard let path = path else { return nil }
var p = path.hasPrefix("/") ? path : "/" + path
if p.hasSuffix("/") {
p.remove(at: p.index(before:p.endIndex))
}
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.
/// - Important: It's a synchronous method. Don't use it on main thread.
/// - Parameter filePath: supposed path of file which should be examined.
public func fileByUniqueName(_ filePath: String) -> String {
//assert(!Thread.isMainThread, "\(#function) is not recommended to be executed on Main Thread.")
let fileUrl = URL(fileURLWithPath: filePath)
let dirPath = fileUrl.deletingLastPathComponent().path
let fileName = fileUrl.deletingPathExtension().lastPathComponent
@@ -732,18 +762,14 @@ extension FileProviderBasic {
return (dirPath as NSString).appendingPathComponent(finalFile)
}
internal func throwError(_ path: String, code: FoundationErrorEnum) -> NSError {
internal func urlError(_ path: String, code: URLError.Code) -> Error {
let fileURL = self.url(of: path)
let domain: String
switch code {
case is URLError:
fallthrough
case is URLError.Code:
domain = NSURLErrorDomain
default:
domain = NSCocoaErrorDomain
}
return NSError(domain: domain, code: code.rawValue, userInfo: [NSURLErrorFailingURLErrorKey: fileURL, NSURLErrorFailingURLStringErrorKey: fileURL.absoluteString])
return URLError(code, userInfo: [NSURLErrorKey: fileURL, NSURLErrorFailingURLErrorKey: fileURL, NSURLErrorFailingURLStringErrorKey: fileURL.absoluteString])
}
internal func cocoaError(_ path: String, code: CocoaError.Code) -> Error {
let fileURL = self.url(of: path)
return CocoaError(code, userInfo: [NSFilePathErrorKey: path, NSURLErrorKey: fileURL])
}
internal func NotImplemented(_ fn: String = #function, file: StaticString = #file) {
@@ -767,19 +793,19 @@ public protocol ExtendedFileProvider: FileProviderBasic {
func propertiesOfFileSupported(path: String) -> Bool
/**
Generates ans returns a thumbnail preview of document asynchronously. The defualt dimension of returned image is different
Generates and 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 closure with result of preview image or error.
- `image`: `NSImage`/`UIImage` object contains preview.
- `error`: Error returned by system.
- image: `NSImage`/`UIImage` object contains preview.
- error: `Error` returned by system.
*/
func thumbnailOfFile(path: String, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void))
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
Generates and 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
@@ -789,10 +815,10 @@ public protocol ExtendedFileProvider: FileProviderBasic {
- path: path of file.
- dimension: width and height of result preview image.
- completionHandler: a closure with result of preview image or error.
- `image`: `NSImage`/`UIImage` object contains preview.
- `error`: Error returned by system.
- image: `NSImage`/`UIImage` object contains preview.
- error: `Error` returned by system.
*/
func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void))
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.
@@ -804,11 +830,11 @@ public protocol ExtendedFileProvider: FileProviderBasic {
- Parameters:
- path: path of file.
- completionHandler: a closure 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.
- 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))
func propertiesOfFile(path: String, completionHandler: @escaping (_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void)
}
extension ExtendedFileProvider {
@@ -1011,26 +1037,8 @@ public enum FileOperationType: CustomStringConvertible {
}
/// Allows to get progress or cancel an in-progress operation, useful for remote providers
@available(*, obsoleted: 1.0, message: "Use Progress class class instead.")
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
}
@available(*, obsoleted: 1.0, message: "Use Foudation.Progress class instead.")
public protocol OperationHandle {}
/// Delegate methods for reporting provider's operation result and progress, when it's ready to update
/// user interface.
@@ -1041,7 +1049,7 @@ public protocol FileProviderDelegate: class {
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)
func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperationType, error: Error)
/// 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.
@@ -1057,12 +1065,3 @@ public protocol FileOperationDelegate: class {
/// fileProvider(_:shouldProceedAfterError:copyingItemAtPath:toPath:) gives the delegate an opportunity to recover from or continue copying after an error. If an error occurs, the error object will contain an ErrorType indicating the problem. The source path and destination paths are also provided. If this method returns true, the FileProvider instance will continue as if the error had not occurred. If this method returns false, the NSFileManager instance will stop copying, return false from copyItemAtPath:toPath:error: and the error will be provied there.
func fileProvider(_ fileProvider: FileProviderOperations, shouldProceedAfterError error: Error, operation: FileOperationType) -> Bool
}
/// For internal use in `FileProvider` framework
public protocol FoundationErrorEnum {
/// Init from error code
init? (rawValue: Int)
// Raw error code
var rawValue: Int { get }
}
-302
View File
@@ -1,302 +0,0 @@
//
// FileProviderExtensions.swift
// FileProvider
//
// Created by Amir Abbas on 12/27/1395 AP.
//
//
import Foundation
public extension Array where Element: FileObject {
/// Returns a sorted array of `FileObject`s by criterias set in attributes.
public func sort(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 attributes.
public mutating func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) {
self = self.sort(by: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
}
}
public extension URLFileResourceType {
/// **FileProvider** returns corresponding `URLFileResourceType` of a `FileAttributeType` value
public init(fileTypeValue: FileAttributeType) {
switch fileTypeValue {
case FileAttributeType.typeCharacterSpecial: self = .characterSpecial
case FileAttributeType.typeDirectory: self = .directory
case FileAttributeType.typeBlockSpecial: self = .blockSpecial
case FileAttributeType.typeRegular: self = .regular
case FileAttributeType.typeSymbolicLink: self = .symbolicLink
case FileAttributeType.typeSocket: self = .socket
case FileAttributeType.typeUnknown: self = .unknown
default: self = .unknown
}
}
}
public extension URLResourceKey {
/// **FileProvider** returns url of file object.
public static let fileURLKey = URLResourceKey(rawValue: "NSURLFileURLKey")
/// **FileProvider** returns modification date of file in server
public static let serverDateKey = URLResourceKey(rawValue: "NSURLServerDateKey")
/// **FileProvider** returns HTTP ETag string of remote resource
public static let entryTagKey = URLResourceKey(rawValue: "NSURLEntryTagKey")
/// **FileProvider** returns MIME type of file, if returned by server
public static let mimeTypeKey = URLResourceKey(rawValue: "NSURLMIMETypeIdentifierKey")
/// **FileProvider** returns either file is encrypted or not
public static let isEncryptedKey = URLResourceKey(rawValue: "NSURLIsEncryptedKey")
}
public extension ProgressUserInfoKey {
/// **FileProvider** returns associated `FileProviderOperationType`
public static let fileProvderOperationTypeKey = ProgressUserInfoKey("FilesProviderOperationTypeKey")
/// **FileProvider** returns start date/time of operation
public static let startingTimeKey = ProgressUserInfoKey("NSProgressStartingTimeKey")
}
internal extension URL {
var uw_scheme: String {
return self.scheme ?? ""
}
var fileIsDirectory: Bool {
return (try? self.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
}
var fileSize: Int64 {
return Int64((try? self.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1)
}
var fileExists: Bool {
return self.isFileURL && FileManager.default.fileExists(atPath: self.path)
}
}
internal extension URLRequest {
mutating func set(httpAuthentication credential: URLCredential?, with type: HTTPAuthenticationType) {
func base64(_ str: String) -> String {
let plainData = str.data(using: .utf8)
let base64String = plainData!.base64EncodedString(options: [])
return base64String
}
guard let credential = credential else { return }
switch type {
case .basic:
let authStr = "\(credential.user ?? ""):\(credential.password ?? "")"
self.setValue("Basic \(authStr)", forHTTPHeaderField: "Authorization")
case .digest:
// handled by RemoteSessionDelegate
break
case .oAuth1:
if let oauth = credential.password {
self.setValue("OAuth \(oauth)", forHTTPHeaderField: "Authorization")
}
case .oAuth2:
if let bearer = credential.password {
self.setValue("Bearer \(bearer)", forHTTPHeaderField: "Authorization")
}
}
}
mutating func set(rangeWithOffset offset: Int64, length: Int) {
if length > 0 {
self.setValue("bytes=\(offset)-\(offset + Int64(length) - 1)", forHTTPHeaderField: "Range")
} else if offset > 0 && length < 0 {
self.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
}
}
enum ContentType: String {
case json = "application/json"
case stream = "application/octet-stream"
case xml = "text/xml; charset=\"utf-8\""
}
mutating func set(contentType: ContentType) {
self.setValue(contentType.rawValue, forHTTPHeaderField: "Content-Type")
}
private func asciiEscape(string:String) -> String {
var res = ""
for char in string.unicodeScalars {
let substring = String(char)
if substring.canBeConverted(to: .ascii) {
res.append(substring)
} else {
res = res.appendingFormat("\\u%04x", char.value)
}
}
return res
}
mutating func set(dropboxArgKey requestDictionary: [String: AnyObject]) {
guard let jsonData = try? JSONSerialization.data(withJSONObject: requestDictionary, options: []) else {
return
}
guard var jsonString = String(data: jsonData, encoding: .utf8) else { return }
jsonString = self.asciiEscape(string: jsonString)
jsonString = jsonString.replacingOccurrences(of: "\\/", with: "/")
self.setValue(jsonString, forHTTPHeaderField: "Dropbox-API-Arg")
}
}
internal extension Data {
internal var isPDF: Bool {
return self.count > 4 && self.scanString(length: 4, using: .ascii) == "%PDF"
}
init? (jsonDictionary dictionary: [String: AnyObject]) {
guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else {
return nil
}
self = data
}
func deserializeJSON() -> [String: AnyObject]? {
if let dic = try? JSONSerialization.jsonObject(with: self, options: []) as? [String: AnyObject] {
return dic
}
return nil
}
init<T>(value: T) {
var value = value
self = Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
}
func scanValue<T>() -> T? {
guard MemoryLayout<T>.size <= self.count else { return nil }
return self.withUnsafeBytes { $0.pointee }
}
func scanValue<T>(start: Int) -> T? {
let length = MemoryLayout<T>.size
guard self.count >= start + length else { return nil }
return self.subdata(in: start..<start+length).withUnsafeBytes { $0.pointee }
}
func scanString(start: Int = 0, length: Int, using encoding: String.Encoding = .utf8) -> String? {
guard self.count >= start + length else { return nil }
return String(data: self.subdata(in: start..<start+length), encoding: encoding)
}
static func mapMemory<T, U>(from: T) -> U? {
guard MemoryLayout<T>.size >= MemoryLayout<U>.size else { return nil }
let data = Data(value: from)
return data.scanValue()
}
}
internal extension String {
init? (jsonDictionary: [String: AnyObject]) {
guard let data = Data(jsonDictionary: jsonDictionary) else {
return nil
}
self.init(data: data, encoding: .utf8)
}
func deserializeJSON(using encoding: String.Encoding = .utf8) -> [String: AnyObject]? {
guard let data = self.data(using: encoding) else {
return nil
}
return data.deserializeJSON()
}
}
internal extension TimeInterval {
internal var formatshort: String {
var result = "0:00"
if self < TimeInterval(Int32.max) {
result = ""
var time = DateComponents()
time.hour = Int(self / 3600)
time.minute = Int((self.truncatingRemainder(dividingBy: 3600)) / 60)
time.second = Int(self.truncatingRemainder(dividingBy: 60))
let formatter = NumberFormatter()
formatter.paddingCharacter = "0"
formatter.minimumIntegerDigits = 2
formatter.maximumFractionDigits = 0
let formatterFirst = NumberFormatter()
formatterFirst.maximumFractionDigits = 0
if time.hour! > 0 {
result = "\(formatterFirst.string(from: NSNumber(value: time.hour!))!):\(formatter.string(from: NSNumber(value: time.minute!))!):\(formatter.string(from: NSNumber(value: time.second!))!)"
} else {
result = "\(formatterFirst.string(from: NSNumber(value: time.minute!))!):\(formatter.string(from: NSNumber(value: time.second!))!)"
}
}
result = result.trimmingCharacters(in: CharacterSet(charactersIn: ": "))
return result
}
}
internal extension Date {
init?(rfcString: String) {
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: rfcString) {
self = rfc3339
return
}
dateFor.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss z"
if let rfc1123 = dateFor.date(from: rfcString) {
self = rfc1123
return
}
dateFor.dateFormat = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
if let rfc850 = dateFor.date(from: rfcString) {
self = rfc850
return
}
dateFor.dateFormat = "EEE MMM d HH':'mm':'ss yyyy"
if let asctime = dateFor.date(from: rfcString) {
self = asctime
return
}
return nil
}
internal func rfc3339utc() -> 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: self)
}
}
internal extension NSPredicate {
func findValue(forKey key: String?, operator op: NSComparisonPredicate.Operator? = nil) -> Any? {
let val = findAllValues(forKey: key).lazy.filter { (op == nil || $0.operator == op!) && !$0.not }
return val.first?.value
}
func findAllValues(forKey key: String?) -> [(value: Any, operator: NSComparisonPredicate.Operator, not: Bool)] {
if let cQuery = self as? NSCompoundPredicate {
let find = cQuery.subpredicates.flatMap { ($0 as! NSPredicate).findAllValues(forKey: key) }
if cQuery.compoundPredicateType == .not {
return find.map { return ($0.value, $0.operator, !$0.not) }
}
return find
} else if let cQuery = self as? NSComparisonPredicate {
if cQuery.leftExpression.expressionType == .keyPath, key == nil || cQuery.leftExpression.keyPath == key!, let const = cQuery.rightExpression.constantValue {
return [(value: const, operator: cQuery.predicateOperatorType, false)]
}
if cQuery.rightExpression.expressionType == .keyPath, key == nil || cQuery.rightExpression.keyPath == key!, let const = cQuery.leftExpression.constantValue {
return [(value: const, operator: cQuery.predicateOperatorType, false)]
}
return []
} else {
return []
}
}
}
extension URLError.Code: FoundationErrorEnum {}
extension CocoaError.Code: FoundationErrorEnum {}
+287 -130
View File
@@ -9,15 +9,18 @@
import Foundation
/**
Allows accessing to Dropbox stored files. This provider doesn't cache or save files internally, however you can
set `useCache` and `cache` properties to use Foundation `NSURLCache` system.
The abstract base class for all REST/Web based providers such as WebDAV, Dropbox, OneDrive, Google Drive, etc. and encapsulates basic
functionalitis such as downloading/uploading.
- Note: Uploading files and data are limited to 150MB, for now.
No instance of this class should (and can) be created. Use derived classes instead. It leads to a crash with `fatalError()`.
*/
open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite {
open class var type: String { fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") }
open let baseURL: URL?
open var currentPath: String
/// **OBSOLETED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method.
@available(*, obsoleted: 0.22, message: "This property is redundant with almost no use internally.")
open var currentPath: String = ""
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue {
@@ -36,7 +39,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
public var useCache: Bool
public var validatingCache: Bool
fileprivate var _session: URLSession?
fileprivate var _session: URLSession!
internal fileprivate(set) var sessionDelegate: SessionDelegate?
public var session: URLSession {
get {
@@ -46,24 +49,25 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
config.urlCache = cache
config.requestCachePolicy = .returnCacheDataElseLoad
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: self.operation_queue)
_session!.sessionDescription = UUID().uuidString
initEmptySessionHandler(_session!.sessionDescription!)
_session.sessionDescription = UUID().uuidString
initEmptySessionHandler(_session.sessionDescription!)
}
return _session!
return _session
}
set {
assert(newValue.delegate is SessionDelegate, "session instances should have a SessionDelegate instance as delegate.")
_session = newValue
if session.sessionDescription?.isEmpty ?? true {
_session?.sessionDescription = UUID().uuidString
if _session.sessionDescription?.isEmpty ?? true {
_session.sessionDescription = UUID().uuidString
}
self.sessionDelegate = newValue.delegate as? SessionDelegate
initEmptySessionHandler(_session!.sessionDescription!)
initEmptySessionHandler(_session.sessionDescription!)
}
}
fileprivate var _longpollSession: URLSession?
/// This session has extended timeout up to 10 minutes, suitable for monitoring.
internal var longpollSession: URLSession {
if _longpollSession == nil {
let config = URLSessionConfiguration.default
@@ -73,9 +77,18 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
return _longpollSession!
}
/**
This is parent initializer for subclasses. Using this method on `HTTPFileProvider` will fail as `type` is not implemented.
- Parameters:
- baseURL: Location of WebDAV server.
- credential: An `URLCredential` object with `user` and `password`.
- cache: A URLCache to cache downloaded files and contents.
*/
public init(baseURL: URL?, credential: URLCredential?, cache: URLCache?) {
self.baseURL = baseURL
self.currentPath = ""
// Make base url absolute and path as directory
let urlStr = baseURL?.absoluteString
self.baseURL = urlStr.flatMap { $0.hasSuffix("/") ? URL(string: $0) : URL(string: $0 + "/") }
self.useCache = false
self.validatingCache = true
self.cache = cache
@@ -98,7 +111,6 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
public func encode(with aCoder: NSCoder) {
aCoder.encode(self.baseURL, forKey: "baseURL")
aCoder.encode(self.credential, forKey: "credential")
aCoder.encode(self.currentPath, forKey: "currentPath")
aCoder.encode(self.useCache, forKey: "useCache")
aCoder.encode(self.validatingCache, forKey: "validatingCache")
}
@@ -123,25 +135,25 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
}
}
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
open func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
open func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
open func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
self.storageProperties { total, _ in
completionHandler(total > 0)
self.storageProperties { volume in
completionHandler(volume != nil)
}
}
@@ -153,17 +165,133 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
}
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
return doOperation(.move(source: path, destination: toPath), overwrite: overwrite, completionHandler: completionHandler)
}
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
return doOperation(.copy(source: path, destination: toPath), overwrite: overwrite, completionHandler: completionHandler)
}
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.remove(path: path), completionHandler: completionHandler)
}
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
// check file is not a folder
guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else {
dispatch_queue.async {
completionHandler?(self.urlError(localFile.path, code: .fileIsDirectory))
}
return nil
}
let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let request = self.request(for: operation, overwrite: overwrite)
return upload_simple(toPath, request: request, localFile: localFile, operation: operation, completionHandler: completionHandler)
}
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.copy(source: path, destination: destURL.absoluteString)
let request = self.request(for: operation)
let cantLoadError = urlError(path, code: .cannotLoadFromNetwork)
return self.download_simple(path: path, request: request, operation: operation, completionHandler: { [weak self] (tempURL, error) in
do {
if let error = error {
throw error
}
guard let tempURL = tempURL else {
throw cantLoadError
}
var coordError: NSError?
NSFileCoordinator().coordinate(writingItemAt: tempURL, options: .forMoving, writingItemAt: destURL, options: .forReplacing, error: &coordError, byAccessor: { (tempURL, destURL) in
do {
try FileManager.default.moveItem(at: tempURL, to: destURL)
completionHandler?(nil)
self?.delegateNotify(operation)
} catch {
completionHandler?(error)
self?.delegateNotify(operation, error: error)
}
})
if let error = coordError {
throw error
}
} catch {
completionHandler?(error)
self?.delegateNotify(operation, error: error)
}
})
}
/**
Progressively fetch data of file and returns fetched data in `progressHandler`.
If path specifies a directory, or if some other error occurs, data will be nil.
- Parameters:
- path: Path of file.
- progressHandler: a closure called every time a new `Data` is available.
- position: start position of data fetched.
- data: a portion of contents of file in a `Data` object.
- completionHandler: a closure with result of file contents or error.
- error: `Error` returned by system if occured.
- Returns: An `Progress` to get progress or cancel progress.
*/
open func contents(path: String, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.fetch(path: path)
let request = self.request(for: operation)
var position: Int64 = 0
return download_progressive(path: path, request: request, operation: operation, progressHandler: { data in
progressHandler(position, data)
position += Int64(data.count)
}, completionHandler: (completionHandler ?? { _ in return }))
}
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
}
return nil
}
let operation = FileOperationType.fetch(path: path)
var request = self.request(for: operation)
let cantLoadError = urlError(path, code: .cannotLoadFromNetwork)
request.set(httpRangeWithOffset: offset, length: length)
return self.download_simple(path: path, request: request, operation: operation, completionHandler: { (tempURL, error) in
do {
if let error = error {
throw error
}
guard let tempURL = tempURL else {
throw cantLoadError
}
let data = try Data(contentsOf: tempURL)
completionHandler(data, nil)
} catch {
completionHandler(nil, error)
}
})
}
public func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let request = self.request(for: operation, overwrite: overwrite, attributes: [.contentModificationDateKey: Date()])
return upload_simple(path, request: request, data: data ?? Data(), operation: operation, completionHandler: completionHandler)
}
internal func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey: Any] = [:]) -> URLRequest {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
@@ -176,7 +304,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
// WebDAV will override this function
}
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
fileprivate func doOperation(_ operation: FileOperationType, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
@@ -186,7 +314,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let request = self.request(for: operation)
let request = self.request(for: operation, overwrite: overwrite)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderHTTPError?
@@ -215,122 +343,161 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
return progress
}
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
// check file is not a folder
guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else {
dispatch_queue.async {
completionHandler?(self.throwError(localFile.path, code: URLError.fileIsDirectory))
}
return nil
/// This method should be used in subclasses to fetch directory content from servers which support paginated results.
/// Almost all HTTP based provider, except WebDAV, supports this method.
///
/// - Important: Please use `[weak self]` when implementing handlers to prevent retain cycles. In these cases,
/// return `nil` as the result of handler as the operation will be aborted.
///
/// - Parameters:
/// - path: path of directory which enqueued for listing, for informational use like errpr reporting.
/// - requestHandler: Get token of next page and returns appropriate `URLRequest` to be sent to server.
/// handler can return `nil` to cancel entire operation.
/// - token: Token of the page which `URLRequest` is needed, token will be `nil` for initial page. .
/// - pageHandler: Handler which is called after fetching results of a page to parse data. will return parse result as
/// array of `FileObject` or error if data is nil or parsing is failed. Method will not continue to next page if
/// `error` is returned, otherwise `nextToken` will be used for next page. `nil` value for `newToken` will indicate
/// last page of directory contents.
/// - data: Raw data returned from server. Handler should parse them and return files.
/// - progress: `Progress` object that `completedUnits` will be increased when a new `FileObject` is parsed in method.
/// - completionHandler: All file objects returned by `pageHandler` will be passed to this handler, or error if occured.
/// This handler will be called when `pageHandler` returns `nil for `newToken`.
/// - contents: all files parsed via `pageHandler` will be return aggregated.
/// - error: `Error` returned by server. `nil` means success. If exists, it means `contents` are incomplete.
internal func paginated(_ path: String, requestHandler: @escaping (_ token: String?) -> URLRequest?, pageHandler: @escaping (_ data: Data?, _ progress: Progress) -> (files: [FileObject], error: Error?, newToken: String?), completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress {
let progress = Progress(totalUnitCount: -1)
self.paginated(path, startToken: nil, currentProgress: progress, previousResult: [], requestHandler: requestHandler, pageHandler: pageHandler, completionHandler: completionHandler)
return progress
}
// codebeat:disable[ARITY]
private func paginated(_ path: String, startToken: String?, currentProgress progress: Progress, previousResult: [FileObject], requestHandler: @escaping (_ token: String?) -> URLRequest?, pageHandler: @escaping (_ data: Data?, _ progress: Progress) -> (files: [FileObject], error: Error?, newToken: String?), completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
guard !progress.isCancelled, let request = requestHandler(startToken) else {
return
}
let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let request = self.request(for: operation, overwrite: overwrite)
return upload_simple(toPath, request: request, localFile: localFile, operation: operation, completionHandler: completionHandler)
}
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.copy(source: path, destination: destURL.absoluteString)
let request = self.request(for: operation)
return self.download_simple(path: path, request: request, operation: operation, completionHandler: { [weak self] (tempURL, error) in
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
if let error = error {
completionHandler?(error)
self?.delegateNotify(operation, error: error)
completionHandler(previousResult, error)
return
}
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
let responseError = self.serverError(with: rCode, path: path, data: data)
completionHandler(previousResult, responseError)
return
}
guard let tempURL = tempURL else {
completionHandler?(error)
self?.delegateNotify(operation, error: error)
let (newFiles, err, newToken) = pageHandler(data, progress)
if let error = err {
completionHandler(previousResult, error)
return
}
do {
try FileManager.default.moveItem(at: tempURL, to: destURL)
completionHandler?(nil)
self?.delegateNotify(operation, error: nil)
} catch let e {
completionHandler?(e)
self?.delegateNotify(operation, error: e)
let files = previousResult + newFiles
if let newToken = newToken, !progress.isCancelled {
_ = self.paginated(path, startToken: newToken, currentProgress: progress, previousResult: files, requestHandler: requestHandler, pageHandler: pageHandler, completionHandler: completionHandler)
} else {
completionHandler(files, nil)
}
})
}
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
}
return nil
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
let operation = FileOperationType.fetch(path: path)
var request = self.request(for: operation)
request.set(rangeWithOffset: offset, length: length)
return self.download_simple(path: path, request: request, operation: operation, completionHandler: { (tempURL, error) in
if let error = error {
completionHandler(nil, error)
return
}
guard let tempURL = tempURL else {
completionHandler(nil, error)
return
}
do {
let data = try Data(contentsOf: tempURL)
completionHandler(data, nil)
} catch let e {
completionHandler(nil, e)
}
})
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
}
// codebeat:enable[ARITY]
internal var maxUploadSimpleSupported: Int64 { return Int64.max }
public func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let request = self.request(for: operation, overwrite: overwrite, attributes: [.contentModificationDateKey: Date()])
return upload_simple(path, request: request, data: data ?? Data(), operation: operation, completionHandler: completionHandler)
}
internal func upload_simple(_ targetPath: String, request: URLRequest, data: Data? = nil, localFile: URL? = nil, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
let size = data?.count ?? Int((try? localFile?.resourceValues(forKeys: [.fileSizeKey]))??.fileSize ?? -1)
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
progress.totalUnitCount = Int64(size)
let task: URLSessionUploadTask
let size: Int64
if let data = data {
task = session.uploadTask(with: request, from: data)
size = Int64(data.count)
} else if let localFile = localFile {
task = session.uploadTask(with: request, fromFile: localFile)
let fSize = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.fileSize
size = Int64(fSize ?? -1)
} else {
return nil
}
if size > maxUploadSimpleSupported {
let error = self.serverError(with: .payloadTooLarge, path: targetPath, data: nil)
completionHandler?(error)
self.delegateNotify(operation, error: error)
return nil
}
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
var responseError: FileProviderHTTPError?
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
// We can't fetch server result from delegate!
responseError = self?.serverError(with: rCode, path: targetPath, data: nil)
var progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
progress.totalUnitCount = size
let taskHandler = { (task: URLSessionTask) -> Void in
completionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
var responseError: FileProviderHTTPError?
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
// We can't fetch server result from delegate!
responseError = self?.serverError(with: rCode, path: targetPath, data: nil)
}
if !(responseError == nil && error == nil) {
progress.cancel()
}
completionHandler?(responseError ?? error)
self?.delegateNotify(operation, error: responseError ?? error)
}
if !(responseError == nil && error == nil) {
task.taskDescription = operation.json
task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
}
if let data = data {
let task = session.uploadTask(with: request, from: data)
taskHandler(task)
} else if let localFile = localFile {
var error: NSError?
NSFileCoordinator().coordinate(readingItemAt: localFile, options: .forUploading, error: &error, byAccessor: { (url) in
let task = self.session.uploadTask(with: request, fromFile: localFile)
taskHandler(task)
})
if let error = error {
completionHandler?(error)
}
}
return progress
}
internal func download_progressive(path: String, request: URLRequest, operation: FileOperationType, responseHandler: ((_ response: URLResponse) -> Void)? = nil, progressHandler: @escaping (_ data: Data) -> Void, completionHandler: @escaping (_ error: Error?) -> Void) -> Progress? {
var progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.dataTask(with: request)
responseCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { response in
responseHandler?(response)
}
dataCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { data in
progressHandler(data)
}
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
progress.cancel()
}
completionHandler?(responseError ?? error)
self?.delegateNotify(operation, error: responseError ?? error)
completionHandler(error)
self.delegateNotify(operation, error: error)
}
task.taskDescription = operation.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
@@ -340,7 +507,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
}
internal func download_simple(path: String, request: URLRequest, operation: FileOperationType, completionHandler: @escaping ((_ tempURL: URL?, _ error: Error?) -> Void)) -> Progress? {
var progress = Progress(parent: nil, userInfo: nil)
var progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
@@ -351,16 +518,18 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
progress.cancel()
}
completionHandler(nil, error)
self.delegateNotify(operation, error: error)
}
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
let code = FileProviderHTTPErrorCode(rawValue: (task.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 serverError : FileProviderHTTPError? = code != nil ? self.serverError(with: code!, path: path, data: errorData) : nil
let errorData : Data? = try? Data(contentsOf: tempURL)
let serverError = code.flatMap { self.serverError(with: $0, path: path, data: errorData) }
if serverError != nil {
progress.cancel()
}
completionHandler(nil, serverError)
self.delegateNotify(operation)
return
}
@@ -378,16 +547,4 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
}
}
extension HTTPFileProvider {
internal 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)
}
})
}
}
extension HTTPFileProvider: FileProvider { }
+147 -133
View File
@@ -17,7 +17,9 @@ import Foundation
open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndoable {
open class var type: String { return "Local" }
open fileprivate(set) var baseURL: URL?
open var currentPath: String
/// **OBSOLETED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method.
@available(*, obsoleted: 0.21, message: "This property is redundant with almost no use internally.")
open var currentPath: String = ""
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue
open weak var delegate: FileProviderDelegate?
@@ -98,8 +100,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
guard baseURL.isFileURL else {
fatalError("Cannot initialize a Local provider from remote URL.")
}
self.baseURL = (baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")).absoluteURL
self.currentPath = ""
self.baseURL = URL(fileURLWithPath: baseURL.path, isDirectory: true)
self.credential = nil
self.isCoorinating = false
@@ -121,13 +122,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
return nil
}
self.init(baseURL: baseURL)
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
self.isCoorinating = aDecoder.decodeBool(forKey: "isCoorinating")
}
open func encode(with aCoder: NSCoder) {
aCoder.encode(self.baseURL, forKey: "currentPath")
aCoder.encode(self.currentPath, forKey: "currentPath")
aCoder.encode(self.isCoorinating, forKey: "isCoorinating")
}
@@ -137,7 +136,6 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
public func copy(with zone: NSZone? = nil) -> Any {
let copy = LocalFileProvider(baseURL: self.baseURL!)
copy.currentPath = self.currentPath
copy.undoManager = self.undoManager
copy.isCoorinating = self.isCoorinating
copy.delegate = self.delegate
@@ -145,7 +143,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
return copy
}
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
open func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
dispatch_queue.async {
do {
let contents = try self.fileManager.contentsOfDirectory(at: self.url(of: path), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants)
@@ -154,27 +152,31 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
return LocalFileObject(fileWithPath: path, relativeTo: self.baseURL)
})
completionHandler(filesAttributes, nil)
} catch let e {
completionHandler([], e)
} catch {
completionHandler([], error)
}
}
}
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
open func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
dispatch_queue.async {
completionHandler(LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), nil)
}
}
open func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) {
let values = try? baseURL?.resourceValues(forKeys: [.volumeTotalCapacityKey, .volumeAvailableCapacityKey])
let totalSize = Int64(values??.volumeTotalCapacity ?? -1)
let freeSize = Int64(values??.volumeAvailableCapacity ?? 0)
completionHandler(totalSize, totalSize - freeSize)
public func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) {
dispatch_queue.async {
var keys: Set<URLResourceKey> = [.volumeTotalCapacityKey, .volumeAvailableCapacityKey, .volumeURLKey, .volumeNameKey, .volumeIsReadOnlyKey, .volumeCreationDateKey]
if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) {
keys.insert(.isEncryptedKey)
}
let values: URLResourceValues? = self.baseURL.flatMap { try? $0.resourceValues(forKeys: keys) }
completionHandler(values.flatMap({ VolumeObject(allValues: $0.allValues) }))
}
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
let progress = Progress(parent: nil, userInfo: nil)
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey)
dispatch_queue.async {
@@ -201,7 +203,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
return progress
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
open func isReachable(completionHandler: @escaping (_ success: Bool) -> Void) {
dispatch_queue.async {
completionHandler(self.fileManager.isReadableFile(atPath: self.baseURL!.path))
}
@@ -211,58 +213,73 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
@discardableResult
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
return self.doOperation(opType, completionHandler: completionHandler)
let operation = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
return self.doOperation(operation, completionHandler: completionHandler)
}
@discardableResult
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.move(source: path, destination: toPath)
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum))
let operation = FileOperationType.move(source: path, destination: toPath)
let fileExists = ((try? self.url(of: toPath).checkResourceIsReachable()) ?? false) ||
((try? self.url(of: toPath).checkPromisedItemIsReachable()) ?? false)
if !overwrite && fileExists {
let e = self.cocoaError(toPath, code: .fileWriteFileExists)
dispatch_queue.async {
completionHandler?(e)
}
self.delegateNotify(operation, error: e)
return nil
}
return self.doOperation(opType, completionHandler: completionHandler)
return self.doOperation(operation, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: toPath)
let operation = FileOperationType.copy(source: path, destination: toPath)
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
self.dispatch_queue.async {
completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum))
let fileExists = ((try? self.url(of: toPath).checkResourceIsReachable()) ?? false) ||
((try? self.url(of: toPath).checkPromisedItemIsReachable()) ?? false)
if !overwrite && fileExists {
let e = self.cocoaError(toPath, code: .fileWriteFileExists)
dispatch_queue.async {
completionHandler?(e)
}
self.delegateNotify(operation, error: e)
return nil
}
return self.doOperation(opType, completionHandler: completionHandler)
return self.doOperation(operation, completionHandler: completionHandler)
}
@discardableResult
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.remove(path: path)
return self.doOperation(opType, completionHandler: completionHandler)
let operation = FileOperationType.remove(path: path)
return self.doOperation(operation, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
self.dispatch_queue.async {
completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum))
let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
let fileExists = ((try? self.url(of: toPath).checkResourceIsReachable()) ?? false) ||
((try? self.url(of: toPath).checkPromisedItemIsReachable()) ?? false)
if !overwrite && fileExists {
let e = self.cocoaError(toPath, code: .fileWriteFileExists)
dispatch_queue.async {
completionHandler?(e)
}
self.delegateNotify(operation, error: e)
return nil
}
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
return self.doOperation(opType, forUploading: true, completionHandler: completionHandler)
return self.doOperation(operation, forUploading: true, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
return self.doOperation(opType, completionHandler: completionHandler)
let operation = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
return self.doOperation(operation, completionHandler: completionHandler)
}
@objc dynamic func doSimpleOperation(_ box: UndoBox) {
@@ -273,12 +290,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
@discardableResult
fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
fileprivate func doOperation(_ operation: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey)
func urlofpath(path: String) -> URL {
if path.hasPrefix("file://") {
let removedSchemePath = path.replacingOccurrences(of: "file://", with: "", options: .anchored)
@@ -289,8 +307,8 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
}
let sourcePath = opType.source
let destPath = opType.destination
let sourcePath = operation.source
let destPath = operation.destination
let source: URL = urlofpath(path: sourcePath)
progress.setUserInfoObject(source, forKey: .fileURLKey)
@@ -301,11 +319,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
dest = nil
}
if let undoManager = self.undoManager, let undoOp = self.undoOperation(for: opType) {
let undoBox = UndoBox(provider: self, operation: opType, undoOperation: undoOp)
if let undoManager = self.undoManager, let undoOp = self.undoOperation(for: operation) {
let undoBox = UndoBox(provider: self, operation: operation, undoOperation: undoOp)
undoManager.beginUndoGrouping()
undoManager.registerUndo(withTarget: self, selector: #selector(LocalFileProvider.doSimpleOperation(_:)), object: undoBox)
undoManager.setActionName(opType.actionDescription)
undoManager.setActionName(operation.actionDescription)
undoManager.endUndoGrouping()
}
@@ -314,17 +332,17 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
let operationHandler: (URL, URL?) -> Void = { source, dest in
do {
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
switch opType {
switch operation {
case .create:
if sourcePath.hasSuffix("/") {
progress.totalUnitCount = 1
try self.opFileManager.createDirectory(at: source, withIntermediateDirectories: true, attributes: [:])
} else {
progress.totalUnitCount = Int64(data?.count ?? 0)
progress.totalUnitCount = Int64(data?.count ?? -1)
try data?.write(to: source, options: .atomic)
}
case .modify:
progress.totalUnitCount = Int64(data?.count ?? 0)
progress.totalUnitCount = Int64(data?.count ?? -1)
try data?.write(to: source, options: atomically ? [.atomic] : [])
case .copy:
guard let dest = dest else { return }
@@ -350,27 +368,23 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
self.dispatch_queue.async {
completionHandler?(nil)
}
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
}
} catch let e {
self.delegateNotify(operation)
} catch {
if successfulSecurityScopedResourceAccess {
source.stopAccessingSecurityScopedResource()
}
progress.cancel()
self.dispatch_queue.async {
completionHandler?(e)
}
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
completionHandler?(error)
}
self.delegateNotify(operation, error: error)
}
}
if isCoorinating {
successfulSecurityScopedResourceAccess = source.startAccessingSecurityScopedResource()
var intents = [NSFileAccessIntent]()
switch opType {
switch operation {
case .create, .modify:
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forReplacing))
case .copy:
@@ -386,13 +400,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
default:
return nil
}
self.coordinated(intents: intents, completionHandler: operationHandler, errorHandler: { error in
self.coordinated(intents: intents, moving: true, operationHandler: operationHandler, errorHandler: { error in
self.dispatch_queue.async {
completionHandler?(error)
}
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
self.delegateNotify(operation, error: error)
})
} else {
operation_queue.addOperation {
@@ -404,16 +416,15 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
@discardableResult
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
let opType = FileOperationType.fetch(path: path)
let operation = FileOperationType.fetch(path: path)
let url = self.url(of: path)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
let progress = Progress(totalUnitCount: url.fileSize)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey)
progress.setUserInfoObject(url, forKey: .fileURLKey)
progress.totalUnitCount = url.fileSize
let operationHandler: (URL) -> Void = { url in
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
@@ -423,23 +434,23 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
self.dispatch_queue.async {
completionHandler(data, nil)
}
} catch let e {
self.delegateNotify(operation)
} catch {
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, e)
completionHandler(nil, error)
}
self.delegateNotify(operation, error: error)
}
}
if isCoorinating {
let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges)
coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
coordinated(intents: [intent], operationHandler: operationHandler, errorHandler: { error in
self.dispatch_queue.async {
completionHandler(nil, error)
}
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
self.delegateNotify(operation, error: error)
})
} else {
dispatch_queue.async {
@@ -451,7 +462,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
@discardableResult
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping (_ contents: Data?, _ error: Error?) -> Void) -> Progress? {
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
@@ -463,61 +474,59 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
return self.contents(path: path, completionHandler: completionHandler)
}
let opType = FileOperationType.fetch(path: path)
let operation = FileOperationType.fetch(path: path)
let url = self.url(of: path)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
let progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(url, forKey: .fileURLKey)
progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey)
let operationHandler: (URL) -> Void = { url in
guard let handle = FileHandle(forReadingAtPath: url.path) else {
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: CocoaError.fileNoSuchFile as FoundationErrorEnum))
do {
guard let handle = FileHandle(forReadingAtPath: url.path) else {
throw self.cocoaError(path, code: .fileNoSuchFile)
}
return
}
defer {
handle.closeFile()
}
let size = LocalFileObject(fileWithURL: url)?.size ?? -1
progress.totalUnitCount = size
guard size > offset else {
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
defer {
handle.closeFile()
}
return
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
handle.seek(toFileOffset: UInt64(offset))
guard Int64(handle.offsetInFile) == offset else {
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
let size = LocalFileObject(fileWithURL: url)?.size ?? -1
progress.totalUnitCount = size
guard size > offset else {
progress.cancel()
throw self.cocoaError(path, code: .fileReadTooLarge)
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
handle.seek(toFileOffset: UInt64(offset))
guard Int64(handle.offsetInFile) == offset else {
progress.cancel()
throw self.cocoaError(path, code: .fileReadTooLarge)
}
let data = handle.readData(ofLength: length)
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
completionHandler(data, nil)
self.delegateNotify(operation)
}
return
}
let data = handle.readData(ofLength: length)
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
completionHandler(data, nil)
catch {
self.dispatch_queue.async {
completionHandler(nil, error)
self.delegateNotify(operation, error: error)
}
}
}
if isCoorinating {
let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges)
coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
coordinated(intents: [intent], operationHandler: operationHandler, errorHandler: { error in
completionHandler(nil, error)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
self.delegateNotify(operation, error: error)
})
} else {
dispatch_queue.async {
@@ -530,9 +539,19 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
@discardableResult
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let fileExists = fileManager.fileExists(atPath: url(of: path).path)
let opType: FileOperationType = fileExists ? .modify(path: path) : .create(path: path)
return self.doOperation(opType, data: data ?? Data(), atomically: atomically, completionHandler: completionHandler)
let fileExists = ((try? self.url(of: path).checkResourceIsReachable()) ?? false) ||
((try? self.url(of: path).checkPromisedItemIsReachable()) ?? false)
if !overwrite && fileExists {
let e = self.cocoaError(path, code: .fileWriteFileExists)
dispatch_queue.async {
completionHandler?(e)
}
self.delegateNotify(.modify(path: path), error: e)
return nil
}
let operation: FileOperationType = fileExists ? .modify(path: path) : .create(path: path)
return self.doOperation(operation, data: data ?? Data(), atomically: atomically, completionHandler: completionHandler)
}
fileprivate var monitors = [LocalFolderMonitor]()
@@ -565,9 +584,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
open func isRegisteredForNotification(path: String) -> Bool {
return monitors.map( { self.relativePathOf(url: $0.url) } ).contains(path.trimmingCharacters(in: CharacterSet(charactersIn: "/")))
}
}
public extension LocalFileProvider {
/**
Creates a symbolic link at the specified path that points to an item at the given path.
This method does not traverse symbolic links contained in destination path, making it possible
@@ -579,19 +596,16 @@ public extension LocalFileProvider {
- withDestinationPath: The path that contains the item to be pointed to by the link. In other words, this is the destination of the link.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
*/
public func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) {
open func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) {
operation_queue.addOperation {
let operation = FileOperationType.link(link: path, target: destPath)
do {
try self.opFileManager.createSymbolicLink(at: self.url(of: path), withDestinationURL: self.url(of: destPath))
completionHandler?(nil)
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: .link(link: path, target: destPath))
}
} catch let e {
completionHandler?(e)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: .link(link: path, target: destPath))
}
self.delegateNotify(operation)
} catch {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
}
}
@@ -601,32 +615,32 @@ public extension LocalFileProvider {
/// - Parameters:
/// - path: The path of a file or directory.
/// - completionHandler: Returns destination url of given symbolic link, or an `Error` object if it fails.
public func destination(ofSymbolicLink path: String, completionHandler: @escaping (_ url: URL?, _ error: Error?) -> Void) {
open func destination(ofSymbolicLink path: String, completionHandler: @escaping (_ url: URL?, _ error: Error?) -> Void) {
dispatch_queue.async {
do {
let destPath = try self.opFileManager.destinationOfSymbolicLink(atPath: self.url(of: path).path)
let destUrl = URL(fileURLWithPath: destPath)
completionHandler(destUrl, nil)
} catch let e{
completionHandler(nil, e)
} catch {
completionHandler(nil, error)
}
}
}
}
internal extension LocalFileProvider {
func coordinated(intents: [NSFileAccessIntent], completionHandler: @escaping (_ url: URL) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
func coordinated(intents: [NSFileAccessIntent], operationHandler: @escaping (_ url: URL) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
let coordinator = NSFileCoordinator(filePresenter: nil)
coordinator.coordinate(with: intents, queue: operation_queue) { (error) in
if let error = error {
errorHandler?(error)
return
}
completionHandler(intents.first!.url)
operationHandler(intents.first!.url)
}
}
func coordinated(intents: [NSFileAccessIntent], moving: Bool = false, completionHandler: @escaping (_ sourceUrl: URL, _ destURL: URL?) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
func coordinated(intents: [NSFileAccessIntent], moving: Bool = false, operationHandler: @escaping (_ sourceUrl: URL, _ destURL: URL?) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
let coordinator = NSFileCoordinator(filePresenter: nil)
coordinator.coordinate(with: intents, queue: operation_queue) { (error) in
if let error = error {
@@ -638,7 +652,7 @@ internal extension LocalFileProvider {
if moving, let newDest = newDest {
coordinator.item(at: newSource, willMoveTo: newDest)
}
completionHandler(newSource, newDest)
operationHandler(newSource, newDest)
if moving, let newDest = newDest {
coordinator.item(at: newSource, didMoveTo: newDest)
}
+12 -5
View File
@@ -17,11 +17,8 @@ public final class LocalFileObject: FileObject {
/// Initiates a `LocalFileObject` with attributes of file in path.
public convenience init? (fileWithPath path: String, relativeTo relativeURL: URL?) {
var fileURL: URL?
var rpath = path.replacingOccurrences(of: relativeURL?.path ?? "", with: "", options: .anchored)
if relativeURL != nil && rpath.hasPrefix("/") {
_=rpath.characters.removeFirst()
}
if #available(iOS 9.0, macOS 10.11, tvOS 9.0, *) {
var rpath = path.replacingOccurrences(of: relativeURL?.path ?? "", with: "", options: .anchored).replacingOccurrences(of: "/", with: "", options: .anchored)
if #available(iOS 9.0, macOS 10.11, *) {
fileURL = URL(fileURLWithPath: rpath, relativeTo: relativeURL)
} else {
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath
@@ -80,6 +77,16 @@ public final class LocalFileObject: FileObject {
return data?.map { String(format: "%02hhx", $0) }.joined()
}
}
/// Count of children items of a driectory. It costs disk access for local directories.
open public(set) override var childrensCount: Int? {
get {
return try? FileManager.default.contentsOfDirectory(atPath: self.url.path).count
}
set {
//
}
}
}
internal final class LocalFolderMonitor {
+266 -88
View File
@@ -15,12 +15,106 @@ import CoreGraphics
This provider doesn't cache or save files internally, however you can set `useCache` and `cache` properties
to use Foundation `NSURLCache` system.
- Note: You can pass file id instead of file path, e.g `"id:1234abcd"`, to point to a file or folder by ID.
- Note: Uploading files and data are limited to 100MB, for now.
*/
open class OneDriveFileProvider: HTTPFileProvider {
open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
override open class var type: String { return "OneDrive" }
/// Drive name for user, default is `root`. Changing its value will effect on new operations.
open var drive: String
/// Route to access file container on OneDrive. For default logined user use `.me` otherwise you can acesss
/// container based on drive id, group id, site id or user id for another user's default container
public enum Route: RawRepresentable {
/// Access to default container for current user
case me
/// Access to a specific drive by id
case drive(uuid: UUID)
/// Access to a default drive of a group by their id
case group(uuid: UUID)
/// Access to a default drive of a site by their id
case site(uuid: UUID)
/// Access to a default drive of a user by their id
case user(uuid: UUID)
public init?(rawValue: String) {
let components = rawValue.components(separatedBy: ";")
guard let type = components.first else {
return nil
}
if type == "me" {
self = .me
}
guard let uuid = components.last.flatMap({ UUID(uuidString: $0) }) else {
return nil
}
switch type {
case "drive":
self = .drive(uuid: uuid)
case "group":
self = .group(uuid: uuid)
case "site":
self = .site(uuid: uuid)
case "user":
self = .user(uuid: uuid)
default:
return nil
}
}
public var rawValue: String {
switch self {
case .me:
return "me;"
case .drive(uuid: let uuid):
return "drive;" + uuid.uuidString
case .group(uuid: let uuid):
return "group;" + uuid.uuidString
case .site(uuid: let uuid):
return "site;" + uuid.uuidString
case .user(uuid: let uuid):
return "user;" + uuid.uuidString
}
}
/// Return path component in URL for selected drive
var drivePath: String {
switch self {
case .me:
return "me/drive"
case .drive(uuid: let uuid):
return "drives/" + uuid.uuidString
case .group(uuid: let uuid):
return "groups/" + uuid.uuidString + "/drive"
case .site(uuid: let uuid):
return "sites/" + uuid.uuidString + "/drive"
case .user(uuid: let uuid):
return "users/" + uuid.uuidString + "/drive"
}
}
}
/// Route for container, default is `.me`.
open let route: Route
/**
Initializer for Onedrive provider with given client ID and Token.
These parameters must be retrieved via [Authentication for the OneDrive API](https://dev.onedrive.com/auth/readme.htm).
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.
- Parameters:
- credential: a `URLCredential` object with Client ID set as `user` and Token set as `password`.
- serverURL: server url, Set it if you are trying to connect OneDrive Business server, otherwise leave it
`nil` to connect to OneDrive Personal user.
- drive: drive name for user on server, default value is `root`.
- cache: A URLCache to cache downloaded files and contents.
*/
@available(*, deprecated, message: "use init(credential:, serverURL:, route:, cache:) instead.")
public init(credential: URLCredential?, serverURL: URL? = nil, drive: String?, cache: URLCache? = nil) {
let baseURL = serverURL?.absoluteURL ?? URL(string: "https://api.onedrive.com/")!
let refinedBaseURL = baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
self.route = drive.flatMap({ UUID(uuidString: $0) }).flatMap({ Route.drive(uuid: $0) }) ?? .me
super.init(baseURL: refinedBaseURL, credential: credential, cache: cache)
}
/**
Initializer for Onedrive provider with given client ID and Token.
@@ -33,33 +127,37 @@ open class OneDriveFileProvider: HTTPFileProvider {
- credential: a `URLCredential` object with Client ID set as `user` and Token set as `password`.
- serverURL: server url, Set it if you are trying to connect OneDrive Business server, otherwise leave it
`nil` to connect to OneDrive Personal uses.
- drive: drive name for user on server, default value is `root`.
- route: drive name for user on server, default value is `.me`.
- cache: A URLCache to cache downloaded files and contents.
*/
public init(credential: URLCredential?, serverURL: URL? = nil, drive: String = "root", cache: URLCache? = nil) {
public init(credential: URLCredential?, serverURL: URL? = nil, route: Route = .me, cache: URLCache? = nil) {
let baseURL = serverURL?.absoluteURL ?? URL(string: "https://api.onedrive.com/")!
let refinedBaseURL = baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
self.drive = drive
self.route = route
super.init(baseURL: refinedBaseURL, credential: credential, cache: cache)
}
public required convenience init?(coder aDecoder: NSCoder) {
let route: Route
if let driveId = aDecoder.decodeObject(forKey: "drive") as? String, let uuid = UUID(uuidString: driveId) {
route = .drive(uuid: uuid)
} else {
route = (aDecoder.decodeObject(forKey: "route") as? String).flatMap({ Route(rawValue: $0) }) ?? .me
}
self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential,
serverURL: aDecoder.decodeObject(forKey: "baseURL") as? URL,
drive: aDecoder.decodeObject(forKey: "drive") as? String ?? "root")
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
route: route)
self.useCache = aDecoder.decodeBool(forKey: "useCache")
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
}
open override func encode(with aCoder: NSCoder) {
super.encode(with: aCoder)
aCoder.encode(self.drive, forKey: "drive")
aCoder.encode(self.route.rawValue, forKey: "route")
}
open override func copy(with zone: NSZone? = nil) -> Any {
let copy = OneDriveFileProvider(credential: self.credential, serverURL: self.baseURL, drive: self.drive, cache: self.cache)
copy.currentPath = self.currentPath
let copy = OneDriveFileProvider(credential: self.credential, serverURL: self.baseURL, route: self.route, cache: self.cache)
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
copy.useCache = self.useCache
@@ -67,23 +165,43 @@ open class OneDriveFileProvider: HTTPFileProvider {
return copy
}
open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
list(path) { (contents, cursor, error) in
completionHandler(contents, error)
}
open override func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
_ = paginated(path, requestHandler: { [weak self] (token) -> URLRequest? in
guard let `self` = self else { return nil }
let url = token.flatMap(URL.init(string:)) ?? self.url(of: path, modifier: "children")
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.set(httpAuthentication: self.credential, with: .oAuth2)
return request
}, pageHandler: { [weak self] (data, _) -> (files: [FileObject], error: Error?, newToken: String?) in
guard let `self` = self else { return ([], nil, nil) }
guard let json = data?.deserializeJSON(), let entries = json["value"] as? [AnyObject] else {
let err = self.urlError(path, code: .badServerResponse)
return ([], err, nil)
}
var files = [FileObject]()
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: entry) {
files.append(file)
}
}
return (files, nil, json["@odata.nextLink"] as? String)
}, completionHandler: completionHandler)
}
open override func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
open override func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
var request = URLRequest(url: url(of: path))
request.httpMethod = "GET"
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderOneDriveError?
var serverError: FileProviderHTTPError?
var fileObject: OneDriveFileObject?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
if let json = data?.deserializeJSON(), let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: json) {
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
if let json = data?.deserializeJSON(), let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: json) {
fileObject = file
}
}
@@ -92,70 +210,104 @@ open class OneDriveFileProvider: HTTPFileProvider {
task.resume()
}
open override func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
var request = URLRequest(url: url())
open override func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) {
var request = URLRequest(url: url(of: ""))
request.httpMethod = "GET"
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var totalSize: Int64 = -1
var usedSize: Int64 = 0
if let json = data?.deserializeJSON() {
totalSize = (json["total"] as? NSNumber)?.int64Value ?? -1
usedSize = (json["used"] as? NSNumber)?.int64Value ?? 0
guard let json = data?.deserializeJSON() else {
completionHandler(nil)
return
}
completionHandler(totalSize, usedSize)
let volume = VolumeObject(allValues: [:])
volume.url = request.url
volume.name = json["name"] as? String
volume.creationDate = (json["createdDateTime"] as? String).flatMap { Date(rfcString: $0) }
volume.totalCapacity = (json["quota"]?["total"] as? NSNumber)?.int64Value ?? -1
volume.availableCapacity = (json["quota"]?["remaining"] as? NSNumber)?.int64Value ?? 0
completionHandler(volume)
})
task.resume()
}
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
var foundFiles = [OneDriveFileObject]()
var queryStr: String?
queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first
guard let finalQueryStr = queryStr else { return nil }
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(url(of: path), forKey: .fileURLKey)
search(path, query: finalQueryStr, progress: progress, foundItem: { (file) in
if query.evaluate(with: file.mapPredicate()) {
foundFiles.append(file)
foundItemHandler?(file)
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
let queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first
return paginated(path, requestHandler: { [weak self] (token) -> URLRequest? in
guard let `self` = self else { return nil }
let url: URL
if let next = token.flatMap(URL.init(string:)) {
url = next
} else {
let bURL = self.baseURL!.appendingPathComponent(self.route.drivePath).appendingPathComponent("root/search")
var components = URLComponents(url: bURL, resolvingAgainstBaseURL: false)!
let qItem = URLQueryItem(name: "q", value: (queryStr ?? "*"))
components.queryItems = [qItem]
if recursive {
components.queryItems?.append(URLQueryItem(name: "expand", value: "children"))
}
url = components.url!
}
}, completionHandler: { (error) in
completionHandler(foundFiles, error)
})
return progress
var request = URLRequest(url: url)
request.httpMethod = "GET"
return request
}, pageHandler: { [weak self] (data, progress) -> (files: [FileObject], error: Error?, newToken: String?) in
guard let `self` = self else { return ([], nil, nil) }
guard let json = data?.deserializeJSON(), let entries = json["value"] as? [AnyObject] else {
let err = self.urlError(path, code: .badServerResponse)
return ([], err, nil)
}
var foundFiles = [FileObject]()
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: entry), query.evaluate(with: file.mapPredicate()) {
foundFiles.append(file)
foundItemHandler?(file)
}
}
return (foundFiles, nil, json["@odata.nextLink"] as? String)
}, completionHandler: completionHandler)
}
open func url(of path: String? = nil, modifier: String? = nil) -> URL {
var rpath: String
if let path = path {
rpath = path
open func url(of path: String, modifier: String? = nil) -> URL {
var url: URL = baseURL!
var rpath: String = path
let isId = path.hasPrefix("id:")
url.appendPathComponent(route.drivePath)
if isId {
url.appendPathComponent("root:")
} else {
rpath = self.currentPath
}
let driveURL = baseURL!.appendingPathComponent("drive/\(drive):/")
if rpath.hasPrefix("/") {
_=rpath.characters.removeFirst()
}
if rpath.isEmpty {
if let modifier = modifier {
return driveURL.appendingPathComponent(modifier)
}
return driveURL
url.appendPathComponent("items")
}
rpath = rpath.trimmingCharacters(in: pathTrimSet)
if let modifier = modifier {
rpath = rpath + ":/" + modifier
switch (modifier == nil, rpath.isEmpty, isId) {
case (true, false, _):
url.appendPathComponent(rpath)
case (true, true, _):
break
case (false, true, _):
url.appendPathComponent(modifier!)
case (false, false, true):
url.appendPathComponent(rpath)
url.appendPathComponent(modifier!)
case (false, false, false):
url.appendPathComponent(rpath + ":")
url.appendPathComponent(modifier!)
}
return driveURL.appendingPathComponent(rpath)
return url
}
open override func isReachable(completionHandler: @escaping (Bool) -> Void) {
var request = URLRequest(url: url())
var request = URLRequest(url: url(of: ""))
request.httpMethod = "HEAD"
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
@@ -166,6 +318,18 @@ open class OneDriveFileProvider: HTTPFileProvider {
}
override func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey : Any] = [:]) -> URLRequest {
func correctPath(_ path: String) -> String {
if path.hasPrefix("id:") {
return path
}
var p = path.hasPrefix("/") ? path : "/" + path
if p.hasSuffix("/") {
p.remove(at: p.index(before:p.endIndex))
}
return p
}
let method: String
let url: URL
switch operation {
@@ -201,35 +365,51 @@ open class OneDriveFileProvider: HTTPFileProvider {
var request = URLRequest(url: url)
request.httpMethod = method
request.set(httpAuthentication: credential, with: .oAuth2)
if let dest = correctPath(operation.destination) as NSString?, !dest.hasPrefix("file://") {
request.set(contentType: .json)
// Remove gzip to fix availability of progress per (Oleg Marchik)[https://github.com/evilutioner] PR (#61)
request.set(httpAcceptEncodings: [.deflate, .identity])
switch operation {
case .copy(let source, let dest) where !source.hasPrefix("file://") && !dest.hasPrefix("file://"),
.move(source: let source, destination: let dest):
request.set(httpContentType: .json)
let cdest = correctPath(dest) as NSString
var parentRefrence: [String: AnyObject] = [:]
if cdest.hasPrefix("id:") {
parentRefrence["id"] = cdest.components(separatedBy: "/").first as NSString?
switch self.route {
case .drive(uuid: let uuid):
parentRefrence["driveId"] = uuid.uuidString as NSString
default:
break
}
} else {
parentRefrence["path"] = cdest.deletingLastPathComponent as NSString
}
var requestDictionary = [String: AnyObject]()
requestDictionary["parentReference"] = ("/drive/\(drive):" + dest.deletingLastPathComponent) as NSString
requestDictionary["name"] = dest.lastPathComponent as NSString
requestDictionary["parentReference"] = parentRefrence as NSDictionary
requestDictionary["name"] = (cdest as NSString).lastPathComponent as NSString
request.httpBody = Data(jsonDictionary: requestDictionary)
default:
break
}
return request
}
override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
return FileProviderOneDriveError(code: code, path: path ?? "", errorDescription: data.flatMap({ String(data: $0, encoding: .utf8) }))
let errorDesc: String?
if let response = data?.deserializeJSON() {
errorDesc = response["error"]?["message"] as? String
} else {
errorDesc = data.flatMap({ String(data: $0, encoding: .utf8) })
}
return FileProviderOneDriveError(code: code, path: path ?? "", errorDescription: errorDesc)
}
override func upload_simple(_ targetPath: String, request: URLRequest, data: Data?, localFile: URL?, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
let size = data?.count ?? Int((try? localFile?.resourceValues(forKeys: [.fileSizeKey]))??.fileSize ?? -1)
if size > 100 * 1024 * 1024 {
let error = FileProviderOneDriveError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
completionHandler?(error)
self.delegateNotify(operation, error: error)
return nil
}
return super.upload_simple(targetPath, request: request, data: data, localFile: localFile, operation: operation, completionHandler: completionHandler)
override var maxUploadSimpleSupported: Int64 {
return 104_857_600 // 100MB
}
}
extension OneDriveFileProvider {
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
@@ -242,20 +422,18 @@ extension OneDriveFileProvider {
fileprivate func unregisterNotifcation(path: String) {
NotImplemented()
}
}
extension OneDriveFileProvider: FileProviderSharing {
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
var request = URLRequest(url: self.url(of: path, modifier: "action.createLink"))
request.httpMethod = "POST"
let requestDictionary: [String: AnyObject] = ["type": "view" as NSString]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderOneDriveError?
var serverError: FileProviderHTTPError?
var link: URL?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
if let json = data?.deserializeJSON() {
if let linkDic = json["link"] as? NSDictionary, let linkStr = linkDic["webUrl"] as? String {
link = URL(string: linkStr)
@@ -302,7 +480,7 @@ extension OneDriveFileProvider: ExtendedFileProvider {
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))
let responseError = self.serverError(with: rCode, path: path, data: data)
completionHandler(nil, responseError)
return
}
@@ -319,12 +497,12 @@ extension OneDriveFileProvider: ExtendedFileProvider {
request.httpMethod = "GET"
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderOneDriveError?
var serverError: FileProviderHTTPError?
var dic = [String: Any]()
var keys = [String]()
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
if let json = data?.deserializeJSON() {
(dic, keys) = self.mapMediaInfo(json)
}
+22 -89
View File
@@ -18,51 +18,52 @@ public struct FileProviderOneDriveError: FileProviderHTTPError {
/// Containts path, url and attributes of a OneDrive file or resource.
public final class OneDriveFileObject: FileObject {
internal init(baseURL: URL?, name: String, path: String) {
var rpath = (URL(string:path)?.appendingPathComponent(name).absoluteString)!
if rpath.hasPrefix("/") {
_=rpath.characters.removeFirst()
}
let rpath = (URL(string:path)?.appendingPathComponent(name).absoluteString)!.replacingOccurrences(of: "/", with: "", options: .anchored)
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: rpath)!
super.init(url: url, name: name, path: rpath.removingPercentEncoding ?? path)
}
internal convenience init? (baseURL: URL?, drive: String, jsonStr: String) {
internal convenience init? (baseURL: URL?, route: OneDriveFileProvider.Route, jsonStr: String) {
guard let json = jsonStr.deserializeJSON() else { return nil }
self.init(baseURL: baseURL, drive: drive, json: json)
self.init(baseURL: baseURL, route: route, json: json)
}
internal convenience init? (baseURL: URL?, drive: String, json: [String: AnyObject]) {
internal convenience init? (baseURL: URL?, route: OneDriveFileProvider.Route, json: [String: AnyObject]) {
guard let name = json["name"] as? String else { return nil }
guard let path = (json["parentReference"] as? NSDictionary)?["path"] as? String else { return nil }
var lPath = path.replacingOccurrences(of: "/drive/\(drive):", with: "/", options: .anchored, range: nil)
guard let path = json["parentReference"]?["path"] as? String else { return nil }
var lPath = path.replacingOccurrences(of: route.drivePath, with: "", options: .anchored, range: nil)
lPath = lPath.replacingOccurrences(of: "/:", with: "", options: .anchored)
lPath = lPath.replacingOccurrences(of: "//", with: "", options: .anchored)
self.init(baseURL: baseURL, name: name, path: lPath)
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
self.modifiedDate = Date(rfcString: json["lastModifiedDateTime"] as? String ?? "")
self.creationDate = Date(rfcString: json["createdDateTime"] as? String ?? "")
self.childrensCount = json["folder"]?["childCount"] as? Int
self.modifiedDate = (json["lastModifiedDateTime"] as? String).flatMap { Date(rfcString: $0) }
self.creationDate = (json["createdDateTime"] as? String).flatMap { Date(rfcString: $0) }
self.type = json["folder"] != nil ? .directory : .regular
self.contentType = json["file"]?["mimeType"] as? String ?? "application/octet-stream"
self.id = json["id"] as? String
self.entryTag = json["eTag"] as? String
let hashes = json["file"]?["hashes"] as? NSDictionary
// checks for both sha1 or quickXor. First is available in personal drives, second in business one.
self.hash = (hashes?["sha1Hash"] as? String) ?? (hashes?["quickXorHash"] as? String)
}
/// The document identifier is a value assigned by the OneDrive to a file.
/// This value is used to identify the document regardless of where it is moved on a volume.
/// The identifier persists across system restarts.
open internal(set) var id: String? {
get {
return allValues[.documentIdentifierKey] as? String
return allValues[.fileResourceIdentifierKey] as? String
}
set {
allValues[.documentIdentifierKey] = newValue
allValues[.fileResourceIdentifierKey] = newValue
}
}
/// MIME type of file contents returned by OneDrive server.
open internal(set) var contentType: String {
get {
return allValues[.mimeTypeKey] as? String ?? ""
return allValues[.mimeTypeKey] as? String ?? "application/octet-stream"
}
set {
allValues[.mimeTypeKey] = newValue
@@ -78,85 +79,17 @@ public final class OneDriveFileObject: FileObject {
allValues[.entryTagKey] = newValue
}
}
}
// codebeat:disable[ARITY]
internal extension OneDriveFileProvider {
func list(_ path: String, cursor: URL? = nil, prevContents: [OneDriveFileObject] = [], completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
let url = cursor ?? self.url(of: path, modifier: "children")
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.set(httpAuthentication: credential, with: .oAuth2)
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 json = data?.deserializeJSON() {
if let entries = json["value"] as? [AnyObject] , entries.count > 0 {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: entry) {
files.append(file)
}
}
let ncursor: URL? = (json["@odata.nextLink"] as? String).flatMap { URL(string: $0) }
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 search(_ startPath: String = "", query: String, next: URL? = nil, progress: Progress, foundItem: @escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
if progress.isCancelled {
return
/// Calculated hash from OneDrive server. Hex string SHA1 in personal or Base65 string [QuickXOR](https://dev.onedrive.com/snippets/quickxorhash.htm) in business drives.
open internal(set) var hash: String? {
get {
return allValues[.documentIdentifierKey] as? String
}
let url: URL
let q = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
url = next ?? self.url(of: startPath, modifier: "view.search?q=\(q)")
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.set(httpAuthentication: credential, with: .oAuth2)
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 json = data?.deserializeJSON() {
if let entries = json["value"] as? [AnyObject] , entries.count > 0 {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: entry) {
foundItem(file)
}
}
let next: URL? = (json["@odata.nextLink"] as? String).flatMap { URL(string: $0) }
if !progress.isCancelled, let next = next {
self.search(startPath, query: query, next: next, progress: progress, foundItem: foundItem, completionHandler: completionHandler)
} else {
completionHandler(responseError ?? error)
}
return
}
}
completionHandler(responseError ?? error)
})
progress.cancellationHandler = { [weak task] in
task?.cancel()
set {
allValues[.documentIdentifierKey] = newValue
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
}
}
// codebeat:enable[ARITY]
internal extension OneDriveFileProvider {
static let dateFormatter = DateFormatter()
+41 -57
View File
@@ -31,21 +31,10 @@ extension FileProviderHTTPError {
}
}
/// Defines HTTP Authentication method required to access
public enum HTTPAuthenticationType {
/// Basic method for authentication
case basic
/// Digest method for authentication
case digest
/// OAuth 1.0 method for authentication (OAuth)
case oAuth1
/// OAuth 2.0 method for authentication (Bearer)
case oAuth2
}
internal var completionHandlersForTasks = [String: [Int: SimpleCompletionHandler]]()
internal var downloadCompletionHandlersForTasks = [String: [Int: (URL) -> Void]]()
internal var dataCompletionHandlersForTasks = [String: [Int: (Data) -> Void]]()
internal var responseCompletionHandlersForTasks = [String: [Int: (URLResponse) -> Void]]()
internal func initEmptySessionHandler(_ uuid: String) {
completionHandlersForTasks[uuid] = [:]
@@ -65,15 +54,6 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession
weak var fileProvider: (FileProviderBasicRemote & FileProviderOperations)?
var credential: URLCredential?
/// Forwardng URLSessionDownloadTaskDelegate call
public var finishDownloadHandler: ((_ session: URLSession, _ downloadTask: URLSessionDownloadTask, _ didFinishDownloadingToURL: URL) -> Void)?
/// Forwardng URLSessionTaskDelegate call
public var didSendDataHandler: ((_ session: URLSession, _ task: URLSessionTask, _ bytesSent: Int64, _ totalBytesSent: Int64, _ totalBytesExpectedToSend: Int64) -> Void)?
/// Forwardng URLSessionDownloadTaskDelegate call
public var didReceivedData: ((_ session: URLSession, _ downloadTask: URLSessionDownloadTask, _ bytesWritten: Int64, _ totalBytesWritten: Int64, _ totalBytesExpectedToWrite: Int64) -> Void)?
/// Forwardng URLSessionStreamTaskDelegate call
public var didBecomeStream :((_ session: URLSession, _ taskId: Int, _ didBecome: InputStream, _ outputStream: OutputStream) -> Void)?
public init(fileProvider: FileProviderBasicRemote & FileProviderOperations) {
self.fileProvider = fileProvider
self.credential = fileProvider.credential
@@ -116,14 +96,15 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession
// codebeat:disable[ARITY]
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if task is URLSessionDownloadTask {
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived))
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive))
}
if task is URLSessionUploadTask {
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent))
//task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToSend))
} else if task is URLSessionDownloadTask {
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived))
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive))
}
_ = dataCompletionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: task.taskIdentifier)
if !(error == nil && task is URLSessionDownloadTask) {
let completionHandler = completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] ?? nil
completionHandler?(error)
@@ -135,6 +116,16 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession
return
}
switch op {
case .fetch:
if task is URLSessionDataTask {
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived))
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive))
}
default:
break
}
if !(task is URLSessionDownloadTask), case FileOperationType.fetch = op {
return
}
@@ -144,33 +135,35 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession
}
}
DispatchQueue.main.async {
if error != nil {
fileProvider.delegate?.fileproviderFailed(fileProvider, operation: op)
} else {
fileProvider.delegate?.fileproviderSucceed(fileProvider, operation: op)
}
}
}
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
let completionHandler = dataCompletionHandlersForTasks[session.sessionDescription!]?[dataTask.taskIdentifier] ?? nil
completionHandler?(data)
_ = dataCompletionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: dataTask.taskIdentifier)
fileProvider.delegateNotify(op, error: error)
}
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
self.finishDownloadHandler?(session, downloadTask, location)
let dcompletionHandler = downloadCompletionHandlersForTasks[session.sessionDescription!]?[downloadTask.taskIdentifier]
dcompletionHandler?(location)
_ = downloadCompletionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: downloadTask.taskIdentifier)
_ = completionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: downloadTask.taskIdentifier)
}
public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
self.didSendDataHandler?(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
let handler = responseCompletionHandlersForTasks[session.sessionDescription!]?[dataTask.taskIdentifier] ?? nil
handler?(response)
completionHandler(.allow)
_ = responseCompletionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: dataTask.taskIdentifier)
}
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
if let completionHandler = dataCompletionHandlersForTasks[session.sessionDescription!]?[dataTask.taskIdentifier] {
if let json = dataTask.taskDescription?.deserializeJSON(),
let op = FileOperationType(json: json), let fileProvider = fileProvider {
fileProvider.delegateNotify(op, progress: Double(dataTask.countOfBytesReceived) / Double(dataTask.countOfBytesExpectedToReceive))
}
completionHandler(data)
}
}
public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
guard let json = task.taskDescription?.deserializeJSON(),
let op = FileOperationType(json: json), let fileProvider = fileProvider else {
return
@@ -188,16 +181,10 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession
return
}
let progress = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
DispatchQueue.main.async {
fileProvider.delegate?.fileproviderProgress(fileProvider, operation: op, progress: progress)
}
fileProvider.delegateNotify(op, progress: Double(totalBytesSent) / Double(totalBytesExpectedToSend))
}
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
self.didReceivedData?(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
if totalBytesExpectedToWrite == NSURLSessionTransferSizeUnknown { return }
guard let json = downloadTask.taskDescription?.deserializeJSON(),
@@ -205,9 +192,7 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession
return
}
DispatchQueue.main.async {
fileProvider.delegate?.fileproviderProgress(fileProvider, operation: op, progress: Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
}
fileProvider.delegateNotify(op, progress: Double(totalBytesWritten) / Double(totalBytesExpectedToWrite))
}
public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
@@ -228,11 +213,6 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession
completionHandler(.performDefaultHandling, nil)
}
}
@available(iOS 9.0, macOS 10.11, *)
public func urlSession(_ session: URLSession, streamTask: URLSessionStreamTask, didBecome inputStream: InputStream, outputStream: OutputStream) {
self.didBecomeStream?(session, streamTask.taskIdentifier, inputStream, outputStream)
}
}
/// HTTP status codes as an enum.
@@ -378,6 +358,10 @@ public enum FileProviderHTTPErrorCode: Int, CustomStringConvertible {
}
}
public var localizedDescription: String {
return HTTPURLResponse.localizedString(forStatusCode: self.rawValue)
}
/// Description of status based on first digit which indicated fail or success.
public var typeDescription: String {
switch self.rawValue {
+2 -1
View File
@@ -138,7 +138,8 @@ extension FileProviderSMBTask {
let paramData = Data(bytesNoCopy: UnsafeMutablePointer<UInt8>(&rawParamWords), count: rawParamWords.count, deallocator: .free)
paramWords = paramData.scanValue()!
offset += paramWordsCount * 2
let messageBytesCount = Int(UInt16(buffer[0]) + UInt16(buffer[1]) << 8)
let messageBytesCountHi = Int(buffer[1]) << 8
let messageBytesCount = Int(buffer[0]) + messageBytesCountHi
offset += MemoryLayout<UInt16>.size
guard data.count >= (offset + messageBytesCount) else {
throw SMBFileProviderError.incorrectMessageLength
+3 -3
View File
@@ -56,15 +56,15 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
return true
}
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObjectClass], _ error: Error?) -> Void)) {
open func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObjectClass], _ error: Error?) -> Void) {
NotImplemented()
}
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObjectClass?, _ error: Error?) -> Void)) {
open func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObjectClass?, _ error: Error?) -> Void) {
NotImplemented()
}
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
open func storageProperties(completionHandler: @escaping (_ volume: VolumeObject?) -> Void) {
NotImplemented()
}
+4
View File
@@ -8,6 +8,7 @@
import Foundation
// codebeat:disable[TOO_MANY_IVARS]
// SMB/CIFS Types
struct SMB1 {
struct Header { // 32 bytes
@@ -75,6 +76,7 @@ struct SMB1 {
}
}
// codebeat:disable[ARITY]
init(command: Command, treeId: UInt16, pid: UInt32, userId: UInt16, multiplexId: UInt16, flags: Flags, flags2: Flags2 = [.LONG_NAMES, .ERR_STATUS, .UNICODE], ntStatus: UInt32 = 0, securityKey: UInt32 = 0, securityCID: UInt16 = 0, securitySequenceNumber: UInt16 = 0) {
self.protocolID = Header.protocolConst
self._command = command.rawValue
@@ -91,6 +93,7 @@ struct SMB1 {
self.userId = userId
self.multiplexId = multiplexId
}
// codebeat:enable[ARITY]
}
struct Flags: OptionSet {
@@ -215,3 +218,4 @@ struct SMB1 {
case INVALID = 0xFE
}
}
// codebeat:enable[TOO_MANY_IVARS]
+4 -1
View File
@@ -64,7 +64,10 @@ extension SMB2 {
var contextCount: UInt16
fileprivate let reserved2: UInt16
var clientStartTime: SMBTime {
let time = Int64(contextOffset) + (Int64(contextCount) << 32) + (Int64(contextCount) << 48)
let lo = Int64(contextOffset)
let hi1 = Int64(contextCount) << 32
let hi2 = Int64(contextCount) << 48
let time: Int64 = lo + hi1 + hi2
return SMBTime(time: time)
}
+1 -1
View File
@@ -26,7 +26,7 @@ extension SMB2 {
return nil
}
self.header = header
let path = "\\\\\(host)\\\(share)"
let path = "\\\\" + host + "\\" + share
self.buffer = path.data(using: .utf16)
}
+141 -133
View File
@@ -20,9 +20,12 @@ import CoreGraphics
in case of using this class with unencrypted HTTP connection.
[Read this to know how](http://iosdevtips.co/post/121756573323/ios-9-xcode-7-http-connect-server-error).
*/
open class WebDAVFileProvider: HTTPFileProvider {
open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing {
override open class var type: String { return "WebDAV" }
public var credentialType: HTTPAuthenticationType = .digest
/// An enum which defines HTTP Authentication method, usually you should it default `.digest`.
/// If the server uses OAuth authentication, credential must be set with token as `password`, like Dropbox.
public var credentialType: URLRequest.AuthenticationType = .digest
/**
Initializes WebDAV provider.
@@ -36,8 +39,8 @@ open class WebDAVFileProvider: HTTPFileProvider {
if !["http", "https"].contains(baseURL.uw_scheme.lowercased()) {
return nil
}
let refinedBaseURL = (baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")).absoluteURL
super.init(baseURL: refinedBaseURL, credential: credential, cache: cache)
let refinedBaseURL = (baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent(""))
super.init(baseURL: refinedBaseURL.absoluteURL, credential: credential, cache: cache)
}
public required convenience init?(coder aDecoder: NSCoder) {
@@ -46,14 +49,12 @@ open class WebDAVFileProvider: HTTPFileProvider {
}
self.init(baseURL: baseURL,
credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
self.useCache = aDecoder.decodeBool(forKey: "useCache")
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
}
override open func copy(with zone: NSZone? = nil) -> Any {
let copy = WebDAVFileProvider(baseURL: self.baseURL!, credential: self.credential, cache: self.cache)!
copy.currentPath = self.currentPath
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
copy.useCache = self.useCache
@@ -62,7 +63,8 @@ open class WebDAVFileProvider: HTTPFileProvider {
}
override open func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) {
self.contentsOfDirectory(path: path, including: [], completionHandler: completionHandler)
let query = NSPredicate(format: "TRUEPREDICATE")
_ = searchFiles(path: path, recursive: false, query: query, including: [], foundItemHandler: nil, completionHandler: completionHandler)
}
/**
@@ -70,42 +72,18 @@ open class WebDAVFileProvider: HTTPFileProvider {
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 path: path to target directory. If empty, root will be iterated.
- Parameter including: An array which determines which file properties should be considered to fetch.
- Parameter completionHandler: a closure with result of directory entries or error.
- `contents`: An array of `FileObject` identifying the the directory entries.
- `error`: Error returned by system.
- Parameter contents: An array of `FileObject` identifying the the directory entries.
- Parameter error: Error returned by system.
*/
open func contentsOfDirectory(path: String, including: [URLResourceKey], completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
let opType = FileOperationType.fetch(path: path)
let url = self.url(of: path).appendingPathComponent("")
var request = URLRequest(url: url)
request.httpMethod = "PROPFIND"
request.setValue("1", forHTTPHeaderField: "Depth")
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n\(WebDavFileObject.propString(including))\n</D:propfind>".data(using: .utf8)
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
runDataTask(with: request, operation: opType, completionHandler: { (data, response, error) in
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
}
var fileObjects = [WebDavFileObject]()
if let data = data {
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(WebDavFileObject(attr))
}
}
completionHandler(fileObjects, responseError ?? error)
})
open func contentsOfDirectory(path: String, including: [URLResourceKey], completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
let query = NSPredicate(format: "TRUEPREDICATE")
_ = searchFiles(path: path, recursive: false, query: query, including: including, foundItemHandler: nil, completionHandler: completionHandler)
}
override open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
override open func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
self.attributesOfItem(path: path, including: [], completionHandler: completionHandler)
}
@@ -114,25 +92,24 @@ open class WebDAVFileProvider: HTTPFileProvider {
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 path: path to target directory. If empty, attributes of root will be returned.
- Parameter including: An array which determines which file properties should be considered to fetch.
- Parameter completionHandler: a closure with result of directory entries or error.
- `attributes`: A `FileObject` containing the attributes of the item.
- `error`: Error returned by system.
- Parameter attributes: A `FileObject` containing the attributes of the item.
- Parameter error: Error returned by system.
*/
open func attributesOfItem(path: String, including: [URLResourceKey], completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
open func attributesOfItem(path: String, including: [URLResourceKey], completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PROPFIND"
request.setValue("1", forHTTPHeaderField: "Depth")
request.setValue("0", forHTTPHeaderField: "Depth")
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n\(WebDavFileObject.propString(including))\n</D:propfind>".data(using: .utf8)
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
request.set(httpContentType: .xml, charset: .utf8)
request.httpBody = WebDavFileObject.xmlProp(including)
runDataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderWebDavError?
var responseError: FileProviderHTTPError?
if let code = (response as? HTTPURLResponse)?.statusCode, code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
responseError = self.serverError(with: rCode, path: path, data: data)
}
if let data = data {
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
@@ -145,7 +122,7 @@ open class WebDAVFileProvider: HTTPFileProvider {
})
}
override open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
override open func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> 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.
@@ -156,56 +133,80 @@ open class WebDAVFileProvider: HTTPFileProvider {
request.httpMethod = "PROPFIND"
request.setValue("0", forHTTPHeaderField: "Depth")
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:prop><D:quota-available-bytes/><D:quota-used-bytes/></D:prop>\n</D:propfind>".data(using: .utf8)
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
request.set(httpContentType: .xml, charset: .utf8)
request.httpBody = WebDavFileObject.xmlProp([.volumeTotalCapacityKey, .volumeAvailableCapacityKey, .creationDateKey])
runDataTask(with: request, completionHandler: { (data, response, error) in
var totalSize: Int64 = -1
var usedSize: Int64 = 0
if let data = 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
}
guard let data = data, let attr = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL).first else {
completionHandler(nil)
return
}
completionHandler(totalSize, usedSize)
let volume = VolumeObject(allValues: [:])
volume.creationDate = attr.prop["creationdate"].flatMap { Date(rfcString: $0) }
volume.availableCapacity = attr.prop["quota-available-bytes"].flatMap({ Int64($0) }) ?? 0
if let usage = attr.prop["quota-used-bytes"].flatMap({ Int64($0) }) {
volume.totalCapacity = volume.availableCapacity + usage
}
completionHandler(volume)
})
}
override open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ([FileObject], Error?) -> Void) -> Progress? {
return searchFiles(path: path, recursive: recursive, query: query, including: [], foundItemHandler: foundItemHandler, completionHandler: completionHandler)
}
/**
Search files inside directory using query asynchronously.
- Note: Query string is limited to file name, to search based on other file attributes, use NSPredicate version.
- Parameters:
- path: location of directory to start search
- recursive: Searching subdirectories of path
- query: Simple string that file name begins with to be search, case-insensitive.
- including: An array which determines which file properties should be considered to fetch.
- foundItemHandler: Closure which is called when a file is found
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
- files: all files meat the `query` criteria.
- error: `Error` returned by server if occured.
*/
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, including: [URLResourceKey], foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PROPFIND"
//request.setValue("1", forHTTPHeaderField: "Depth")
// Depth infinity is disabled on some servers. Implement workaround?!
request.setValue(recursive ? "infinity" : "1", forHTTPHeaderField: "Depth")
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:allprop/></D:propfind>".data(using: .utf8)
let progress = Progress(parent: nil, userInfo: nil)
request.set(httpContentType: .xml, charset: .utf8)
request.httpBody = WebDavFileObject.xmlProp([])
let progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(url, forKey: .fileURLKey)
let queryIsTruePredicate = query.predicateFormat == "TRUEPREDICATE"
let task = session.dataTask(with: request) { (data, response, error) in
// FIXME: paginating results
var responseError: FileProviderWebDavError?
var responseError: FileProviderHTTPError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
responseError = self.serverError(with: rCode, path: path, data: data)
}
if let data = data {
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
var fileObjects = [WebDavFileObject]()
for attr in xresponse {
let fileObject = WebDavFileObject(attr)
if !query.evaluate(with: fileObject.mapPredicate()) {
continue
}
fileObjects.append(fileObject)
progress.completedUnitCount = Int64(fileObjects.count)
foundItemHandler?(fileObject)
}
completionHandler(fileObjects, responseError ?? error)
guard let data = data else {
completionHandler([], responseError ?? error)
return
}
completionHandler([], responseError ?? error)
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
var fileObjects = [WebDavFileObject]()
for attr in xresponse where attr.href.path != url.path {
let fileObject = WebDavFileObject(attr)
if !queryIsTruePredicate && !query.evaluate(with: fileObject.mapPredicate()) {
continue
}
fileObjects.append(fileObject)
progress.completedUnitCount = Int64(fileObjects.count)
foundItemHandler?(fileObject)
}
completionHandler(fileObjects, responseError ?? error)
}
progress.cancellationHandler = { [weak task] in
task?.cancel()
@@ -220,15 +221,45 @@ open class WebDAVFileProvider: HTTPFileProvider {
request.httpMethod = "PROPFIND"
request.setValue("0", forHTTPHeaderField: "Depth")
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:prop><D:quota-available-bytes/><D:quota-used-bytes/></D:prop>\n</D:propfind>".data(using: .utf8)
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
request.set(httpContentType: .xml, charset: .utf8)
request.httpBody = WebDavFileObject.xmlProp([.volumeTotalCapacityKey, .volumeAvailableCapacityKey])
runDataTask(with: request, completionHandler: { (data, response, error) in
let status = (response as? HTTPURLResponse)?.statusCode ?? 400
completionHandler(status < 300)
})
}
open func publicLink(to path: String, completionHandler: @escaping ((URL?, FileObject?, Date?, Error?) -> Void)) {
guard self.baseURL?.host?.contains("dav.yandex.") ?? false else {
dispatch_queue.async {
completionHandler(nil, nil, nil, self.urlError(path, code: .resourceUnavailable))
}
return
}
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PROPPATCH"
request.set(httpAuthentication: credential, with: credentialType)
request.set(httpContentType: .xml, charset: .utf8)
let body = "<propertyupdate xmlns=\"DAV:\">\n<set><prop>\n<public_url xmlns=\"urn:yandex:disk:meta\">true</public_url>\n</prop></set>\n</propertyupdate>"
request.httpBody = body.data(using: .utf8)
runDataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderHTTPError?
if let code = (response as? HTTPURLResponse)?.statusCode, code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = self.serverError(with: rCode, path: path, data: data)
}
if let data = data {
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
if let urlStr = xresponse.first?.prop["public_url"], let url = URL(string: urlStr) {
completionHandler(url, nil, nil, nil)
return
}
}
completionHandler(nil, nil, nil, responseError ?? error)
})
}
override func request(for operation: FileOperationType, overwrite: Bool = true, attributes: [URLResourceKey: Any] = [:]) -> URLRequest {
let method: String
let url: URL
@@ -282,19 +313,17 @@ open class WebDAVFileProvider: HTTPFileProvider {
}
override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
return FileProviderWebDavError(code: code, path: path ?? "", errorDescription: data.flatMap({ String(data: $0, encoding: .utf8) }), url: self.url(of: path))
return FileProviderWebDavError(code: code, path: path ?? "", errorDescription: data.flatMap({ String(data: $0, encoding: .utf8) }), url: self.url(of: path ?? ""))
}
override func multiStatusHandler(source: String, data: Data, completionHandler: SimpleCompletionHandler) {
let xresponses = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
for xresponse in xresponses where (xresponse.status ?? 0) >= 300 {
let code = xresponse.status.flatMap { FileProviderHTTPErrorCode(rawValue: $0) } ?? .internalServerError
let error = FileProviderWebDavError(code: code, path: source, errorDescription: String(data: data, encoding: .utf8), url: self.url(of: source))
let error = self.serverError(with: code, path: source, data: data)
completionHandler?(error)
}
}
}
extension WebDAVFileProvider {
/*
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
@@ -324,7 +353,7 @@ extension WebDAVFileProvider: ExtendedFileProvider {
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((ImageClass?, Error?) -> Void)) {
guard self.baseURL?.host?.contains("dav.yandex.") ?? false else {
dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: URLError.resourceUnavailable))
completionHandler(nil, self.urlError(path, code: .resourceUnavailable))
}
return
}
@@ -335,9 +364,9 @@ extension WebDAVFileProvider: ExtendedFileProvider {
request.httpMethod = "GET"
request.set(httpAuthentication: credential, with: credentialType)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderWebDavError?
var responseError: FileProviderHTTPError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: url.relativePath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
responseError = self.serverError(with: rCode, path: url.relativePath, data: data)
completionHandler(nil, responseError ?? error)
return
}
@@ -353,45 +382,11 @@ extension WebDAVFileProvider: ExtendedFileProvider {
open func propertiesOfFile(path: String, completionHandler: @escaping (([String : Any], [String], Error?) -> Void)) {
dispatch_queue.async {
completionHandler([:], [], self.throwError(path, code: URLError.resourceUnavailable))
completionHandler([:], [], self.urlError(path, code: .resourceUnavailable))
}
}
}
extension WebDAVFileProvider: FileProviderSharing {
open func publicLink(to path: String, completionHandler: @escaping ((URL?, FileObject?, Date?, Error?) -> Void)) {
guard self.baseURL?.host?.contains("dav.yandex.") ?? false else {
dispatch_queue.async {
completionHandler(nil, nil, nil, self.throwError(path, code: URLError.resourceUnavailable))
}
return
}
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PROPPATCH"
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
let body = "<propertyupdate xmlns=\"DAV:\">\n<set><prop>\n<public_url xmlns=\"urn:yandex:disk:meta\">true</public_url>\n</prop></set>\n</propertyupdate>"
request.httpBody = body.data(using: .utf8)
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
runDataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode, code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
}
if let data = data {
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
if let urlStr = xresponse.first?.prop["public_url"], let url = URL(string: urlStr) {
completionHandler(url, nil, nil, nil)
return
}
}
completionHandler(nil, nil, nil, responseError ?? error)
})
}
}
// MARK: WEBDAV XML response implementation
struct DavResponse {
@@ -403,8 +398,12 @@ struct DavResponse {
init? (_ node: AEXMLElement, baseURL: URL?) {
func standardizePath(_ str: String) -> String {
#if swift(>=4.0)
let trimmedStr = str.hasPrefix("/") ? String(str[str.index(after: str.startIndex)...]) : str
#else
let trimmedStr = str.hasPrefix("/") ? str.substring(from: str.index(after: str.startIndex)) : str
return trimmedStr.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed.subtracting(CharacterSet(charactersIn: ":"))) ?? str
#endif
return trimmedStr.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? str
}
// find node names with namespace
@@ -500,9 +499,9 @@ public final class WebDavFileObject: FileObject {
let path = relativePath.hasPrefix("/") ? relativePath : ("/" + relativePath)
super.init(url: href, name: name, path: path)
self.size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown
self.creationDate = Date(rfcString: davResponse.prop["creationdate"] ?? "")
self.modifiedDate = Date(rfcString: davResponse.prop["getlastmodified"] ?? "")
self.contentType = davResponse.prop["getcontenttype"] ?? "octet/stream"
self.creationDate = davResponse.prop["creationdate"].flatMap { Date(rfcString: $0) }
self.modifiedDate = davResponse.prop["getlastmodified"].flatMap { Date(rfcString: $0) }
self.contentType = davResponse.prop["getcontenttype"] ?? "application/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"]
@@ -511,7 +510,7 @@ public final class WebDavFileObject: FileObject {
/// MIME type of the file.
open internal(set) var contentType: String {
get {
return allValues[.mimeTypeKey] as? String ?? ""
return allValues[.mimeTypeKey] as? String ?? "application/octet-stream"
}
set {
allValues[.mimeTypeKey] = newValue
@@ -542,6 +541,11 @@ public final class WebDavFileObject: FileObject {
return "ishidden"
case URLResourceKey.entryTagKey:
return "getetag"
case URLResourceKey.volumeTotalCapacityKey:
// WebDAV doesn't have total capacity, but it's can be calculated via used capacity
return "quota-used-bytes"
case URLResourceKey.volumeAvailableCapacityKey:
return "quota-available-bytes"
default:
return nil
}
@@ -559,6 +563,10 @@ public final class WebDavFileObject: FileObject {
}
return propKeys
}
internal class func xmlProp(_ keys: [URLResourceKey]) -> Data {
return "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n\(WebDavFileObject.propString(keys))\n</D:propfind>".data(using: .utf8)!
}
}
/// Error returned by WebDAV server when trying to access or do operations on a file or folder.