Compare commits

...

34 Commits

Author SHA1 Message Date
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
Amir Abbas cad68da076 Refactored Dropbox, WebDAV & OneDrive to HTTPFileProvider
- FileOperationType.source is not optional anymore
- critical bugfix: KVO exception crash on task completion
2017-08-19 05:51:25 +04:30
Amir Abbas Mousavian 39da09edd4 Merge pull request #60 from evilutioner/master
Fixed file downloading progress
2017-08-15 18:53:21 +04:30
Amir Abbas 5129aee1b5 Added fileURL to Progress 2017-08-15 18:41:48 +04:30
Oleg Marchik 478f0819b5 Fixed file downloading progress 2017-08-15 16:57:50 +03:00
Amir Abbas b2a7800f7c Fixed typo 2017-08-15 15:08:06 +04:30
Amir Abbas 96c102d156 Updated readme and podspec to ver 0.19.0 2017-08-15 14:57:36 +04:30
Amir Abbas 8aedd8e72a Replaced OperationHandle with (NS)Progress 2017-08-15 13:42:41 +04:30
Amir Abbas 5b55debe8a Fix warning in OneDrive provider 2017-08-12 17:50:00 +04:30
Amir Abbas Mousavian 0fa062a946 Merge pull request #59 from evilutioner/master
OneDrive: list folder issue fixed, removed escape symbols in path
2017-08-11 20:43:28 +04:30
Oleg Marchik 879f86c1f9 Merge branch 'master' into master 2017-08-11 18:07:25 +03:00
Oleg Marchik 80d5f02bbd OneDrive: list folder issue fixed, removed escape symbols in path 2017-08-11 18:04:18 +03:00
Amir Abbas Mousavian b8a7721b2f Merge pull request #58 from evilutioner/master
Dropbox bug fixing: added unicode symbols support in http header path
2017-08-10 22:29:08 +04:30
Oleg Marchik 44b4784cd3 Important dropbox bug fixing: added unicode symbols support in path 2017-08-10 11:36:15 +03:00
Amir Abbas 7d9e2247f2 Fixed Compile error, removed redundant comments 2017-08-04 00:30:46 +04:30
Amir Abbas Mousavian 0ace562442 Merge pull request #57 from evilutioner/master
Fixed outdated OneDrive API
2017-08-04 00:27:36 +04:30
evilutioner 63d831ef90 Merge branch 'master' into master 2017-08-01 16:08:51 +03:00
Oleg Marchik d6b91348a3 Fixed outdated OneDrive API 2017-08-01 12:55:39 +03:00
Amir Abbas fd9d4c1ab4 Probable fix for # 55 (OneDrive url issue) 2017-07-31 20:10:40 +04:30
22 changed files with 2651 additions and 2198 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
#
s.name = "FilesProvider"
s.version = "0.18.1"
s.version = "0.20.1"
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.
+38 -14
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 */; };
@@ -50,9 +56,9 @@
794C220E1D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */; };
794C220F1D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */; };
794C22101D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.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 */; };
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 */; };
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 */; };
@@ -140,11 +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>"; };
796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileProviderExtensions.swift; sourceTree = "<group>"; };
795815591F478ED9003344DD /* HTTPFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPFileProvider.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; };
@@ -255,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 = (
@@ -290,26 +307,27 @@
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 */,
79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */,
79BD638B1E2CC2300035128C /* ExtendedLocalFileProvider.swift */,
7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */,
7902C0851D61B56D00564440 /* RemoteSession.swift */,
795815591F478ED9003344DD /* HTTPFileProvider.swift */,
799396931D48C02300086753 /* DropboxFileProvider.swift */,
794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */,
7936BC111E880F5700A6C81C /* FTPFileProvider.swift */,
798654321E8874BC002FA550 /* FTPHelper.swift */,
79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */,
79BD63C41E2D17880035128C /* OneDriveHelper.swift */,
7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */,
799396A61D48C02300086753 /* WebDAVFileProvider.swift */,
7936BC111E880F5700A6C81C /* FTPFileProvider.swift */,
798654321E8874BC002FA550 /* FTPHelper.swift */,
799396971D48C02300086753 /* SMBClient.swift */,
799396981D48C02300086753 /* SMBFileProvider.swift */,
799396A61D48C02300086753 /* WebDAVFileProvider.swift */,
);
path = Sources;
sourceTree = "<group>";
@@ -490,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 */,
@@ -502,6 +519,7 @@
7924B1991D89DAE000589DB7 /* Element.swift in Sources */,
799396C81D48C02300086753 /* SMB2IOCtl.swift in Sources */,
799396D71D48C02300086753 /* SMB2Types.swift in Sources */,
7958155A1F478ED9003344DD /* HTTPFileProvider.swift in Sources */,
798654331E8874BC002FA550 /* FTPHelper.swift in Sources */,
7924B1B21D89FCDA00589DB7 /* FPSStreamTask.swift in Sources */,
799396C51D48C02300086753 /* SMB2FileOperation.swift in Sources */,
@@ -519,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 */,
@@ -533,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 */,
@@ -545,6 +564,7 @@
7924B1B01D89F7DE00589DB7 /* FPSStreamTask.swift in Sources */,
7924B19A1D89DAE000589DB7 /* Element.swift in Sources */,
799396C91D48C02300086753 /* SMB2IOCtl.swift in Sources */,
7958155B1F478ED9003344DD /* HTTPFileProvider.swift in Sources */,
79F4678B1E8B80F200C91A85 /* FTPHelper.swift in Sources */,
799396D81D48C02300086753 /* SMB2Types.swift in Sources */,
799396C61D48C02300086753 /* SMB2FileOperation.swift in Sources */,
@@ -562,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 */,
@@ -576,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 */,
@@ -588,6 +609,7 @@
7924B1B11D89F7DF00589DB7 /* FPSStreamTask.swift in Sources */,
7924B19B1D89DAE000589DB7 /* Element.swift in Sources */,
799396CA1D48C02300086753 /* SMB2IOCtl.swift in Sources */,
7958155C1F478ED9003344DD /* HTTPFileProvider.swift in Sources */,
79F4678C1E8B80F200C91A85 /* FTPHelper.swift in Sources */,
799396D91D48C02300086753 /* SMB2Types.swift in Sources */,
799396C71D48C02300086753 /* SMB2FileOperation.swift in Sources */,
@@ -605,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 */,
@@ -621,7 +645,7 @@
799396601D48B7BF00086753 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.18.1;
BUNDLE_VERSION_STRING = 0.20.1;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -653,7 +677,7 @@
799396611D48B7BF00086753 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.18.1;
BUNDLE_VERSION_STRING = 0.20.1;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
+10 -10
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.
@@ -356,6 +356,12 @@ documentsProvider.copyItem(path: "/download/image.jpg", toLocalURL: fileURL, ove
* It's safe only to assume these methods **won't** handle directories to upload/download recursively. If you need, you can list directories, create directories on target and copy files using these methods.
* FTP provider allows developer to either use apple implemented `URLSessionDownloadTask` or custom implemented method based on stream task via `useAppleImplementation` property. FTP protocol is not supported by background session.
### Operation Progress
Creating/Copying/Deleting/Searching functions return a `(NS)Progress`. It provides operation type, progress and a `.cancel()` method which allows you to cancel operation in midst. You can check `cancellable` property to check either you can cancel operation via this object or not.
- **Note:** Progress reporting is not supported by native `(NS)FileManager` so `LocalFileProvider`.
### Undo Operations
Providers conform to `FileProviderUndoable` can perform undo for **some** operations like moving/renaming, copying and creating (file or folder). **Now, only `LocalFileProvider` supports this feature.** To implement:
@@ -399,12 +405,6 @@ class ViewController: UIViewController
}
```
### Operation Handle
Creating/Copying/Deleting functions return a `OperationHandle` for remote operations. It provides operation type, progress and a `.cancel()` method which allows you to cancel operation in midst.
It's not supported by native `(NS)FileManager` so `LocalFileProvider`, but this functionality will be added to future `PosixFileProvider` class.
### File Coordination
`LocalFileProvider` and its descendents has a `isCoordinating` property. By setting this, provider will use `NSFileCoordinating` class to do all file operations. It's mandatory for iCloud, while recommended when using shared container or anywhere that simultaneous operations on a file/folder is common.
@@ -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.
+155 -197
View File
@@ -52,7 +52,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
- 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) {
assert(!CloudFileProvider.asserting || !Thread.isMainThread, "LocalFileProvider.init(containerId:) is not recommended to be executed on Main Thread.")
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
}
@@ -118,7 +118,7 @@ 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 path: path to target directory. If empty, root will be iterated.
- 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.
@@ -187,7 +187,7 @@ 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 path: path to target directory. If empty, attributes of root will be returned.
- 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.
@@ -243,13 +243,13 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
- Note: For now only it's limited to file names. `query` parameter may take `NSPredicate` format in near future.
- Parameters:
- path: location of directory to start search
- recursive: Searching subdirectories of path
- query: Simple string of file name to be search (for now).
- foundItemHandler: 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: 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.
*/
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
let mapDict: [String: String] = ["url": NSMetadataItemURLKey, "name": NSMetadataItemFSNameKey, "path": NSMetadataItemPathKey, "filesize": NSMetadataItemFSSizeKey, "modifiedDate": NSMetadataItemFSContentChangeDateKey, "creationDate": NSMetadataItemFSCreationDateKey, "contentType": NSMetadataItemContentTypeKey]
@@ -282,14 +282,21 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
}
}
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?
@@ -314,6 +321,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
}
}
lastReportedCount = mdquery.resultCount
progress.totalUnitCount = Int64(lastReportedCount)
mdquery.enableUpdates()
})
@@ -346,12 +354,14 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
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.throwError(path, code: CocoaError.fileReadNoPermission))
@@ -359,6 +369,8 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
}
}
}
return progress
}
open override func isReachable(completionHandler: @escaping (Bool) -> Void) {
@@ -367,55 +379,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: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
*/
@discardableResult
open override func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
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: An `OperationHandle` 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) -> OperationHandle? {
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: An `OperationHandle` 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) -> OperationHandle? {
return super.copyItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler)
}
/**
Removes the file or directory at the specified path.
@@ -426,11 +389,10 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
- Parameters:
- path: file or directory path.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
- Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
*/
@discardableResult
open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return super.removeItem(path: path, completionHandler: completionHandler)
}
@@ -443,13 +405,19 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
- to: destination path of file, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress.
- Returns: A `Progress` object to get progress or cancel progress.
*/
@discardableResult
open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
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 operationHandle = CloudOperationHandle(operationType: opType, baseURL: self.baseURL)
let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
let progress = Progress(parent: nil, userInfo: nil)
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, operation: operation, progress: progress)
operation_queue.addOperation {
let tempFolder: URL
if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) {
@@ -464,20 +432,16 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
let toUrl = self.url(of: toPath)
try self.opFileManager.setUbiquitous(true, itemAt: tmpFile, destinationURL: toUrl)
completionHandler?(nil)
DispatchQueue.main.async(execute: {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
self.delegateNotify(operation)
} catch let e {
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)
})
self.delegateNotify(operation, error: e)
}
}
return operationHandle
return progress
}
/**
@@ -488,23 +452,27 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
- path: original file or directory path.
- toLocalURL: destination local url of file, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress.
- Returns: A `Progress` object to get progress or cancel progress.
*/
@discardableResult
open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(operation, 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, 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)
})
self.delegateNotify(operation, error: e)
return nil
}
let handle = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler)
return handle
let _ = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler)
return progress
}
/**
@@ -516,11 +484,19 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
- completionHandler: a closure with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress.
- Returns: A `Progress` object to get progress or cancel progress.
*/
@discardableResult
open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
return super.contents(path: path, completionHandler: completionHandler)
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, operation: operation, progress: progress)
_ = super.contents(path: path, completionHandler: completionHandler)
return progress
}
/**
@@ -534,11 +510,19 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
- completionHandler: a closure with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress.
- Returns: A `Progress` object to get progress or cancel progress.
*/
@discardableResult
open override func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
return super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler)
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, operation: operation, progress: progress)
_ = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler)
return progress
}
/**
@@ -550,11 +534,19 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
open override func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler)
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)
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, operation: operation, progress: progress)
_ = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler)
return progress
}
fileprivate var monitors = [String: (NSMetadataQuery, NSObjectProtocol)]()
@@ -639,62 +631,51 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
return file
}
func monitorFile(path: String, opType: FileOperationType) {
fileprivate func monitorFile(path: String, operation: 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: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
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) {
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(uploaded / 100))
}
progress?.completedUnitCount = Int64(uploaded / 100 * Double(progress?.totalUnitCount ?? 0))
self.delegateNotify(operation, progress: uploaded / 100)
} else if (uploaded == 0 || uploaded == 100) && (downloaded > 0 && downloaded < 100) {
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(downloaded / 100))
}
progress?.completedUnitCount = Int64(downloaded / 100 * Double(progress?.totalUnitCount ?? 0))
self.delegateNotify(operation, progress: 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)
}
self.delegateNotify(operation)
}
query.enableUpdates()
})
DispatchQueue.main.async {
progress?.setUserInfoObject(Date(), forKey: .startingTimeKey)
query.start()
}
}
}
/// Removes local copy of file, but spares cloud copy/
/// - Parameter path: Path of file or directory to be remoed from local
/// - Parameter completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
open func evictItem(path: String, completionHandler: SimpleCompletionHandler) {
operation_queue.addOperation {
do {
try self.opFileManager.evictUbiquitousItem(at: self.url(of: path))
completionHandler?(nil)
} catch let e {
completionHandler?(e)
}
}
}
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
operation_queue.addOperation {
do {
@@ -710,6 +691,35 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
}
}
}
/// Removes local copy of file, but spares cloud copy.
/// - Parameter path: Path of file or directory to be removed from local
/// - Parameter completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
open func evictItem(path: String, completionHandler: SimpleCompletionHandler) {
operation_queue.addOperation {
do {
try self.opFileManager.evictUbiquitousItem(at: self.url(of: path))
completionHandler?(nil)
} catch let e {
completionHandler?(e)
}
}
}
/// Returns current version of file on this device and all versions of files in user devices.
/// - Parameter path: Path of file or directory.
/// - Parameter completionHandler: Retrieve current version on this device and all versions available. `currentVersion` will be nil if file doesn't exist. If an error parameter was provided, a presentable `Error` will be returned.
func versionsOfItem(path: String, completionHandler: @escaping ((_ currentVersion: NSFileVersion?, _ versions: [NSFileVersion], _ error: Error?) -> Void)) {
NotImplemented()
}
/// Resolves conflicts by selecting a version.
/// - Parameter path: Path of file or directory.
/// - Parameter version: Version than will be choose as main version. `nil` value indicates current version on this device will be selected.
/// - Parameter completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
func selectVersionOfItem(path: String, version: NSFileVersion? = nil, completionHandler: SimpleCompletionHandler) {
NotImplemented()
}
}
/// Scope of iCloud, wrapper for NSMetadataQueryUbiquitous...Scope constants
@@ -717,9 +727,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
@@ -745,92 +759,36 @@ public enum UbiquitousScope: RawRepresentable {
}
}
/// Get progress of CloudFileProvider operations
open class CloudOperationHandle: OperationHandle {
/// Url of file which operation is doing on
public let baseURL: URL?
/// Type of operation
public let operationType: FileOperationType
/*
func getMetadataItem(url: URL) -> NSMetadataItem? {
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]
init (operationType: FileOperationType, baseURL: URL?) {
self.baseURL = baseURL
self.operationType = operationType
}
var item: NSMetadataItem?
private var sourceURL: URL? {
guard let source = operationType.source, let baseURL = baseURL else { return nil }
return source.hasPrefix("file://") ? URL(fileURLWithPath: source) : baseURL.appendingPathComponent(source)
}
private var destURL: URL? {
guard let dest = operationType.destination, let baseURL = baseURL else { return nil }
return dest.hasPrefix("file://") ? URL(fileURLWithPath: dest) : baseURL.appendingPathComponent(dest)
}
open var bytesSoFar: Int64 {
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return 0 }
let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0
let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0
guard let size = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 else { return -1 }
if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) {
return Int64(uploaded * (Double(size) / 100))
} else if (uploaded == 0 || uploaded == 100) && (downloaded > 0 && downloaded < 100) {
return Int64(downloaded * (Double(size) / 100))
} else if uploaded == 100 || downloaded == 100 {
return size
let group = DispatchGroup()
group.enter()
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
defer {
query.stop()
group.leave()
NotificationCenter.default.removeObserver(finishObserver!)
}
return 0
}
open var totalBytes: Int64 {
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return -1 }
return item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 ?? -1
}
open var inProgress: Bool {
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return false }
let downloadStatus = item.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String ?? NSMetadataUbiquitousItemDownloadingStatusNotDownloaded
let isUploading = item.value(forAttribute: NSMetadataUbiquitousItemIsUploadingKey) as? Bool ?? false
return downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent || isUploading
}
/// Not usable in local provider
open func cancel() -> Bool {
return false
}
fileprivate static func getMetadataItem(url: URL) -> NSMetadataItem? {
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]
var item: NSMetadataItem?
let group = DispatchGroup()
group.enter()
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
defer {
query.stop()
group.leave()
NotificationCenter.default.removeObserver(finishObserver!)
}
if query.resultCount > 0 {
item = query.result(at: 0) as? NSMetadataItem
}
query.disableUpdates()
})
DispatchQueue.main.async {
query.start()
if query.resultCount > 0 {
item = query.result(at: 0) as? NSMetadataItem
}
_ = group.wait(timeout: .now() + 30)
return item
query.disableUpdates()
})
DispatchQueue.main.async {
query.start()
}
_ = group.wait(timeout: .now() + 30)
return item
}
*/
+84 -255
View File
@@ -16,70 +16,14 @@ import CoreGraphics
- Note: Uploading files and data are limited to 150MB, for now.
*/
open class DropboxFileProvider: FileProviderBasicRemote {
open class var type: String { return "Dropbox" }
open let baseURL: URL?
open var currentPath: String
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/)
open let apiURL: URL
/// Dropbox contents download/upload API URL, which is equal with [https://content.dropboxapi.com/2/](https://content.dropboxapi.com/2/)
open let contentURL: URL
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue {
willSet {
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
}
}
open weak var delegate: FileProviderDelegate?
open var credential: URLCredential? {
didSet {
sessionDelegate?.credential = self.credential
}
}
open private(set) var cache: URLCache?
public var useCache: Bool
public var validatingCache: Bool
fileprivate var _session: URLSession?
fileprivate 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!)
}
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
}
self.sessionDelegate = newValue.delegate as? SessionDelegate
initEmptySessionHandler(_session!.sessionDescription!)
}
}
fileprivate var _longpollSession: URLSession?
internal var longpollSession: URLSession {
if _longpollSession == nil {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 600
_longpollSession = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
}
return _longpollSession!
}
/**
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).
@@ -91,24 +35,9 @@ open class DropboxFileProvider: FileProviderBasicRemote {
- Parameter cache: A URLCache to cache downloaded files and contents.
*/
public init(credential: URLCredential?, cache: URLCache? = nil) {
self.baseURL = nil
self.currentPath = ""
self.useCache = false
self.validatingCache = true
self.cache = cache
self.credential = credential
self.apiURL = URL(string: "https://api.dropboxapi.com/2/")!
self.contentURL = URL(string: "https://content.dropboxapi.com/2/")!
#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"
super.init(baseURL: nil, credential: credential, cache: cache)
}
public required convenience init?(coder aDecoder: NSCoder) {
@@ -118,18 +47,7 @@ open class DropboxFileProvider: FileProviderBasicRemote {
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
}
public func encode(with aCoder: NSCoder) {
aCoder.encode(self.credential, forKey: "credential")
aCoder.encode(self.currentPath, forKey: "currentPath")
aCoder.encode(self.useCache, forKey: "useCache")
aCoder.encode(self.validatingCache, forKey: "validatingCache")
}
public static var supportsSecureCoding: Bool {
return true
}
open func copy(with zone: NSZone? = nil) -> Any {
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
@@ -139,31 +57,19 @@ open class DropboxFileProvider: FileProviderBasicRemote {
return copy
}
deinit {
if let sessionuuid = _session?.sessionDescription {
removeSessionHandler(for: sessionuuid)
}
if fileProviderCancelTasksOnInvalidating {
_session?.invalidateAndCancel()
} else {
_session?.finishTasksAndInvalidate()
}
}
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
list(path) { (contents, cursor, error) in
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)
}
}
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
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
@@ -181,7 +87,7 @@ open class DropboxFileProvider: FileProviderBasicRemote {
task.resume()
}
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
open override func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
let url = URL(string: "users/get_space_usage", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
@@ -198,12 +104,13 @@ open class DropboxFileProvider: FileProviderBasicRemote {
task.resume()
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
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, foundItem: { (file) in
search(path, query: queryStr, progress: progress, foundItem: { (file) in
if query.evaluate(with: file.mapPredicate()) {
foundFiles.append(file)
foundItemHandler?(file)
@@ -214,7 +121,7 @@ open class DropboxFileProvider: FileProviderBasicRemote {
} 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, progressHandler: { (files, _, error) in
list(path, recursive: true, progress: progress, progressHandler: { (files, _, error) in
for file in files where query.evaluate(with: file.mapPredicate()) {
foundItemHandler?(file)
}
@@ -223,59 +130,75 @@ open class DropboxFileProvider: FileProviderBasicRemote {
completionHandler(predicatedFiles, error)
})
}
return progress
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
self.storageProperties { total, _ in
completionHandler(total > 0)
override func request(for operation: FileOperationType, overwrite: Bool = false, 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)?.format(with: .rfc3339) as NSString?
request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(httpContentType: .stream)
request.set(dropboxArgKey: requestDictionary)
case .modify(let path):
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)?.format(with: .rfc3339) as NSString?
request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(httpContentType: .stream)
request.set(dropboxArgKey: requestDictionary)
default:
return self.apiRequest(for: operation, overwrite: overwrite)
}
return request
}
open weak var fileOperationDelegate: FileOperationDelegate?
}
extension DropboxFileProvider: FileProviderOperations {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
return doOperation(.create(path: path), completionHandler: completionHandler)
}
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
}
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
}
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.remove(path: path), completionHandler: completionHandler)
}
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
func apiRequest(for operation: FileOperationType, overwrite: Bool = false) -> URLRequest {
let url: String
guard let sourcePath = operation.source else { return nil }
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
return nil
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
@@ -283,115 +206,25 @@ extension DropboxFileProvider: FileProviderOperations {
requestDictionary["path"] = correctPath(sourcePath) as NSString?
}
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderDropboxError?
if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
serverError = FileProviderDropboxError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
completionHandler?(serverError ?? error)
self.delegateNotify(operation, error: serverError ?? error)
})
task.taskDescription = operation.json
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
return request
}
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
// 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))
}
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) }))
}
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
}
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
return super.upload_simple(targetPath, request: request, data: data, localFile: localFile, operation: operation, completionHandler: completionHandler)
}
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = URL(string: "files/download", relativeTo: contentURL)!
var request = URLRequest(url: url)
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(dropboxArgKey: ["path": path as NSString])
let task = session.downloadTask(with: request)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = completionHandler
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 : FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
completionHandler?(serverError)
return
}
do {
try FileManager.default.moveItem(at: tempURL, to: destURL)
completionHandler?(nil)
} catch let e {
completionHandler?(e)
}
}
task.taskDescription = opType.json
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
}
}
extension DropboxFileProvider: FileProviderReadWrite {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
}
return nil
}
let opType = FileOperationType.fetch(path: path)
let url = URL(string: "files/download", relativeTo: contentURL)!
var request = URLRequest(url: url)
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(rangeWithOffset: offset, length: length)
request.set(dropboxArgKey: ["path": correctPath(path)! as NSString])
let task = session.downloadTask(with: request)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
completionHandler(nil, 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 : FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
completionHandler(nil, serverError)
return
}
do {
let data = try Data(contentsOf: tempURL)
completionHandler(data, nil)
} catch let e {
completionHandler(nil, e)
}
}
task.taskDescription = opType.json
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
}
public func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
// FIXME: remove 150MB restriction
return upload_simple(path, data: data ?? Data(), overwrite: overwrite, operation: opType, completionHandler: completionHandler)
}
/*
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
/* There is two ways to monitor folders changing in Dropbox. Either using webooks
@@ -407,15 +240,13 @@ extension DropboxFileProvider: FileProviderReadWrite {
}
*/
// 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
@@ -461,7 +292,7 @@ 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, "url" : remoteURL.absoluteString as NSString]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
@@ -496,7 +327,7 @@ 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
@@ -579,7 +410,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.throwError(path, code: URLError.cannotDecodeRawData))
}
}
if let data = data {
@@ -605,7 +436,7 @@ 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
@@ -624,5 +455,3 @@ extension DropboxFileProvider: ExtendedFileProvider {
task.resume()
}
}
extension DropboxFileProvider: FileProvider { }
+28 -60
View File
@@ -23,6 +23,10 @@ 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)
@@ -71,7 +75,11 @@ 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, progressHandler: ((_ contents: [FileObject], _ nextCursor: String?, _ error: Error?) -> Void)? = nil, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
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 {
@@ -85,7 +93,7 @@ internal extension DropboxFileProvider {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
request.set(httpContentType: .json)
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = (session ?? self.session).dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderDropboxError?
@@ -99,13 +107,14 @@ internal extension DropboxFileProvider {
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 {
if hasmore && !progress.isCancelled {
progressHandler?(files, ncursor, responseError ?? error)
self.list(path, cursor: ncursor, prevContents: prevContents + files, completionHandler: completionHandler)
self.list(path, cursor: ncursor, prevContents: prevContents + files, progress: progress, completionHandler: completionHandler)
return
}
}
@@ -113,58 +122,22 @@ internal extension DropboxFileProvider {
progressHandler?(files, nil, responseError ?? error)
completionHandler(prevContents + files, nil, responseError ?? error)
})
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.taskDescription = FileOperationType.fetch(path: path).json
task.resume()
}
func upload_simple(_ targetPath: String, data: Data? = nil, localFile: URL? = nil, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
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(.create(path: targetPath), error: error)
return nil
}
var requestDictionary = [String: AnyObject]()
let url: URL
url = URL(string: "files/upload", relativeTo: contentURL)!
requestDictionary["path"] = correctPath(targetPath) as NSString?
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
requestDictionary["client_modified"] = modifiedDate.rfc3339utc() as NSString
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .stream)
request.set(dropboxArgKey: requestDictionary)
let task: URLSessionUploadTask
if let data = data {
task = session.uploadTask(with: request, from: data)
} else if let localFile = localFile {
task = session.uploadTask(with: request, fromFile: localFile)
} else {
return nil
}
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 }
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
var responseError: FileProviderDropboxError?
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
// We can't fetch server result from delegate!
responseError = FileProviderDropboxError(code: rCode, path: targetPath, errorDescription: nil)
}
completionHandler?(responseError ?? error)
self?.delegateNotify(.create(path: targetPath), error: responseError ?? error)
}
task.taskDescription = operation.json
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
}
func search(_ startPath: String = "", query: String, start: Int = 0, maxResultPerPage: Int = 25, maxResults: Int = -1, foundItem:@escaping ((_ file: DropboxFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
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)
request.set(httpContentType: .json)
var requestDictionary: [String: AnyObject] = ["path": startPath as NSString]
requestDictionary["query"] = query as NSString
requestDictionary["start"] = start as NSNumber
@@ -180,12 +153,13 @@ internal extension DropboxFileProvider {
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, let rstart = rstart {
self.search(startPath, query: query, start: rstart + entries.count, maxResultPerPage: maxResultPerPage, foundItem: foundItem, completionHandler: completionHandler)
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)
}
@@ -193,7 +167,11 @@ internal extension DropboxFileProvider {
}
}
completionHandler(responseError ?? error)
})
})
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
}
}
@@ -230,14 +208,4 @@ internal extension DropboxFileProvider {
}
return (dic, keys)
}
func delegateNotify(_ operation: FileOperationType, error: Error?) {
DispatchQueue.main.async(execute: {
if error == nil {
self.delegate?.fileproviderSucceed(self, operation: operation)
} else {
self.delegate?.fileproviderFailed(self, operation: operation)
}
})
}
}
+478
View File
@@ -0,0 +1,478 @@
//
// 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")
}
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)
}
}
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(describing: value)
let quality = 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
}
}
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 []
}
}
}
extension URLError.Code: FoundationErrorEnum {}
extension CocoaError.Code: FoundationErrorEnum {}
+462
View File
@@ -0,0 +1,462 @@
// 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 func calculate(_ message: [UInt8]) -> [UInt8] {
var tmpMessage = message
let len = 64
// 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 = [UInt32]()
Self.h.forEach {(h) -> () in
hh.append(UInt32(h))
}
// 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 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: Self.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 = 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)
M[x] = M[x-16] &+ s0 &+ M[x-7] &+ s1
break
}
}
var A = hh[0]
var B = hh[1]
var C = hh[2]
var D = hh[3]
var E = hh[4]
var F = hh[5]
var G = hh[6]
var H = hh[7]
// Main loop
for j in 0..<Self.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(Self.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
result += [UInt8(item & 0xff), UInt8((item >> 8) & 0xff), UInt8((item >> 16) & 0xff), UInt8((item >> 24) & 0xff)]
}
return result
}
}
extension SHA2Variant64 {
static func calculate(_ message: [UInt8]) -> [UInt8] {
var tmpMessage = message
let len = 128
// 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.forEach {(h) -> () in
hh.append(h)
}
// 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: Self.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)
M[x] = M[x-16] &+ s0 &+ M[x-7] &+ s1
break
}
}
var A = hh[0]
var B = hh[1]
var C = hh[2]
var D = hh[3]
var E = hh[4]
var F = hh[5]
var G = hh[6]
var H = hh[7]
// Main loop
for j in 0..<Self.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 &+ Self.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
}
}
final class SHA256 : SHA2Variant32 {
static let size = 64
static let h: [UInt64] = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]
static let k: [UInt64] = [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]
static func resultingArray<T>(_ hh: [T]) -> ArraySlice<T> {
return ArraySlice(hh)
}
}
final class SHA384 : SHA2Variant64 {
static let size = 128
static let h: [UInt64] = [0xcbbb9d5dc1059ed8, 0x629a292a367cd507, 0x9159015a3070dd17, 0x152fecd8f70e5939, 0x67332667ffc00b31, 0x8eb44a8768581511, 0xdb0c2e0d64f98fa7, 0x47b5481dbefa4fa4]
static let k: [UInt64] = [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]
public static func resultingArray<T>(_ hh: [T]) -> ArraySlice<T> {
return hh[0..<6]
}
}
final class SHA512 : SHA2Variant64 {
static let size = 128
static let h: [UInt64] = [0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179]
static let k: [UInt64] = [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]
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))
}
}
+76 -9
View File
@@ -26,7 +26,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
fileprivate var _taskDescription: String?
/// Force using `URLSessionStreamTask` for iOS 9 and later
public var useURLSession = true
public let useURLSession: Bool
@available(iOS 9.0, OSX 10.11, *)
fileprivate static var streamTasks = [Int: URLSessionStreamTask]()
@@ -121,8 +121,31 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
}
}
fileprivate var _countOfBytesSent: Int64 = 0
fileprivate var _countOfBytesRecieved: Int64 = 0
fileprivate var _countOfBytesSent: Int64 = 0 {
willSet {
for observer in observers where observer.keyPath == "countOfBytesSent" {
observer.observer.observeValue(forKeyPath: observer.keyPath, of: self, change: [.oldKey: _countOfBytesSent, .oldKey: newValue], context: observer.context)
}
}
didSet {
for observer in observers where observer.keyPath == "countOfBytesSent" {
observer.observer.observeValue(forKeyPath: observer.keyPath, of: self, change: [.oldKey: oldValue, .oldKey: _countOfBytesSent], context: observer.context)
}
}
}
fileprivate var _countOfBytesRecieved: Int64 = 0 {
willSet {
for observer in observers where observer.keyPath == "countOfBytesRecieved" {
observer.observer.observeValue(forKeyPath: observer.keyPath, of: self, change: [.oldKey: _countOfBytesRecieved, .oldKey: newValue], context: observer.context)
}
}
didSet {
for observer in observers where observer.keyPath == "countOfBytesRecieved" {
observer.observer.observeValue(forKeyPath: observer.keyPath, of: self, change: [.oldKey: oldValue, .oldKey: _countOfBytesRecieved], context: observer.context)
}
}
}
/**
* The number of bytes that the task has sent to the server in the request body.
@@ -133,7 +156,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)` delegate method.
*/
override open var countOfBytesSent: Int64 {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
return _underlyingTask!.countOfBytesSent
}
@@ -192,6 +215,49 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
return Int64(dataReceived.count)
}
var observers: [(keyPath: String, observer: NSObject, context: UnsafeMutableRawPointer?)] = []
public override func addObserver(_ observer: NSObject, forKeyPath keyPath: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer?) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
self._underlyingTask?.addObserver(observer, forKeyPath: keyPath, options: options, context: context)
return
}
}
switch keyPath {
case #keyPath(countOfBytesSent):
fallthrough
case #keyPath(countOfBytesReceived):
fallthrough
case #keyPath(countOfBytesExpectedToSend):
fallthrough
case #keyPath(countOfBytesExpectedToReceive):
observers.append((keyPath: keyPath, observer: observer, context: context))
default:
break
}
super.addObserver(observer, forKeyPath: keyPath, options: options, context: context)
}
public override func removeObserver(_ observer: NSObject, forKeyPath keyPath: String) {
var newObservers: [(keyPath: String, observer: NSObject, context: UnsafeMutableRawPointer?)] = []
for observer in observers where observer.keyPath != keyPath {
newObservers.append(observer)
}
self.observers = newObservers
super.removeObserver(observer, forKeyPath: keyPath)
}
public override func removeObserver(_ observer: NSObject, forKeyPath keyPath: String, context: UnsafeMutableRawPointer?) {
var newObservers: [(keyPath: String, observer: NSObject, context: UnsafeMutableRawPointer?)] = []
for observer in observers where observer.keyPath != keyPath || observer.context != context {
newObservers.append(observer)
}
self.observers = newObservers
super.removeObserver(observer, forKeyPath: keyPath, context: context)
}
override public init() {
fatalError("Use NSURLSession.fpstreamTask() method")
}
@@ -199,10 +265,11 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
fileprivate var host: (hostname: String, port: Int)?
fileprivate var service: NetService?
internal init(session: URLSession, host: String, port: Int) {
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 self.useURLSession {
if useURLSession {
let task = session.streamTask(withHostName: host, port: port)
self._taskIdentifier = task.taskIdentifier
FileProviderStreamTask.streamTasks[_taskIdentifier] = task
@@ -218,10 +285,11 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
self.operation_queue.maxConcurrentOperationCount = 1
}
internal init(session: URLSession, netService: NetService) {
internal init(session: URLSession, netService: NetService, useURLSession: Bool = true) {
self._underlyingSession = session
self.useURLSession = useURLSession
if #available(iOS 9.0, OSX 10.11, *) {
if self.useURLSession {
if useURLSession {
let task = session.streamTask(with: netService)
self._taskIdentifier = task.taskIdentifier
FileProviderStreamTask.streamTasks[_taskIdentifier] = task
@@ -437,7 +505,6 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
return
}
operation_queue.addOperation {
self.dataToBeSent.append(data)
let result = self.write(timeout: timeout, close: false)
+373 -282
View File
@@ -12,7 +12,7 @@ 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
@@ -161,7 +161,7 @@ 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.
@@ -216,7 +216,7 @@ 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.
@@ -279,26 +279,46 @@ open class FTPFileProvider: FileProviderBasicRemote {
}
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
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(parent: nil, userInfo: nil)
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)
}
}
}, 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 ?? self.currentPath).trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? (path ?? self.currentPath)
var baseUrlComponent = URLComponents(url: self.baseURL!, resolvingAgainstBaseURL: true)
baseUrlComponent?.user = credential?.user
@@ -306,7 +326,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 {
@@ -327,217 +347,25 @@ open class FTPFileProvider: FileProviderBasicRemote {
}
open weak var fileOperationDelegate: FileOperationDelegate?
}
extension FTPFileProvider: FileProviderOperations {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
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)
}
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
}
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
}
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.remove(path: path), completionHandler: completionHandler)
}
fileprivate func doOperation(_ opType: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
guard let sourcePath = opType.source else { return nil }
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 operationHandle = RemoteOperationHandle(operationType: opType, tasks: [])
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:
let opHandle = self.fallbackCopy(opType, completionHandler: completionHandler) as? RemoteOperationHandle
operationHandle.tasks = opHandle?.tasks ?? []
return
case .move:
errorCode = URLError.cannotMoveFile
case .remove:
self.fallbackRemove(opType, on: task, completionHandler: completionHandler)
return
case .link:
errorCode = URLError.cannotWriteToFile
default:
errorCode = URLError.cannotOpenFile
}
let error = self.throwError(sourcePath, code: errorCode)
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
return
}
self.dispatch_queue.async {
completionHandler?(nil)
self.delegateNotify(opType, error: nil)
}
})
}
operationHandle.add(task: task)
return operationHandle
}
private func fallbackCopy(_ opType: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard let sourcePath = opType.source else { return nil }
guard let destPath = opType.destination else { return nil }
let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).appendingPathExtension("tmp")
let operationHandle = RemoteOperationHandle(operationType: opType, tasks: [])
let firstOp = self.copyItem(path: sourcePath, toLocalURL: localURL, completionHandler: { (error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
return
}
let secondOp = self.copyItem(localFile: localURL, to: destPath, completionHandler: { error in
completionHandler?(nil)
self.delegateNotify(opType, error: nil)
}) as? RemoteOperationHandle
operationHandle.tasks = secondOp?.tasks ?? []
}) as? RemoteOperationHandle
operationHandle.tasks = firstOp?.tasks ?? []
return operationHandle
}
private func fallbackRemove(_ opType: FileOperationType, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
guard let sourcePath = opType.source else { return }
self.execute(command: "SITE RMDIR \(ftpPath(sourcePath))", on: task) { (response, error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
return
}
guard let response = response else {
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, 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, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
guard let sourcePath = opType.source else { return }
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 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
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) -> OperationHandle? {
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 {
@@ -546,53 +374,66 @@ extension FTPFileProvider: FileProviderOperations {
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 operation = RemoteOperationHandle(operationType: opType, tasks: [])
let progress = Progress(totalUnitCount: 0)
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 {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
self.delegateNotify(operation, error: error)
}
return
}
self.ftpStore(task, filePath: self.ftpPath(toPath), fromData: nil, fromFile: localFile, onTask: {
operation.add(task: $0)
}, onProgress: { bytesSent, totalSent, expectedBytes in
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(Double(totalSent) / Double(expectedBytes)))
self.ftpStore(task, filePath: self.ftpPath(toPath), fromData: nil, fromFile: localFile, onTask: { task in
weak var weakTask = task
progress.cancellationHandler = {
weakTask?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { bytesSent, totalSent, expectedBytes in
progress.completedUnitCount = totalSent
self.delegateNotify(operation, progress: progress.fractionCompleted)
}, completionHandler: { (error) in
if error != nil {
progress.cancel()
}
self.ftpQuit(task)
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
self.delegateNotify(operation, error: error)
}
})
}
return operation
return progress
}
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let operation = RemoteOperationHandle(operationType: opType, tasks: [])
var progress = Progress(totalUnitCount: 0)
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)
self.delegateNotify(operation, error: error)
}
return
}
@@ -601,11 +442,13 @@ extension FTPFileProvider: FileProviderOperations {
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
}
progress.totalUnitCount = file?.size ?? 0
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
@@ -616,8 +459,13 @@ extension FTPFileProvider: FileProviderOperations {
completionHandler?(e)
}
}
operation.add(task: task)
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
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
})
} else {
@@ -630,16 +478,22 @@ extension FTPFileProvider: FileProviderOperations {
return
}
self.ftpRetrieveFile(task, filePath: self.ftpPath(path), onTask: {
operation.add(task: $0)
self.ftpRetrieveFile(task, filePath: self.ftpPath(path), onTask: { task in
weak var weakTask = task
progress.cancellationHandler = {
weakTask?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { recevied, totalReceived, totalSize in
let progress = Double(totalReceived) / Double(totalSize)
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress))
progress.totalUnitCount = totalSize
progress.completedUnitCount = totalReceived
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
}
@@ -648,26 +502,32 @@ 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)
}
}
}
}
}
return operation
return progress
}
}
extension FTPFileProvider: FileProviderReadWrite {
open func contents(path: String, completionHandler: @escaping ((Data?, Error?) -> Void)) -> OperationHandle? {
let opType = FileOperationType.fetch(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
open func contents(path: String, completionHandler: @escaping ((Data?, Error?) -> Void)) -> Progress? {
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(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.downloadTask(with: url(of: path))
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
progress.cancel()
}
completionHandler(nil, error)
}
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
@@ -678,24 +538,33 @@ extension FTPFileProvider: FileProviderReadWrite {
completionHandler(nil, e)
}
}
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
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
} else {
return self.contents(path: path, offset: 0, length: -1, completionHandler: completionHandler)
}
}
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
let opType = FileOperationType.fetch(path: path)
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
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 operation = RemoteOperationHandle(operationType: opType, tasks: [])
let progress = Progress(totalUnitCount: 0)
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
@@ -706,16 +575,22 @@ extension FTPFileProvider: FileProviderReadWrite {
return
}
self.ftpRetrieveData(task, filePath: self.ftpPath(path), from: offset, length: length, onTask: {
operation.add(task: $0)
self.ftpRetrieveData(task, filePath: self.ftpPath(path), from: offset, length: length, onTask: { task in
weak var weakTask = task
progress.cancellationHandler = {
weakTask?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { recevied, totalReceived, totalSize in
let progress = Double(totalReceived) / Double(totalSize)
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress))
progress.totalUnitCount = totalSize
progress.completedUnitCount = totalReceived
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
}
@@ -723,44 +598,54 @@ extension FTPFileProvider: FileProviderReadWrite {
if let data = data {
self.dispatch_queue.async {
completionHandler(data, nil)
self.delegateNotify(opType, error: nil)
self.delegateNotify(operation)
}
}
}
}
return operation
return progress
}
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
open 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 operation = RemoteOperationHandle(operationType: opType, tasks: [])
let progress = Progress(totalUnitCount: Int64(data?.count ?? 0))
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 {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
self.delegateNotify(operation, error: error)
}
return
}
let storeHandler = {
self.ftpStore(task, filePath: self.ftpPath(path), fromData: data ?? Data(), fromFile: nil, onTask: {
operation.add(task: $0)
}, onProgress: { bytesSent, totalSent, expectedBytes in
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(Double(totalSent) / Double(expectedBytes)))
self.ftpStore(task, filePath: self.ftpPath(path), fromData: data ?? Data(), fromFile: nil, onTask: { task in
weak var weakTask = task
progress.cancellationHandler = {
weakTask?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { bytesSent, totalSent, expectedBytes in
progress.completedUnitCount = totalSent
self.delegateNotify(operation, progress: progress.fractionCompleted)
}, completionHandler: { (error) in
if error != nil {
progress.cancel()
}
self.ftpQuit(task)
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
self.delegateNotify(operation, error: error)
}
})
}
@@ -776,11 +661,9 @@ extension FTPFileProvider: FileProviderReadWrite {
}
}
return operation
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
@@ -794,9 +677,217 @@ 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: // modify, fetch
return nil
}
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 {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, 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(operation, error: error)
}
return
}
guard let response = response else {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, 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 operation {
case .create:
errorCode = URLError.cannotCreateFile
case .modify:
errorCode = URLError.cannotWriteToFile
case .copy:
self.fallbackCopy(operation, progress: progress, completionHandler: completionHandler)
return
case .move:
errorCode = URLError.cannotMoveFile
case .remove:
self.fallbackRemove(operation, 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(operation, error: error)
return
}
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
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
if let error = error {
progress.cancel()
self.dispatch_queue.async {
completionHandler?(error)
}
self.delegateNotify(operation, 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(operation, error: error)
return
}
if response.hasPrefix("50") {
self.fallbackRecursiveRemove(operation, 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(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(parent: progress, userInfo: nil)
recursiveProgress.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
})
})
}
}
+13 -17
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 {
@@ -399,8 +389,9 @@ internal extension FTPFileProvider {
}
}
func recursiveList(path: String, useMLST: Bool, foundItemsHandler: ((_ contents: [FileObject]) -> Void)? = nil, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
let queue = DispatchQueue(label: "test")
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 queue = DispatchQueue(label: "\(self.type).recursiveList")
queue.async {
let group = DispatchGroup()
var result = [FileObject]()
@@ -415,12 +406,14 @@ internal extension FTPFileProvider {
}
result.append(contentsOf: files)
progress.completedUnitCount = Int64(files.count)
foundItemsHandler?(files)
let directories: [FileObject] = files.filter { $0.isDirectory }
progress.becomeCurrent(withPendingUnitCount: Int64(directories.count))
for dir in directories {
group.enter()
self.recursiveList(path: dir.path, useMLST: useMLST, foundItemsHandler: foundItemsHandler, completionHandler: { (contents, error) in
_=self.recursiveList(path: dir.path, useMLST: useMLST, foundItemsHandler: foundItemsHandler, completionHandler: { (contents, error) in
success = success && (error == nil)
if let error = error {
completionHandler([], error)
@@ -430,9 +423,11 @@ internal extension FTPFileProvider {
foundItemsHandler?(files)
result.append(contentsOf: contents)
group.leave()
})
}
progress.resignCurrent()
group.leave()
})
group.wait()
@@ -443,12 +438,13 @@ internal extension FTPFileProvider {
}
}
}
return progress
}
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)
}
@@ -508,7 +504,7 @@ internal extension FTPFileProvider {
}
}
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)
@@ -546,7 +542,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)
@@ -613,7 +609,7 @@ internal extension FTPFileProvider {
}
}
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)
+1 -1
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: "/")!
}
}
+93 -81
View File
@@ -29,7 +29,8 @@ 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.
/// **DEPRECATED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method.
@available(*, deprecated, message: "This property is redundant with almost no use internally.")
var currentPath: String { get set }
/**
@@ -64,7 +65,7 @@ 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 path: path to target directory. If empty, root will be iterated.
- 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.
@@ -76,7 +77,7 @@ public protocol FileProviderBasic: class, NSSecureCoding {
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 completionHandler: a closure with result of directory entries or error.
- `attributes`: A `FileObject` containing the attributes of the item.
- `error`: Error returned by system.
@@ -99,7 +100,8 @@ public protocol FileProviderBasic: class, NSSecureCoding {
- 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.
*/
func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void))
@discardableResult
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.
@@ -121,8 +123,10 @@ 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.
*/
func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void))
@discardableResult
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.
@@ -130,7 +134,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
@@ -146,9 +150,9 @@ public protocol FileProviderBasic: class, NSSecureCoding {
}
extension FileProviderBasic {
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
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)
self.searchFiles(path: path, recursive: recursive, query: predicate, foundItemHandler: foundItemHandler, completionHandler: completionHandler)
return self.searchFiles(path: path, recursive: recursive, query: predicate, foundItemHandler: foundItemHandler, completionHandler: completionHandler)
}
/// The maximum number of queued operations that can execute at the same time.
@@ -162,8 +166,6 @@ extension FileProviderBasic {
operation_queue.maxConcurrentOperationCount = newValue
}
}
}
/// Checking equality of two file provider, regardless of current path queues and delegates.
@@ -241,7 +243,7 @@ internal extension FileProviderBasicRemote {
return false
}
func runDataTask(with request: URLRequest, operationHandle: RemoteOperationHandle? = nil, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) {
func runDataTask(with request: URLRequest, operation: FileOperationType? = nil, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) {
let useCache = self.useCache
let validatingCache = self.validatingCache
dispatch_queue.async {
@@ -251,8 +253,7 @@ internal extension FileProviderBasicRemote {
}
}
let task = self.session.dataTask(with: request, completionHandler: completionHandler)
task.taskDescription = operationHandle?.operationType.json
operationHandle?.add(task: task)
task.taskDescription = operation?.json
task.resume()
}
}
@@ -271,10 +272,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- folder: Directory name.
- at: Parent path of new directory.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func create(folder: String, at: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func create(folder: String, at: String, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Moves a file or directory from `path` to designated path asynchronously.
@@ -285,10 +286,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- path: original file or directory path.
- to: destination path of file or directory, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Moves a file or directory from `path` to designated path asynchronously.
@@ -300,10 +301,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- to: destination path of file or directory, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func moveItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func moveItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Copies a file or directory from `path` to designated path asynchronously.
@@ -314,10 +315,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- path: original file or directory path.
- to: destination path of file or directory, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Copies a file or directory from `path` to designated path asynchronously.
@@ -329,10 +330,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- to: destination path of file or directory, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func copyItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Removes the file or directory at the specified path.
@@ -340,11 +341,11 @@ public protocol FileProviderOperations: FileProviderBasic {
- Parameters:
- path: file or directory path.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Uploads a file from local file url to designated path asynchronously.
@@ -356,10 +357,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- localFile: a file url to file.
- to: destination path of file, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress.
- Returns: An `Progress` to get progress or cancel progress.
*/
@discardableResult
func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Uploads a file from local file url to designated path asynchronously.
@@ -372,10 +373,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- to: destination path of file, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress.
- Returns: An `Progress` to get progress or cancel progress.
*/
@discardableResult
func copyItem(localFile: URL, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func copyItem(localFile: URL, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Download a file from `path` to designated local file url asynchronously.
@@ -387,37 +388,55 @@ public protocol FileProviderOperations: FileProviderBasic {
- path: original file or directory path.
- toLocalURL: destination local url of file, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress?
}
public extension FileProviderOperations {
/// *DEPRECATED:* Use Use FileProviderReadWrite.writeContents(path:, data:, completionHandler:) method instead.
@available(*, deprecated, message: "Use FileProviderReadWrite.writeContents(path:, data:, completionHandler:) method instead.")
@discardableResult
public func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> Progress? {
let path = (at as NSString).appendingPathComponent(file)
return (self as? FileProviderReadWrite)?.writeContents(path: path, contents: data, completionHandler: completionHandler)
}
@discardableResult
public func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.moveItem(path: path, to: to, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
public func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.copyItem(localFile: localFile, to: to, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
public func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.copyItem(path: path, to: to, overwrite: false, completionHandler: completionHandler)
}
}
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 {
/**
@@ -429,10 +448,10 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- completionHandler: a closure with result of file contents or error.
- `contents`: contents of file in a `Data` object.
- `error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- 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)) -> OperationHandle?
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.
@@ -445,10 +464,10 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- completionHandler: a closure with result of file contents or error.
- `contents`: contents of file in a `Data` object.
- `error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- 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)) -> OperationHandle?
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.
@@ -459,10 +478,10 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- path: Path of target file.
- contents: Data to be written into file, pass nil to create empty file.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func writeContents(path: String, contents: Data?, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Write the contents of the `Data` to a location asynchronously.
@@ -473,10 +492,10 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- contents: Data to be written into file, pass nil to create empty file.
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data?, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func writeContents(path: String, contents: Data?, atomically: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Write the contents of the `Data` to a location asynchronously.
@@ -487,10 +506,10 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- contents: Data to be written into file, pass nil to create empty file.
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data?, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func writeContents(path: String, contents: Data?, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Write the contents of the `Data` to a location asynchronously.
@@ -501,30 +520,30 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func writeContents(path: String, contents: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
}
extension FileProviderReadWrite {
@discardableResult
public func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle?{
public func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
return self.contents(path: path, offset: 0, length: -1, completionHandler: completionHandler)
}
@discardableResult
public func writeContents(path: String, contents: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func writeContents(path: String, contents: Data?, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.writeContents(path: path, contents: contents, atomically: false, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
public func writeContents(path: String, contents: Data?, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func writeContents(path: String, contents: Data?, atomically: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.writeContents(path: path, contents: contents, atomically: atomically, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
public func writeContents(path: String, contents: Data?, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func writeContents(path: String, contents: Data?, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.writeContents(path: path, contents: contents, atomically: false, overwrite: overwrite, completionHandler: completionHandler)
}
}
@@ -570,7 +589,7 @@ public protocol FileProvideUndoable: FileProviderOperations {
var undoManager: UndoManager? { get set }
/// UndoManager supports undoing this file operation
func canUndo(handle: OperationHandle) -> Bool
func canUndo(handle: Progress) -> Bool
/// UndoManager supports undoing this operation
func canUndo(operation: FileOperationType) -> Bool
}
@@ -580,8 +599,11 @@ public extension FileProvideUndoable {
return undoOperation(for: operation) != nil
}
public func canUndo(handle: OperationHandle) -> Bool {
return canUndo(operation: handle.operationType)
public func canUndo(handle: Progress) -> Bool {
if let operationType = handle.userInfo[.fileProvderOperationTypeKey] as? FileOperationType {
return canUndo(operation: operationType)
}
return false
}
internal func undoOperation(for operation: FileOperationType) -> FileOperationType? {
@@ -644,9 +666,9 @@ 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)
@@ -692,8 +714,9 @@ extension FileProviderBasic {
}
/// 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.
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
@@ -729,18 +752,14 @@ extension FileProviderBasic {
return (dirPath as NSString).appendingPathComponent(finalFile)
}
internal func throwError(_ path: String, code: FoundationErrorEnum) -> NSError {
internal func throwError(_ 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 throwError(_ 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) {
@@ -953,10 +972,10 @@ public enum FileOperationType: CustomStringConvertible {
}
/// Path of subjecting file.
public var source: String? {
guard let reflect = Mirror(reflecting: self).children.first?.value else { return nil }
public var source: String {
let reflect = Mirror(reflecting: self).children.first!.value
let mirror = Mirror(reflecting: reflect)
return reflect as? String ?? mirror.children.first?.value as? String
return reflect as? String ?? mirror.children.first?.value as! String
}
/// Path of subjecting file.
@@ -1008,6 +1027,7 @@ 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 }
@@ -1028,14 +1048,6 @@ public protocol OperationHandle {
func cancel() -> Bool
}
public extension OperationHandle {
public var progress: Float {
let bytesSoFar = self.bytesSoFar
let totalBytes = self.totalBytes
return totalBytes > 0 ? Float(Double(bytesSoFar) / Double(totalBytes)) : Float.nan
}
}
/// Delegate methods for reporting provider's operation result and progress, when it's ready to update
/// user interface.
/// All methods are called in main thread to avoids UI bugs.
@@ -1045,7 +1057,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.
-277
View File
@@ -1,277 +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")
}
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")
}
mutating func set(dropboxArgKey requestDictionary: [String: AnyObject]) {
if let requestJson = String(jsonDictionary: requestDictionary) {
self.setValue(requestJson, 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 {}
+392
View File
@@ -0,0 +1,392 @@
//
// HTTPFileProvider.swift
// FilesProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2017 Mousavian. Distributed under MIT license.
//
import Foundation
/**
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.
No instance of this class should (and can) be created. Use derivated 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
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue {
willSet {
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
}
}
open weak var delegate: FileProviderDelegate?
open var credential: URLCredential? {
didSet {
sessionDelegate?.credential = self.credential
}
}
open private(set) var cache: URLCache?
public var useCache: Bool
public var validatingCache: Bool
fileprivate var _session: URLSession?
internal fileprivate(set) 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!)
}
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
}
self.sessionDelegate = newValue.delegate as? SessionDelegate
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
config.timeoutIntervalForRequest = 600
_longpollSession = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
}
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 = ""
self.useCache = false
self.validatingCache = true
self.cache = cache
self.credential = credential
#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) {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
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")
}
public static var supportsSecureCoding: Bool {
return true
}
open func copy(with zone: NSZone? = nil) -> Any {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
deinit {
if let sessionuuid = _session?.sessionDescription {
removeSessionHandler(for: sessionuuid)
}
if fileProviderCancelTasksOnInvalidating {
_session?.invalidateAndCancel()
} else {
_session?.finishTasksAndInvalidate()
}
}
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)) {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> 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? {
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)
}
}
open weak var fileOperationDelegate: FileOperationDelegate?
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)
}
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
}
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.copy(source: path, destination: toPath), 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.throwError(localFile.path, code: URLError.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)
return self.download_simple(path: path, request: request, operation: operation, completionHandler: { [weak self] (tempURL, error) in
if let error = error {
completionHandler?(error)
self?.delegateNotify(operation, error: error)
return
}
guard let tempURL = tempURL else {
completionHandler?(error)
self?.delegateNotify(operation, error: error)
return
}
do {
try FileManager.default.moveItem(at: tempURL, to: destURL)
completionHandler?(nil)
self?.delegateNotify(operation)
} catch let e {
completionHandler?(e)
self?.delegateNotify(operation, error: e)
}
})
}
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)
request.set(httpRangeWithOffset: 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)
}
})
}
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.")
}
internal func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
internal func multiStatusHandler(source: String, data: Data, completionHandler: SimpleCompletionHandler) -> Void {
// WebDAV will override this function
}
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let progress = Progress(totalUnitCount: 1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let request = self.request(for: operation)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderHTTPError?
if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
serverError = self.serverError(with: code, path: operation.source, data: data)
}
if let response = response as? HTTPURLResponse, FileProviderHTTPErrorCode(rawValue: response.statusCode) == .multiStatus, let data = data {
self.multiStatusHandler(source: operation.source, data: data, completionHandler: completionHandler)
}
if serverError == nil && error == nil {
progress.completedUnitCount = 1
} else {
progress.cancel()
}
completionHandler?(serverError ?? error)
self.delegateNotify(operation, error: serverError ?? error)
})
task.taskDescription = operation.json
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return progress
}
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
if let data = data {
task = session.uploadTask(with: request, from: data)
} else if let localFile = localFile {
task = session.uploadTask(with: request, fromFile: localFile)
} else {
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)
}
if !(responseError == nil && error == nil) {
progress.cancel()
}
completionHandler?(responseError ?? error)
self?.delegateNotify(operation, error: responseError ?? error)
}
task.taskDescription = operation.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return progress
}
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)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.downloadTask(with: request)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
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
if serverError != nil {
progress.cancel()
}
completionHandler(nil, serverError)
self.delegateNotify(operation)
return
}
completionHandler(tempURL, nil)
}
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
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return progress
}
}
extension HTTPFileProvider: FileProvider { }
+162 -105
View File
@@ -173,22 +173,32 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
completionHandler(totalSize, totalSize - freeSize)
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
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)
progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey)
dispatch_queue.async {
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
let iterator = self.fileManager.enumerator(at: self.url(of: path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in
completionHandler([], e)
return true
}
var result = [LocalFileObject]()
while let fileURL = iterator?.nextObject() as? URL {
if progress.isCancelled {
break
}
let path = self.relativePathOf(url: fileURL)
if let fileObject = LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), query.evaluate(with: fileObject.mapPredicate()) {
result.append(fileObject)
progress.completedUnitCount = Int64(result.count)
foundItemHandler?(fileObject)
}
}
completionHandler(result, nil)
}
return progress
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
@@ -200,59 +210,74 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
open weak var fileOperationDelegate : FileOperationDelegate?
@discardableResult
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
return self.doOperation(opType, completionHandler: completionHandler)
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
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) -> OperationHandle? {
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))
return nil
}
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.move(source: path, destination: toPath)
return self.doOperation(opType, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = 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.throwError(toPath, code: CocoaError.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) -> OperationHandle? {
let opType = FileOperationType.remove(path: path)
return self.doOperation(opType, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
self.dispatch_queue.async {
completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum))
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.copy(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.throwError(toPath, code: CocoaError.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, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
return self.doOperation(opType, completionHandler: completionHandler)
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
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? {
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.throwError(toPath, code: CocoaError.fileWriteFileExists)
dispatch_queue.async {
completionHandler?(e)
}
self.delegateNotify(operation, error: e)
return nil
}
return self.doOperation(operation, forUploading: true, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
return self.doOperation(operation, completionHandler: completionHandler)
}
@objc dynamic func doSimpleOperation(_ box: UndoBox) {
@@ -263,9 +288,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
@discardableResult
fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let localOperationHandle = LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
fileprivate func doOperation(_ operation: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let progress = Progress(parent: nil, userInfo: nil)
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)
@@ -276,9 +304,10 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
}
guard let sourcePath = opType.source else { return nil }
let destPath = opType.destination
let sourcePath = operation.source
let destPath = operation.destination
let source: URL = urlofpath(path: sourcePath)
progress.setUserInfoObject(source, forKey: .fileURLKey)
let dest: URL?
if let destPath = destPath {
@@ -287,11 +316,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()
}
@@ -299,23 +328,31 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
let operationHandler: (URL, URL?) -> Void = { source, dest in
do {
localOperationHandle.inProgress = true
switch opType {
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
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)
try data?.write(to: source, options: .atomic)
}
case .modify:
progress.totalUnitCount = Int64(data?.count ?? 0)
try data?.write(to: source, options: atomically ? [.atomic] : [])
case .copy:
guard let dest = dest else { return }
progress.setUserInfoObject(Progress.FileOperationKind.copying, forKey: .fileOperationKindKey)
progress.totalUnitCount = abs(source.fileSize)
try self.opFileManager.copyItem(at: source, to: dest)
case .move:
progress.setUserInfoObject(Progress.FileOperationKind.copying, forKey: .fileOperationKindKey)
guard let dest = dest else { return }
progress.totalUnitCount = abs(source.fileSize)
try self.opFileManager.moveItem(at: source, to: dest)
case.remove:
progress.totalUnitCount = abs(source.fileSize)
try self.opFileManager.removeItem(at: source)
default:
return
@@ -324,30 +361,27 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
source.stopAccessingSecurityScopedResource()
}
localOperationHandle.inProgress = false
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
completionHandler?(nil)
}
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
}
self.delegateNotify(operation)
} catch let e {
if successfulSecurityScopedResourceAccess {
source.stopAccessingSecurityScopedResource()
}
progress.cancel()
self.dispatch_queue.async {
completionHandler?(e)
}
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
self.delegateNotify(operation, error: e)
}
}
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:
@@ -363,52 +397,58 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
default:
return nil
}
self.coordinated(intents: intents, completionHandler: operationHandler, errorHandler: { error in
self.coordinated(intents: intents, 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 {
operationHandler(source, dest)
}
}
return localOperationHandle
return progress
}
@discardableResult
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
let opType = FileOperationType.fetch(path: path)
let localOperationHandle = LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
let operation = FileOperationType.fetch(path: path)
let url = self.url(of: path)
let progress = Progress(parent: nil, userInfo: nil)
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)
do {
localOperationHandle.inProgress = true
let data = try Data(contentsOf: url)
localOperationHandle.inProgress = false
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
completionHandler(data, nil)
}
self.delegateNotify(operation)
} catch let e {
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, e)
}
self.delegateNotify(operation, error: e)
}
}
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 {
@@ -416,11 +456,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
}
return localOperationHandle
return progress
}
@discardableResult
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
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)
@@ -432,14 +472,22 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
return self.contents(path: path, completionHandler: completionHandler)
}
let opType = FileOperationType.fetch(path: path)
let localOperationHandle = LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
let operation = FileOperationType.fetch(path: path)
let url = self.url(of: path)
let progress = Progress(parent: nil, userInfo: nil)
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))
let e = self.throwError(path, code: CocoaError.fileNoSuchFile)
completionHandler(nil, e)
self.delegateNotify(operation, error: e)
}
return
}
@@ -448,38 +496,42 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
handle.closeFile()
}
localOperationHandle.inProgress = true
let size = LocalFileObject(fileWithURL: url)?.size ?? -1
progress.totalUnitCount = size
guard size > offset else {
localOperationHandle.inProgress = false
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
let e = self.throwError(path, code: CocoaError.fileReadTooLarge)
completionHandler(nil, e)
self.delegateNotify(operation, error: e)
}
return
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
handle.seek(toFileOffset: UInt64(offset))
guard Int64(handle.offsetInFile) == offset else {
localOperationHandle.inProgress = false
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
let e = self.throwError(path, code: CocoaError.fileReadTooLarge)
completionHandler(nil, e)
self.delegateNotify(operation, error: e)
}
return
}
let data = handle.readData(ofLength: length)
localOperationHandle.inProgress = false
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
completionHandler(data, nil)
self.delegateNotify(operation)
}
}
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 {
@@ -487,14 +539,24 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
}
return localOperationHandle
return progress
}
@discardableResult
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
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)
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let fileExists = ((try? self.url(of: path).checkResourceIsReachable()) ?? false) ||
((try? self.url(of: path).checkPromisedItemIsReachable()) ?? false)
if !overwrite && fileExists {
let e = self.throwError(path, code: CocoaError.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]()
@@ -527,9 +589,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
@@ -541,19 +601,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))
}
self.delegateNotify(operation)
} catch let e {
completionHandler?(e)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: .link(link: path, target: destPath))
}
self.delegateNotify(operation, error: e)
}
}
}
@@ -563,7 +620,7 @@ 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)
@@ -577,18 +634,18 @@ public extension LocalFileProvider {
}
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 {
@@ -600,7 +657,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)
}
+1 -99
View File
@@ -19,7 +19,7 @@ public final class LocalFileObject: FileObject {
var fileURL: URL?
var rpath = path.replacingOccurrences(of: relativeURL?.path ?? "", with: "", options: .anchored)
if relativeURL != nil && rpath.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
_=rpath.characters.removeFirst()
}
if #available(iOS 9.0, macOS 10.11, tvOS 9.0, *) {
fileURL = URL(fileURLWithPath: rpath, relativeTo: relativeURL)
@@ -217,104 +217,6 @@ internal class LocalFileProviderManagerDelegate: NSObject, FileManagerDelegate {
}
}
/// - Note: Local operation handling is limited. Please don't use as much as possible.
open class LocalOperationHandle: OperationHandle {
/// Url of file which operation is doing on
public let baseURL: URL
/// Type of operation
public let operationType: FileOperationType
init (operationType: FileOperationType, baseURL: URL?) {
self.baseURL = baseURL ?? URL(fileURLWithPath: "/")
self.operationType = operationType
inProgress = false
}
private var sourceURL: URL? {
guard let source = operationType.source else { return nil }
return source.hasPrefix("file://") ? URL(fileURLWithPath: source) : baseURL.appendingPathComponent(source)
}
private var destURL: URL? {
guard let dest = operationType.destination else { return nil }
return dest.hasPrefix("file://") ? URL(fileURLWithPath: dest) : baseURL.appendingPathComponent(dest)
}
/// Caution: may put pressure on CPU, may have latency
open var bytesSoFar: Int64 {
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
switch operationType {
case .modify:
guard let url = sourceURL, url.isFileURL else { return 0 }
if url.fileIsDirectory {
return iterateDirectory(url, deep: true).totalsize
} else {
return url.fileSize
}
case .copy, .move:
guard let url = destURL, url.isFileURL else { return 0 }
if url.fileIsDirectory {
return iterateDirectory(url, deep: true).totalsize
} else {
return url.fileSize
}
default:
return 0
}
}
/// Caution: may put pressure on CPU, may have latency
open var totalBytes: Int64 {
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
switch operationType {
case .copy, .move:
guard let url = sourceURL, url.isFileURL else { return 0 }
if url.fileIsDirectory {
return iterateDirectory(url, deep: true).totalsize
} else {
return url.fileSize
}
default:
return 0
}
}
/// Not usable in local provider
open var inProgress: Bool
/// Not usable in local provider
open func cancel() -> Bool{
return false
}
func iterateDirectory(_ pathURL: URL, deep: Bool) -> (folders: Int, files: Int, totalsize: Int64) {
var folders = 0
var files = 0
var totalsize: Int64 = 0
let keys: [URLResourceKey] = [.isDirectoryKey, .fileSizeKey]
let enumOpt: FileManager.DirectoryEnumerationOptions = !deep ? [.skipsSubdirectoryDescendants, .skipsPackageDescendants] : []
let fp = FileManager()
let filesList = fp.enumerator(at: pathURL, includingPropertiesForKeys: keys, options: enumOpt, errorHandler: nil)
while let fileURL = filesList?.nextObject() as? URL {
guard let values = try? fileURL.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey]) else { continue }
let isdir = values.isDirectory ?? false
let size = Int64(values.fileSize ?? 0)
if isdir {
folders += 1
} else {
files += 1
}
totalsize += size
}
return (folders, files, totalsize)
}
}
class UndoBox: NSObject {
weak var provider: FileProvideUndoable?
let operation: FileOperationType
+87 -261
View File
@@ -17,59 +17,10 @@ import CoreGraphics
- Note: Uploading files and data are limited to 100MB, for now.
*/
open class OneDriveFileProvider: FileProviderBasicRemote {
open class var type: String { return "OneDrive" }
open let baseURL: URL?
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
/// Generated storage url from server url and drive name
open var currentPath: String
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue {
willSet {
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
}
}
open weak var delegate: FileProviderDelegate?
open var credential: URLCredential? {
didSet {
sessionDelegate?.credential = self.credential
}
}
open private(set) var cache: URLCache?
public var useCache: Bool
public var validatingCache: Bool
fileprivate var _session: URLSession?
fileprivate var sessionDelegate: SessionDelegate?
public var session: URLSession {
get {
if _session == nil {
self.sessionDelegate = SessionDelegate(fileProvider: self)
let queue = OperationQueue()
//queue.underlyingQueue = dispatch_queue
let config = URLSessionConfiguration.default
config.urlCache = cache
config.requestCachePolicy = .returnCacheDataElseLoad
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: queue)
_session?.sessionDescription = UUID().uuidString
initEmptySessionHandler(_session!.sessionDescription!)
}
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
}
self.sessionDelegate = newValue.delegate as? SessionDelegate
initEmptySessionHandler(_session!.sessionDescription!)
}
}
/**
Initializer for Onedrive provider with given client ID and Token.
@@ -87,22 +38,9 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
*/
public init(credential: URLCredential?, serverURL: URL? = nil, drive: String = "root", cache: URLCache? = nil) {
let baseURL = serverURL?.absoluteURL ?? URL(string: "https://api.onedrive.com/")!
self.baseURL = baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
let refinedBaseURL = baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
self.drive = drive
self.currentPath = ""
self.useCache = false
self.validatingCache = true
self.cache = cache
self.credential = credential
#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"
super.init(baseURL: refinedBaseURL, credential: credential, cache: cache)
}
public required convenience init?(coder aDecoder: NSCoder) {
@@ -114,20 +52,12 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
}
open func encode(with aCoder: NSCoder) {
aCoder.encode(self.credential, forKey: "credential")
aCoder.encode(self.baseURL, forKey: "baseURL")
open override func encode(with aCoder: NSCoder) {
super.encode(with: aCoder)
aCoder.encode(self.drive, forKey: "drive")
aCoder.encode(self.currentPath, forKey: "currentPath")
aCoder.encode(self.useCache, forKey: "useCache")
aCoder.encode(self.validatingCache, forKey: "validatingCache")
}
public static var supportsSecureCoding: Bool {
return true
}
open func copy(with zone: NSZone? = nil) -> Any {
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
copy.delegate = self.delegate
@@ -137,25 +67,13 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
return copy
}
deinit {
if let sessionuuid = _session?.sessionDescription {
removeSessionHandler(for: sessionuuid)
}
if fileProviderCancelTasksOnInvalidating {
_session?.invalidateAndCancel()
} else {
_session?.finishTasksAndInvalidate()
}
}
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
list(path) { (contents, cursor, error) in
completionHandler(contents, error)
}
}
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
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)
@@ -174,8 +92,8 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
task.resume()
}
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
var request = URLRequest(url: url())
open override func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> 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
@@ -190,12 +108,14 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
task.resume()
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
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 }
search(path, query: finalQueryStr, foundItem: { (file) in
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, recursive: recursive, progress: progress, foundItem: { (file) in
if query.evaluate(with: file.mapPredicate()) {
foundFiles.append(file)
foundItemHandler?(file)
@@ -203,36 +123,34 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
}, completionHandler: { (error) in
completionHandler(foundFiles, error)
})
return progress
}
open func url(of path: String? = nil, modifier: String? = nil) -> URL {
var rpath: String
if let path = path {
rpath = path
} else {
rpath = self.currentPath
}
open func url(of path: String, modifier: String? = nil) -> URL {
var rpath: String = path
let driveURL = baseURL!.appendingPathComponent("drive/\(drive):/")
if rpath.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
_=rpath.characters.removeFirst()
}
if rpath.isEmpty {
if let modifier = modifier {
return baseURL!.appendingPathComponent("drive/\(drive)/\(modifier)")
return driveURL.appendingPathComponent(modifier)
}
return baseURL!.appendingPathComponent("drive/\(drive)")
return driveURL
}
let driveURL = baseURL!.appendingPathComponent("drive/\(drive):/")
rpath = (rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath)
rpath = rpath.trimmingCharacters(in: pathTrimSet)
if let modifier = modifier {
rpath = rpath + ":/" + modifier
}
return URL(string: rpath, relativeTo: driveURL) ?? driveURL
return driveURL.appendingPathComponent(rpath)
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
var request = URLRequest(url: url())
open override func isReachable(completionHandler: @escaping (Bool) -> Void) {
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
@@ -242,163 +160,75 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
task.resume()
}
open weak var fileOperationDelegate: FileOperationDelegate?
}
extension OneDriveFileProvider: FileProviderOperations {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
return doOperation(.create(path: path), completionHandler: completionHandler)
}
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
}
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
}
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.remove(path: path), completionHandler: completionHandler)
}
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
guard let sourcePath = operation.source else { return nil }
let destPath = operation.destination
var request = URLRequest(url: url(of: sourcePath))
override func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey : Any] = [:]) -> URLRequest {
let method: String
let url: URL
switch operation {
case .create:
request.httpMethod = "CREATE"
case .copy:
request.httpMethod = "POST"
case .move:
request.httpMethod = "PATCH"
case .remove:
request.httpMethod = "DELETE"
default: // modify, link, fetch
return nil
case .fetch(path: let path):
method = "GET"
url = self.url(of: path, modifier: "content")
case .modify(path: let path):
method = "PUT"
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
url = self.url(of: path, modifier: "content\(queryStr)")
case .create(path: let path):
method = "CREATE"
url = self.url(of: path)
case .copy(let source, let dest) where !source.hasPrefix("file://") && !dest.hasPrefix("file://"):
method = "POST"
url = self.url(of: source)
case .copy(let source, let dest) where source.hasPrefix("file://"):
method = "PUT"
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
url = self.url(of: dest, modifier: "content\(queryStr)")
case .copy(let source, let dest) where dest.hasPrefix("file://"):
method = "GET"
url = self.url(of: source, modifier: "content")
case .move(source: let source, destination: _):
method = "PATCH"
url = self.url(of: source)
case .remove(path: let path):
method = "DELETE"
url = self.url(of: path)
default: // link
fatalError("Unimplemented operation \(operation.description) in \(#file)")
}
var request = URLRequest(url: url)
request.httpMethod = method
request.set(httpAuthentication: credential, with: .oAuth2)
var requestDictionary = [String: AnyObject]()
if let dest = correctPath(destPath) as NSString? {
request.set(contentType: .json)
requestDictionary["parentReference"] = ("/drive/\(drive):" + dest.deletingLastPathComponent) as NSString
requestDictionary["name"] = dest.lastPathComponent as NSString
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 requestDictionary = [String: AnyObject]()
requestDictionary["parentReference"] = ("/drive/\(drive):" + cdest.deletingLastPathComponent) as NSString
requestDictionary["name"] = cdest.lastPathComponent as NSString
request.httpBody = Data(jsonDictionary: requestDictionary)
default:
break
}
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderOneDriveError?
if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
serverError = FileProviderOneDriveError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
completionHandler?(serverError ?? error)
self.delegateNotify(operation, error: serverError ?? error)
})
task.taskDescription = operation.json
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
return request
}
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
// 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))
}
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) }))
}
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
}
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
return super.upload_simple(targetPath, request: request, data: data, localFile: localFile, operation: operation, completionHandler: completionHandler)
}
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
var request = URLRequest(url: self.url(of: path, modifier: "content"))
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.downloadTask(with: request)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = completionHandler
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 : FileProviderOneDriveError? = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
completionHandler?(serverError)
return
}
do {
try FileManager.default.moveItem(at: tempURL, to: destURL)
completionHandler?(nil)
} catch let e {
completionHandler?(e)
}
}
task.taskDescription = opType.json
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
}
}
extension OneDriveFileProvider: FileProviderReadWrite {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
}
return nil
}
let opType = FileOperationType.fetch(path: path)
var request = URLRequest(url: self.url(of: path, modifier: "content"))
request.httpMethod = "GET"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(rangeWithOffset: offset, length: length)
let task = session.downloadTask(with: request)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
completionHandler(nil, 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 : FileProviderOneDriveError? = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
completionHandler(nil, serverError)
return
}
do {
let data = try Data(contentsOf: tempURL)
completionHandler(data, nil)
} catch let e {
completionHandler(nil, e)
}
}
task.taskDescription = opType.json
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
}
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
// FIXME: remove 150MB restriction
return upload_simple(path, data: data ?? Data(), overwrite: overwrite, operation: opType, completionHandler: completionHandler)
}
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
@@ -411,9 +241,7 @@ extension OneDriveFileProvider: FileProviderReadWrite {
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"
@@ -503,5 +331,3 @@ extension OneDriveFileProvider: ExtendedFileProvider {
task.resume()
}
}
extension OneDriveFileProvider: FileProvider { }
+18 -55
View File
@@ -18,12 +18,13 @@ 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 = path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? path
var rpath = (URL(string:path)?.appendingPathComponent(name).absoluteString)!
if rpath.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
_=rpath.characters.removeFirst()
}
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: rpath)!
super.init(url: url, name: name, path: path)
super.init(url: url, name: name, path: rpath.removingPercentEncoding ?? path)
}
internal convenience init? (baseURL: URL?, drive: String, jsonStr: String) {
@@ -34,14 +35,14 @@ public final class OneDriveFileObject: FileObject {
internal convenience init? (baseURL: URL?, drive: String, json: [String: AnyObject]) {
guard let name = json["name"] as? String else { return nil }
guard let path = (json["parentReference"] as? NSDictionary)?["path"] as? String else { return nil }
var lPath = path.replacingOccurrences(of: "/drive/\(drive)", with: "/", options: .anchored, range: nil)
var lPath = path.replacingOccurrences(of: "/drive/\(drive):", 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.type = (json["folder"] as? String) != nil ? .directory : .regular
self.type = json["folder"] != nil ? .directory : .regular
self.id = json["id"] as? String
self.entryTag = json["eTag"] as? String
}
@@ -113,47 +114,15 @@ internal extension OneDriveFileProvider {
task.resume()
}
func upload_simple(_ targetPath: String, data: Data? = nil , localFile: URL? = nil, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let size = data?.count ?? (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(.create(path: targetPath), error: error)
return nil
}
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
let url = self.url(of: targetPath, modifier: "content\(queryStr)")
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .stream)
let task: URLSessionUploadTask
if let data = data {
task = session.uploadTask(with: request, from: data)
} else if let localFile = localFile {
task = session.uploadTask(with: request, fromFile: localFile)
} else {
return nil
func search(_ startPath: String = "", query: String, recursive: Bool, next: URL? = nil, progress: Progress, foundItem: @escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
if progress.isCancelled {
return
}
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
var responseError: FileProviderOneDriveError?
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
// We can't fetch server result from delegate!
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: nil)
}
completionHandler?(responseError ?? error)
self?.delegateNotify(.create(path: targetPath), error: responseError ?? error)
}
task.taskDescription = operation.json
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
}
func search(_ startPath: String = "", query: String, next: URL? = nil, foundItem:@escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
let url: URL
let q = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
url = next ?? self.url(of: startPath, modifier: "view.search?q=\(q)")
let expanded = recursive ? "&expand=children" : ""
url = next ?? self.url(of: startPath, modifier: "view.search?q=\(q)\(expanded)")
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.set(httpAuthentication: credential, with: .oAuth2)
@@ -171,8 +140,8 @@ internal extension OneDriveFileProvider {
}
}
let next: URL? = (json["@odata.nextLink"] as? String).flatMap { URL(string: $0) }
if let next = next {
self.search(startPath, query: query, next: next, foundItem: foundItem, completionHandler: completionHandler)
if !progress.isCancelled, let next = next {
self.search(startPath, query: query, recursive: recursive, next: next, progress: progress, foundItem: foundItem, completionHandler: completionHandler)
} else {
completionHandler(responseError ?? error)
}
@@ -180,7 +149,11 @@ internal extension OneDriveFileProvider {
}
}
completionHandler(responseError ?? error)
})
})
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
}
}
@@ -258,14 +231,4 @@ internal extension OneDriveFileProvider {
return (dic, keys)
}
func delegateNotify(_ operation: FileOperationType, error: Error?) {
DispatchQueue.main.async(execute: {
if error == nil {
self.delegate?.fileproviderSucceed(self, operation: operation)
} else {
self.delegate?.fileproviderFailed(self, operation: operation)
}
})
}
}
Regular → Executable
+52 -83
View File
@@ -8,67 +8,6 @@
import Foundation
/// Allows to get progress or cancel an in-progress operation, for remote, `URLSession` based providers.
/// This class keeps strong reference to tasks.
open class RemoteOperationHandle: OperationHandle {
internal var tasks: [URLSessionTask]
open private(set) var operationType: FileOperationType
init(operationType: FileOperationType, tasks: [URLSessionTask]) {
self.operationType = operationType
self.tasks = tasks
}
internal func add(task: URLSessionTask) {
tasks.append(task)
}
internal func reape() {
self.tasks = tasks.filter { $0.state != .completed }
}
open var bytesSoFar: Int64 {
return tasks.reduce(0) {
switch $1 {
case let task as URLSessionUploadTask:
return $0 + task.countOfBytesSent
case let task as FileProviderStreamTask:
return $0 + task.countOfBytesSent + task.countOfBytesReceived
default:
return $0 + $1.countOfBytesReceived
}
}
}
open var totalBytes: Int64 {
return tasks.reduce(0) {
switch $1 {
case let task as URLSessionUploadTask:
return $0 + task.countOfBytesExpectedToSend
case let task as FileProviderStreamTask:
return $0 + task.countOfBytesExpectedToSend + task.countOfBytesExpectedToReceive
default:
return $0 + $1.countOfBytesExpectedToReceive
}
}
}
open func cancel() -> Bool {
var canceled = false
for taskbox in tasks {
taskbox.cancel()
canceled = true
}
return canceled
}
open var inProgress: Bool {
return tasks.reduce(false) { $0 || $1.state == .running }
}
}
/// A protocol defines properties for errors returned by HTTP/S based providers.
/// Including Dropbox, OneDrive and WebDAV.
public protocol FileProviderHTTPError: Error, CustomStringConvertible {
@@ -92,18 +31,6 @@ 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]]()
@@ -140,8 +67,51 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession
self.credential = fileProvider.credential
}
open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let progress = context?.load(as: Progress.self), let newVal = change?[.newKey] as? Int64 {
switch keyPath ?? "" {
case #keyPath(URLSessionTask.countOfBytesReceived):
progress.completedUnitCount = newVal
if let startTime = progress.userInfo[ProgressUserInfoKey.startingTimeKey] as? Date, let task = object as? URLSessionTask {
let elapsed = Date().timeIntervalSince(startTime)
let throughput = Double(newVal) / elapsed
progress.setUserInfoObject(NSNumber(value: throughput), forKey: .throughputKey)
if task.countOfBytesExpectedToReceive > 0 {
let remain = task.countOfBytesExpectedToReceive - task.countOfBytesReceived
let estimatedTimeRemaining = Double(remain) / elapsed
progress.setUserInfoObject(NSNumber(value: estimatedTimeRemaining), forKey: .estimatedTimeRemainingKey)
}
}
case #keyPath(URLSessionTask.countOfBytesSent):
progress.completedUnitCount = newVal
if let startTime = progress.userInfo[ProgressUserInfoKey.startingTimeKey] as? Date, let task = object as? URLSessionTask {
let elapsed = Date().timeIntervalSince(startTime)
let throughput = Double(newVal) / elapsed
progress.setUserInfoObject(NSNumber(value: throughput), forKey: .throughputKey)
if task.countOfBytesExpectedToSend > 0 {
let remain = task.countOfBytesExpectedToSend - task.countOfBytesSent
let estimatedTimeRemaining = Double(remain) / elapsed
progress.setUserInfoObject(NSNumber(value: estimatedTimeRemaining), forKey: .estimatedTimeRemainingKey)
}
}
case #keyPath(URLSessionTask.countOfBytesExpectedToReceive), #keyPath(URLSessionTask.countOfBytesExpectedToSend):
progress.totalUnitCount = newVal
default:
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}
// 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))
}
if !(error == nil && task is URLSessionDownloadTask) {
let completionHandler = completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] ?? nil
completionHandler?(error)
@@ -163,8 +133,8 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession
}
DispatchQueue.main.async {
if error != nil {
fileProvider.delegate?.fileproviderFailed(fileProvider, operation: op)
if let error = error {
fileProvider.delegate?.fileproviderFailed(fileProvider, operation: op, error: error)
} else {
fileProvider.delegate?.fileproviderSucceed(fileProvider, operation: op)
}
@@ -197,30 +167,29 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession
switch op {
case .create(path: let path):
if path.hasSuffix("/") { return }
break
case .modify:
break
case .copy(source: let source, destination: _) where source.hasPrefix("file://"):
break
default:
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(),
let op = FileOperationType(json: json), let fileProvider = fileProvider else {
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) {
+11 -10
View File
@@ -74,53 +74,54 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
open weak var fileOperationDelegate: FileOperationDelegate?
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
NotImplemented()
return nil
}
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
NotImplemented()
return nil
}
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) {
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) -> Progress? {
NotImplemented()
return nil
}
open func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
+116 -381
View File
@@ -20,57 +20,9 @@ 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: FileProviderBasicRemote {
open class var type: String { return "WebDAV" }
open let baseURL: URL?
open var currentPath: String
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue {
willSet {
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
}
}
public weak var delegate: FileProviderDelegate?
public var credentialType: HTTPAuthenticationType = .digest
open var credential: URLCredential? {
didSet {
sessionDelegate?.credential = credential
}
}
open private(set) var cache: URLCache?
public var useCache: Bool
public var validatingCache: Bool
fileprivate var _session: URLSession?
fileprivate var sessionDelegate: SessionDelegate?
public var session: URLSession {
get {
if _session == nil {
self.sessionDelegate = SessionDelegate(fileProvider: self)
let queue = OperationQueue()
//queue.underlyingQueue = dispatch_queue
let config = URLSessionConfiguration.default
config.urlCache = cache
config.requestCachePolicy = .returnCacheDataElseLoad
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDownloadDelegate?, delegateQueue: queue)
_session?.sessionDescription = UUID().uuidString
initEmptySessionHandler(_session!.sessionDescription!)
}
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
}
self.sessionDelegate = newValue.delegate as? SessionDelegate
initEmptySessionHandler(_session!.sessionDescription!)
}
}
open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing {
override open class var type: String { return "WebDAV" }
public var credentialType: URLRequest.AuthenticationType = .digest
/**
Initializes WebDAV provider.
@@ -84,21 +36,8 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
if !["http", "https"].contains(baseURL.uw_scheme.lowercased()) {
return nil
}
self.baseURL = (baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")).absoluteURL
self.currentPath = ""
self.useCache = false
self.validatingCache = true
self.cache = cache
self.credential = credential
#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"
let refinedBaseURL = (baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")).absoluteURL
super.init(baseURL: refinedBaseURL, credential: credential, cache: cache)
}
public required convenience init?(coder aDecoder: NSCoder) {
@@ -112,19 +51,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
}
open 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")
}
public static var supportsSecureCoding: Bool {
return true
}
open func copy(with zone: NSZone? = nil) -> Any {
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
@@ -134,19 +61,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
return copy
}
deinit {
if let sessionuuid = _session?.sessionDescription {
removeSessionHandler(for: sessionuuid)
}
if fileProviderCancelTasksOnInvalidating {
_session?.invalidateAndCancel()
} else {
_session?.finishTasksAndInvalidate()
}
}
open func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) {
override open func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) {
self.contentsOfDirectory(path: path, including: [], completionHandler: completionHandler)
}
@@ -155,23 +70,23 @@ open class WebDAVFileProvider: 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 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.
*/
open func contentsOfDirectory(path: String, including: [URLResourceKey], completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
let opType = FileOperationType.fetch(path: path)
let operation = 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.set(httpContentType: .xml, charset: .utf8)
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, operationHandle: RemoteOperationHandle(operationType: opType, tasks: []), completionHandler: { (data, response, error) in
runDataTask(with: request, operation: operation, 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)
@@ -190,7 +105,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
})
}
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)
}
@@ -199,7 +114,7 @@ open class WebDAVFileProvider: 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 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.
@@ -209,9 +124,9 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
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.set(httpContentType: .xml, charset: .utf8)
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, completionHandler: { (data, response, error) in
@@ -230,7 +145,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
})
}
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
override open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
// Not all WebDAV clients implements RFC2518 which allows geting storage quota.
// In this case you won't get error. totalSize is NSURLSessionTransferSizeUnknown
// and used space is zero.
@@ -241,7 +156,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
request.httpMethod = "PROPFIND"
request.setValue("0", forHTTPHeaderField: "Depth")
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
request.set(httpContentType: .xml, charset: .utf8)
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")
runDataTask(with: request, completionHandler: { (data, response, error) in
@@ -258,15 +173,18 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
})
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
override open func searchFiles(path: String, recursive: Bool, query: NSPredicate, 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.set(httpContentType: .xml, charset: .utf8)
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:allprop/></D:propfind>".data(using: .utf8)
runDataTask(with: request, completionHandler: { (data, response, error) in
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(url, forKey: .fileURLKey)
let task = session.dataTask(with: request) { (data, response, error) in
// FIXME: paginating results
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
@@ -282,21 +200,28 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
}
fileObjects.append(fileObject)
progress.completedUnitCount = Int64(fileObjects.count)
foundItemHandler?(fileObject)
}
completionHandler(fileObjects, responseError ?? error)
return
}
completionHandler([], responseError ?? error)
})
}
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return progress
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
override open func isReachable(completionHandler: @escaping (Bool) -> Void) {
var request = URLRequest(url: baseURL!)
request.httpMethod = "PROPFIND"
request.setValue("0", forHTTPHeaderField: "Depth")
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
request.set(httpContentType: .xml, charset: .utf8)
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")
runDataTask(with: request, completionHandler: { (data, response, error) in
@@ -305,243 +230,101 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
})
}
open weak var fileOperationDelegate: FileOperationDelegate?
}
extension WebDAVFileProvider: FileProviderOperations {
@discardableResult
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = self.url(of: atPath).appendingPathComponent(folderName, isDirectory: true)
var request = URLRequest(url: url)
request.httpMethod = "MKCOL"
request.set(httpAuthentication: credential, with: credentialType)
let task = session.dataTask(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: url.relativePath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
}
completionHandler?(responseError ?? error)
self.delegateNotify(opType, error: responseError ?? error)
})
task.taskDescription = opType.json
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
}
@discardableResult
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.move(source: path, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
return self.doOperation(operation: opType, overwrite: overwrite, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
return self.doOperation(operation: opType, overwrite: overwrite, completionHandler: completionHandler)
}
@discardableResult
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.remove(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
return self.doOperation(operation: opType, completionHandler: completionHandler)
}
fileprivate func doOperation(operation opType: FileOperationType, overwrite: Bool? = nil, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let source = opType.source!
let sourceURL = self.url(of: source)
var request = URLRequest(url: sourceURL)
if let dest = opType.destination {
request.setValue(url(of:dest).absoluteString, forHTTPHeaderField: "Destination")
}
switch opType {
case .copy:
request.httpMethod = "COPY"
case .move:
request.httpMethod = "MOVE"
case .remove:
request.httpMethod = "DELETE"
default:
return nil
}
request.set(httpAuthentication: credential, with: credentialType)
if let overwrite = overwrite, !overwrite {
request.setValue("F", forHTTPHeaderField: "Overwrite")
}
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderWebDavError?
if let response = response as? HTTPURLResponse, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
if response.statusCode >= 300 {
responseError = FileProviderWebDavError(code: code, path: source, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: sourceURL)
}
if code == .multiStatus, let data = data {
let xresponses = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
for xresponse in xresponses where (xresponse.status ?? 0) >= 300 {
let error = FileProviderWebDavError(code: code, path: source, errorDescription: String(data: data, encoding: .utf8), url: sourceURL)
completionHandler?(error)
}
}
}
if (response as? HTTPURLResponse)?.statusCode ?? 0 != FileProviderHTTPErrorCode.multiStatus.rawValue {
completionHandler?(responseError ?? error)
}
self.delegateNotify(opType, error: responseError ?? error)
})
task.taskDescription = opType.json
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
}
@discardableResult
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
// check file is not a folder
guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else {
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?(self.throwError(localFile.path, code: URLError.fileIsDirectory))
completionHandler(nil, nil, nil, self.throwError(path, code: URLError.resourceUnavailable))
}
return nil
return
}
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = self.url(of:toPath)
var request = URLRequest(url: url)
if !overwrite {
request.setValue("F", forHTTPHeaderField: "Overwrite")
}
request.httpMethod = "PUT"
request.set(httpAuthentication: credential, with: credentialType)
let task = session.uploadTask(with: request, fromFile: localFile)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
var responseError: FileProviderWebDavError?
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
// We can't fetch server result from delegate!
responseError = FileProviderWebDavError(code: rCode, path: toPath, errorDescription: nil, url: url)
}
completionHandler?(responseError ?? error)
self?.delegateNotify(.create(path: toPath), error: responseError ?? error)
}
task.taskDescription = opType.json
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
}
@discardableResult
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = self.url(of:path)
var request = URLRequest(url: url)
request.set(httpAuthentication: credential, with: credentialType)
let task = session.downloadTask(with: request)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = completionHandler
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 serverError : FileProviderWebDavError? = code != nil ? FileProviderWebDavError(code: code!, path: path, errorDescription: code?.description, url: url) : nil
completionHandler?(serverError)
return
}
do {
try FileManager.default.moveItem(at: tempURL, to: destURL)
completionHandler?(nil)
} catch let e {
completionHandler?(e)
}
}
task.taskDescription = opType.json
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
}
}
extension WebDAVFileProvider: FileProviderReadWrite {
@discardableResult
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
}
return nil
}
let opType = FileOperationType.fetch(path: path)
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PROPPATCH"
request.set(httpAuthentication: credential, with: credentialType)
request.set(rangeWithOffset: offset, length: length)
let task = session.downloadTask(with: request)
let handle = RemoteOperationHandle(operationType: opType, tasks: [task])
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
completionHandler(nil, 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 serverError : FileProviderWebDavError? = code != nil ? FileProviderWebDavError(code: code!, path: path, errorDescription: code?.description, url: url) : nil
completionHandler(nil, serverError)
return
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)
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)
}
do {
let data = try Data(contentsOf: tempURL)
self.dispatch_queue.async {
completionHandler(data, nil)
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
}
} catch let e {
completionHandler(nil, e)
}
}
task.taskDescription = opType.json
task.resume()
return handle
completionHandler(nil, nil, nil, responseError ?? error)
})
}
@discardableResult
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
// FIXME: lock destination before writing process
let url = atomically ? self.url(of: path).appendingPathExtension("tmp") : self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.set(httpAuthentication: credential, with: credentialType)
if !overwrite {
request.setValue("F", forHTTPHeaderField: "Overwrite")
}
let task = session.uploadTask(with: request, from: data ?? Data())
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
var responseError: FileProviderWebDavError?
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
// We can't fetch server result from delegate!
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: nil, url: url)
override func request(for operation: FileOperationType, overwrite: Bool = true, attributes: [URLResourceKey: Any] = [:]) -> URLRequest {
let method: String
let url: URL
let sourceURL = self.url(of: operation.source)
switch operation {
case .fetch:
method = "GET"
url = sourceURL
case .create:
if sourceURL.absoluteString.hasSuffix("/") {
method = "MKCOL"
url = sourceURL
} else {
fallthrough
}
completionHandler?(responseError ?? error)
self?.delegateNotify(opType, error: responseError ?? error)
case .modify:
method = "PUT"
url = sourceURL
break
case .copy(let source, let dest):
if source.hasPrefix("file://") {
method = "PUT"
url = self.url(of: dest)
} else if dest.hasPrefix("file://") {
method = "GET"
url = sourceURL
} else {
method = "COPY"
url = sourceURL
}
case .move:
method = "MOVE"
url = sourceURL
case .remove:
method = "DELETE"
url = sourceURL
default:
fatalError("Unimplemented operation \(operation.description) in \(#file)")
}
var request = URLRequest(url: url)
request.httpMethod = method
request.set(httpAuthentication: credential, with: credentialType)
request.setValue(overwrite ? "T" : "F", forHTTPHeaderField: "Overwrite")
if let dest = operation.destination, !dest.hasPrefix("file://") {
request.setValue(self.url(of:dest).absoluteString, forHTTPHeaderField: "Destination")
}
return request
}
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 ?? ""))
}
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))
completionHandler?(error)
}
task.taskDescription = opType.json
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
}
/*
@@ -606,56 +389,8 @@ extension WebDAVFileProvider: ExtendedFileProvider {
}
}
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)
})
}
}
extension WebDAVFileProvider: FileProvider { }
// MARK: WEBDAV XML response implementation
internal extension WebDAVFileProvider {
fileprivate func delegateNotify(_ operation: FileOperationType, error: Error?) {
DispatchQueue.main.async(execute: {
if error == nil {
self.delegate?.fileproviderSucceed(self, operation: operation)
} else {
self.delegate?.fileproviderFailed(self, operation: operation)
}
})
}
}
struct DavResponse {
let href: URL
let hrefString: String
@@ -666,7 +401,7 @@ struct DavResponse {
func standardizePath(_ str: String) -> String {
let trimmedStr = str.hasPrefix("/") ? str.substring(from: str.index(after: str.startIndex)) : str
return trimmedStr.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed.subtracting(CharacterSet(charactersIn: ":"))) ?? str
return trimmedStr.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? str
}
// find node names with namespace