Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e3582f6fa | |||
| dd7a9d20b6 | |||
| d4a9b4a34f | |||
| 34c663e62c | |||
| 1415dda987 | |||
| cd465c1288 | |||
| a605b0cd85 | |||
| bf7043de29 | |||
| ff5e13931f | |||
| 75af738d2e | |||
| f54a1253e4 | |||
| 1394a92662 | |||
| dab171c755 | |||
| ea5de2e2aa | |||
| 2253cca086 | |||
| e15a900ade | |||
| 5c2c56c44c | |||
| ff4bbdf0de | |||
| 163a218ac2 | |||
| 81401ee36f | |||
| 759ba3c7bf | |||
| f5c8f6308b | |||
| 4dbb0adb18 | |||
| bf62d585fd | |||
| f21f658874 | |||
| e6eba3d198 | |||
| 6959a14dc1 | |||
| 29a9e0fb82 | |||
| c7b4e1f124 | |||
| 99a433a0fc | |||
| 4023fbc62e |
+1
-1
@@ -56,7 +56,7 @@ script:
|
||||
after_success:
|
||||
# Run `pod trunk push` if specified
|
||||
- if [ $POD == "YES" ] && [ -n "$TRAVIS_TAG" ]; then
|
||||
pod trunk push;
|
||||
pod trunk push --allow-warnings;
|
||||
fi
|
||||
|
||||
# - bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
@@ -16,8 +16,8 @@ Pod::Spec.new do |s|
|
||||
#
|
||||
|
||||
s.name = "FileProvider"
|
||||
s.version = "0.14.5"
|
||||
s.summary = "FileManager replacement for Local and Remote (WebDAV/Dropbox/OneDrive/SMB2) files on iOS and macOS."
|
||||
s.version = "0.16.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.
|
||||
# * Think: What does it do? Why did you write it? What is the focus?
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; };
|
||||
792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; };
|
||||
792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; };
|
||||
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 */; };
|
||||
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,6 +53,7 @@
|
||||
796807551E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */; };
|
||||
796807561E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */; };
|
||||
796807571E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */; };
|
||||
798654331E8874BC002FA550 /* FTPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798654321E8874BC002FA550 /* FTPHelper.swift */; };
|
||||
799396AA1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; };
|
||||
799396AB1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; };
|
||||
799396AC1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; };
|
||||
@@ -112,12 +116,14 @@
|
||||
79BD63BE1E2CC3C20035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63BD1E2CC3C20035128C /* ImageIO.framework */; };
|
||||
79BD63C01E2CC3CD0035128C /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */; };
|
||||
79BD63C21E2CC3D30035128C /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63C11E2CC3D30035128C /* AVFoundation.framework */; };
|
||||
79BD63C51E2D17880035128C /* OneDriveFileProvide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */; };
|
||||
79BD63C61E2D17880035128C /* OneDriveFileProvide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */; };
|
||||
79BD63C71E2D17880035128C /* OneDriveFileProvide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */; };
|
||||
79BD63C51E2D17880035128C /* OneDriveFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */; };
|
||||
79BD63C61E2D17880035128C /* OneDriveFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */; };
|
||||
79BD63C71E2D17880035128C /* OneDriveFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */; };
|
||||
79BD63C81E2D17880035128C /* OneDriveHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C41E2D17880035128C /* OneDriveHelper.swift */; };
|
||||
79BD63C91E2D17880035128C /* OneDriveHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C41E2D17880035128C /* OneDriveHelper.swift */; };
|
||||
79BD63CA1E2D17880035128C /* OneDriveHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C41E2D17880035128C /* OneDriveHelper.swift */; };
|
||||
79F4678B1E8B80F200C91A85 /* FTPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798654321E8874BC002FA550 /* FTPHelper.swift */; };
|
||||
79F4678C1E8B80F200C91A85 /* FTPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798654321E8874BC002FA550 /* FTPHelper.swift */; };
|
||||
79F5745B1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; };
|
||||
79F5745C1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; };
|
||||
79F5745D1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; };
|
||||
@@ -133,11 +139,13 @@
|
||||
7924B1921D89DAE000589DB7 /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
798654321E8874BC002FA550 /* FTPHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTPHelper.swift; sourceTree = "<group>"; };
|
||||
799396671D48B7F600086753 /* FileProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
799396751D48B80D00086753 /* FileProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
799396821D48B82700086753 /* FileProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -174,7 +182,7 @@
|
||||
79BD63BD1E2CC3C20035128C /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/ImageIO.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/CoreGraphics.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63C11E2CC3D30035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneDriveFileProvide.swift; sourceTree = "<group>"; };
|
||||
79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneDriveFileProvider.swift; sourceTree = "<group>"; };
|
||||
79BD63C41E2D17880035128C /* OneDriveHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneDriveHelper.swift; sourceTree = "<group>"; };
|
||||
79F5745A1DFDB10A00179ABF /* FileObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileObject.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -294,7 +302,9 @@
|
||||
7902C0851D61B56D00564440 /* RemoteSession.swift */,
|
||||
799396931D48C02300086753 /* DropboxFileProvider.swift */,
|
||||
794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */,
|
||||
79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */,
|
||||
7936BC111E880F5700A6C81C /* FTPFileProvider.swift */,
|
||||
798654321E8874BC002FA550 /* FTPHelper.swift */,
|
||||
79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */,
|
||||
79BD63C41E2D17880035128C /* OneDriveHelper.swift */,
|
||||
7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */,
|
||||
799396971D48C02300086753 /* SMBClient.swift */,
|
||||
@@ -485,12 +495,14 @@
|
||||
799396D41D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
7924B1A21D89DAE000589DB7 /* Options.swift in Sources */,
|
||||
792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */,
|
||||
7936BC121E880F5700A6C81C /* FTPFileProvider.swift in Sources */,
|
||||
79480FF61E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
|
||||
79F5745B1DFDB10B00179ABF /* FileObject.swift in Sources */,
|
||||
79BD63C51E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
|
||||
79BD63C51E2D17880035128C /* OneDriveFileProvider.swift in Sources */,
|
||||
7924B1991D89DAE000589DB7 /* Element.swift in Sources */,
|
||||
799396C81D48C02300086753 /* SMB2IOCtl.swift in Sources */,
|
||||
799396D71D48C02300086753 /* SMB2Types.swift in Sources */,
|
||||
798654331E8874BC002FA550 /* FTPHelper.swift in Sources */,
|
||||
7924B1B21D89FCDA00589DB7 /* FPSStreamTask.swift in Sources */,
|
||||
799396C51D48C02300086753 /* SMB2FileOperation.swift in Sources */,
|
||||
79BD63C81E2D17880035128C /* OneDriveHelper.swift in Sources */,
|
||||
@@ -526,12 +538,14 @@
|
||||
799396D51D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
7924B1A31D89DAE000589DB7 /* Options.swift in Sources */,
|
||||
792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */,
|
||||
7936BC131E880F5700A6C81C /* FTPFileProvider.swift in Sources */,
|
||||
79480FF71E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
|
||||
79F5745C1DFDB10B00179ABF /* FileObject.swift in Sources */,
|
||||
79BD63C61E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
|
||||
79BD63C61E2D17880035128C /* OneDriveFileProvider.swift in Sources */,
|
||||
7924B1B01D89F7DE00589DB7 /* FPSStreamTask.swift in Sources */,
|
||||
7924B19A1D89DAE000589DB7 /* Element.swift in Sources */,
|
||||
799396C91D48C02300086753 /* SMB2IOCtl.swift in Sources */,
|
||||
79F4678B1E8B80F200C91A85 /* FTPHelper.swift in Sources */,
|
||||
799396D81D48C02300086753 /* SMB2Types.swift in Sources */,
|
||||
799396C61D48C02300086753 /* SMB2FileOperation.swift in Sources */,
|
||||
79BD63C91E2D17880035128C /* OneDriveHelper.swift in Sources */,
|
||||
@@ -567,12 +581,14 @@
|
||||
799396D61D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
7924B1A41D89DAE000589DB7 /* Options.swift in Sources */,
|
||||
792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */,
|
||||
7936BC141E880F5700A6C81C /* FTPFileProvider.swift in Sources */,
|
||||
79480FF81E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
|
||||
79F5745D1DFDB10B00179ABF /* FileObject.swift in Sources */,
|
||||
79BD63C71E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
|
||||
79BD63C71E2D17880035128C /* OneDriveFileProvider.swift in Sources */,
|
||||
7924B1B11D89F7DF00589DB7 /* FPSStreamTask.swift in Sources */,
|
||||
7924B19B1D89DAE000589DB7 /* Element.swift in Sources */,
|
||||
799396CA1D48C02300086753 /* SMB2IOCtl.swift in Sources */,
|
||||
79F4678C1E8B80F200C91A85 /* FTPHelper.swift in Sources */,
|
||||
799396D91D48C02300086753 /* SMB2Types.swift in Sources */,
|
||||
799396C71D48C02300086753 /* SMB2FileOperation.swift in Sources */,
|
||||
79BD63CA1E2D17880035128C /* OneDriveHelper.swift in Sources */,
|
||||
@@ -605,7 +621,7 @@
|
||||
799396601D48B7BF00086753 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.14.5;
|
||||
BUNDLE_VERSION_STRING = 0.16.1;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
@@ -615,6 +631,7 @@
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -635,7 +652,7 @@
|
||||
799396611D48B7BF00086753 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.14.5;
|
||||
BUNDLE_VERSION_STRING = 0.16.1;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
[](https://codecov.io/gh/amosavian/FileProvider)
|
||||
--->
|
||||
|
||||
This library provides implementaion of WebDav, Dropbox, OneDrive and SMB2 (incomplete) and local files.
|
||||
This library provides implementaion of WebDav, FTP, Dropbox, OneDrive and SMB2 (incomplete) and local files.
|
||||
|
||||
All functions do async calls and it wont block your main thread.
|
||||
|
||||
@@ -28,7 +28,9 @@ All functions do async calls and it wont block your main thread.
|
||||
|
||||
- [x] **LocalFileProvider** a wrapper around `FileManager` with some additions like builtin coordinating, searching and reading a portion of file.
|
||||
- [x] **CloudFileProvider** A wrapper around app's ubiquitous container API of iCloud Drive.
|
||||
- [x] **WebDAVFileProvider** WebDAV protocol is defacto file transmission standard, used by many cloud services like Yandex.
|
||||
- [x] **WebDAVFileProvider** WebDAV protocol is defacto file transmission standard, supported by some cloud services like `ownCloud`, `Box.com` and `Yandex.disk`.
|
||||
- [x] **FTPFileProvider** While deprecated in 1990s due to serious security concerns, it's still in use on some Web hosts.
|
||||
* Active mode is not implemented yet.
|
||||
- [x] **DropboxFileProvider** A wrapper around Dropbox Web API.
|
||||
* For now it has limitation in uploading files up to 150MB.
|
||||
- [x] **OneDriveFileProvider** A wrapper around OneDrive REST API, works with `onedrive.com` and compatible (business) servers.
|
||||
@@ -37,16 +39,15 @@ All functions do async calls and it wont block your main thread.
|
||||
- [ ] **AmazonS3FileProvider** Amazon storage backend. Used by many sites.
|
||||
- [ ] **SMBFileProvider** SMB2/3 introduced in 2006, which is a file and printer sharing protocol originated from Microsoft Windows and now is replacing AFP protocol on macOS.
|
||||
* Data types and some basic functions are implemented but *main interface is not implemented yet!*.
|
||||
* SMB1/CIFS is deprecated and very tricky to be implemented.
|
||||
- [ ] **FTPFileProvider** while deprecated in 1990s, it's still in use on some Web hosts.
|
||||
* SMB1/CIFS is deprecated and very tricky to be implemented due to strict memory allignment in Swift.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Swift 3.0 +**
|
||||
- **Swift 3.0 or higher**
|
||||
- iOS 8.0 , OSX 10.10
|
||||
- XCode 8.0
|
||||
|
||||
Legacy version is available in swift-2 branch
|
||||
Legacy version is available in swift-2 branch.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -67,7 +68,7 @@ github "amosavian/FileProvider"
|
||||
Or to use in Swift Package Manager add this line in `Dependencies`:
|
||||
|
||||
```swift
|
||||
.Package(url: "https://github.com/amosavian/FileProvider.git", majorVersion: 0, minorVersion: 12)
|
||||
.Package(url: "https://github.com/amosavian/FileProvider.git", majorVersion: 0)
|
||||
```
|
||||
|
||||
### Manually
|
||||
@@ -271,12 +272,7 @@ documentsProvider.create(folder: "new folder", at: "/", completionHandler: { err
|
||||
})
|
||||
```
|
||||
|
||||
Creating new file from data:
|
||||
|
||||
```swift
|
||||
let data = "hello world!".data(encoding: .utf8)
|
||||
documentsProvider.create(file: "newFile.txt", at: "/", contents: data, completionHandler: nil)
|
||||
```
|
||||
To create a file, use `writeContents(path:, content:, atomically:, completionHandler:)` method.
|
||||
|
||||
### Copy and Move/Rename Files
|
||||
|
||||
@@ -300,7 +296,7 @@ documentsProvider.moveItem(path: "new folder/old.txt", to: "new.txt", overwrite:
|
||||
documentsProvider.removeItem(path: "new.txt", completionHandler: nil)
|
||||
```
|
||||
|
||||
***Caution:*** This method will delete directories with all it's contents recursively.
|
||||
***Caution:*** This method will delete directories with all it's contents recursively except for FTP providers that don't support `SITE RMDIR` command, this will be fixed later.
|
||||
|
||||
### Fetching Contents of File
|
||||
|
||||
@@ -333,6 +329,27 @@ let data = "What's up Newyork!".data(encoding: .utf8)
|
||||
documentsProvider.writeContents(path: "old.txt", content: data, atomically: true, completionHandler: nil)
|
||||
```
|
||||
|
||||
### Copying Files to and From Local URL
|
||||
|
||||
There are two methods to download and upload files between provider's and local storage. These methods use `URLSessionDownloadTask` and `URLSessionUploadTask` classes and allows to use background session and provide progress via delegate.
|
||||
|
||||
To upload a file:
|
||||
|
||||
```swift
|
||||
let fileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("image.jpg")
|
||||
documentsProvider.copyItem(localFile: fileURL, to: "/upload/image.jpg", overwrite: true, completionHandler: nil)
|
||||
```
|
||||
|
||||
To download a file:
|
||||
|
||||
```swift
|
||||
let fileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("image.jpg")
|
||||
documentsProvider.copyItem(path: "/download/image.jpg", toLocalURL: fileURL, overwrite: true, completionHandler: nil)
|
||||
```
|
||||
|
||||
* 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.
|
||||
|
||||
### 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:
|
||||
@@ -443,9 +460,10 @@ if documentsProvider..propertiesOfFile(path: file.path, completionHandler: { (pr
|
||||
|
||||
We would love for you to contribute to **FileProvider**, check the `LICENSE` file for more info.
|
||||
|
||||
Things to do:
|
||||
Things you may consider to help us:
|
||||
|
||||
- [ ] Implement Test-case (XCTest)
|
||||
- [ ] Implement request/response stack for `SMBClient`
|
||||
- [ ] Implement Test-case (`XCTest`)
|
||||
- [ ] Add Sample project for iOS
|
||||
- [ ] Add Sample project for macOS
|
||||
|
||||
@@ -467,7 +485,7 @@ Distributed under the MIT license. See `LICENSE` for more information.
|
||||
[https://github.com/amosavian/](https://github.com/amosavian/)
|
||||
|
||||
[cocoapods]: https://cocoapods.org/pods/FileProvider
|
||||
[swift-image]: https://img.shields.io/badge/swift-3.0-orange.svg
|
||||
[swift-image]: https://img.shields.io/badge/swift-3.0,%203.1-orange.svg
|
||||
[swift-url]: https://swift.org/
|
||||
[platform-image]: https://img.shields.io/cocoapods/p/FileProvider.svg
|
||||
[license-image]: https://img.shields.io/github/license/amosavian/FileProvider.svg
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2014-2017 Marko Tadić <tadija@me.com> http://tadija.net
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -128,7 +128,7 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
query.stop()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
@@ -195,7 +195,7 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
query.stop()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
@@ -378,23 +378,6 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Creates an new file with data passed to method asynchronously.
|
||||
Returns error via completionHandler if file is already exists.
|
||||
|
||||
- Parameters:
|
||||
- file: New file name with extension separated by period.
|
||||
- at: Parent path of new file.
|
||||
- data: Data of new files. Pass nil or `Data()` to create empty file.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.create(file: fileName, at: atPath, contents: data, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
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.
|
||||
@@ -571,7 +554,7 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
- Returns: An `OperationHandle` 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? {
|
||||
open override func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
@@ -645,7 +628,7 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
|
||||
let path = self.relativePathOf(url: url)
|
||||
let rpath = path.hasPrefix("/") ? path.substring(from: path.index(after: path.startIndex)) : path
|
||||
let relativeUrl = URL(string: rpath, relativeTo: self.baseURL)
|
||||
let relativeUrl = URL(string: rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath, relativeTo: self.baseURL)
|
||||
let file = FileObject(url: relativeUrl ?? url, name: name, path: path)
|
||||
|
||||
file.size = (attribs[NSMetadataItemFSSizeKey] as? NSNumber)?.int64Value ?? -1
|
||||
@@ -734,8 +717,11 @@ 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
|
||||
|
||||
init (operationType: FileOperationType, baseURL: URL?) {
|
||||
|
||||
@@ -17,7 +17,7 @@ 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 class var type: String { return "Dropbox" }
|
||||
open let baseURL: URL?
|
||||
open var currentPath: String
|
||||
|
||||
@@ -34,7 +34,11 @@ open class DropboxFileProvider: FileProviderBasicRemote {
|
||||
}
|
||||
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open let credential: URLCredential?
|
||||
open var credential: URLCredential? {
|
||||
didSet {
|
||||
sessionDelegate?.credential = self.credential
|
||||
}
|
||||
}
|
||||
open private(set) var cache: URLCache?
|
||||
public var useCache: Bool
|
||||
public var validatingCache: Bool
|
||||
@@ -42,14 +46,28 @@ open class DropboxFileProvider: FileProviderBasicRemote {
|
||||
fileprivate var _session: URLSession?
|
||||
fileprivate var sessionDelegate: SessionDelegate?
|
||||
public var session: URLSession {
|
||||
if _session == nil {
|
||||
self.sessionDelegate = SessionDelegate(fileProvider: self, credential: credential)
|
||||
let config = URLSessionConfiguration.default
|
||||
config.urlCache = cache
|
||||
config.requestCachePolicy = .returnCacheDataElseLoad
|
||||
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: self.operation_queue)
|
||||
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!)
|
||||
}
|
||||
return _session!
|
||||
}
|
||||
|
||||
fileprivate var _longpollSession: URLSession?
|
||||
@@ -117,11 +135,16 @@ open class DropboxFileProvider: FileProviderBasicRemote {
|
||||
}
|
||||
|
||||
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)) {
|
||||
@@ -212,11 +235,6 @@ extension DropboxFileProvider: FileProviderOperations {
|
||||
return doOperation(.create(path: path), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let filePath = (path as NSString).appendingPathComponent(fileName)
|
||||
return self.writeContents(path: filePath, contents: data ?? Data(), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
@@ -274,6 +292,14 @@ extension DropboxFileProvider: FileProviderOperations {
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
@@ -288,26 +314,27 @@ extension DropboxFileProvider: FileProviderOperations {
|
||||
}
|
||||
let url = URL(string: "files/download", relativeTo: contentURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
let requestDictionary: [String: AnyObject] = ["path": path as NSString]
|
||||
let requestJson = String(jsonDictionary: requestDictionary) ?? ""
|
||||
request.setValue(requestJson, forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
let task = session.downloadTask(with: request, completionHandler: { (cacheURL, response, error) in
|
||||
guard let cacheURL = cacheURL, let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: (response as? HTTPURLResponse)?.statusCode ?? -1)
|
||||
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 ?? error)
|
||||
completionHandler?(serverError)
|
||||
return
|
||||
}
|
||||
do {
|
||||
try FileManager.default.moveItem(at: cacheURL, to: destURL)
|
||||
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])
|
||||
@@ -326,35 +353,45 @@ extension DropboxFileProvider: FileProviderReadWrite {
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
let url = URL(string: "files/download", relativeTo: contentURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
if length > 0 {
|
||||
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
|
||||
request.setValue("bytes=\(offset)-\(offset + Int64(length) - 1)", forHTTPHeaderField: "Range")
|
||||
} else if offset > 0 && length < 0 {
|
||||
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
|
||||
request.setValue(String(jsonDictionary: requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderDropboxError?
|
||||
if let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: httpResponse.statusCode) {
|
||||
serverError = FileProviderDropboxError(code: code, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
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
|
||||
}
|
||||
let filedata = serverError ?? error == nil ? data : nil
|
||||
completionHandler(filedata, serverError ?? error)
|
||||
})
|
||||
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? {
|
||||
public func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.modify(path: path)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
// FIXME: remove 150MB restriction
|
||||
return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
|
||||
return upload_simple(path, data: data ?? Data(), overwrite: overwrite, operation: opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -375,22 +412,6 @@ extension DropboxFileProvider: FileProviderReadWrite {
|
||||
}
|
||||
|
||||
extension DropboxFileProvider {
|
||||
/// *OBSOLETED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead.
|
||||
@available(*, obsoleted: 1.0, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
|
||||
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
|
||||
self.publicLink(to: path) { (url, file, _, error) in
|
||||
completionHandler(url, file, error)
|
||||
}
|
||||
}
|
||||
|
||||
/// *OBSOLETED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead.
|
||||
@available(*, obsoleted: 1.0, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
|
||||
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
self.publicLink(to: path) { (url, file, expiration, error) in
|
||||
completionHandler(url, file, expiration, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Genrates a public url to a file to be shared with other users and can be downloaded without authentication.
|
||||
|
||||
@@ -399,10 +420,10 @@ extension DropboxFileProvider {
|
||||
- Parameters:
|
||||
- to: path of file, including file/directory name.
|
||||
- completionHandler: a closure with result of directory entries or error.
|
||||
`link`: a url returned by Dropbox to share.
|
||||
`attribute`: a `FileObject` containing the attributes of the item.
|
||||
`expiration`: a `Date` object, determines when the public url will expires.
|
||||
`error`: Error returned by Dropbox.
|
||||
- `link`: a url returned by Dropbox to share.
|
||||
- `attribute`: a `FileObject` containing the attributes of the item.
|
||||
- `expiration`: a `Date` object, determines when the public url will expires.
|
||||
- `error`: Error returned by Dropbox.
|
||||
*/
|
||||
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
let url = URL(string: "files/get_temporary_link", relativeTo: apiURL)!
|
||||
@@ -442,9 +463,9 @@ extension DropboxFileProvider {
|
||||
- remoteURL: a valid remote url to file.
|
||||
- to: Destination path of file, including file/directory name.
|
||||
- completionHandler: a closure with result of directory entries or error.
|
||||
`jobId`: Job ID returned by Dropbox to monitor the copy/download progress.
|
||||
`attribute`: A `FileObject` containing the attributes of the item.
|
||||
`error`: Error returned by Dropbox.
|
||||
- `jobId`: Job ID returned by Dropbox to monitor the copy/download progress.
|
||||
- `attribute`: A `FileObject` containing the attributes of the item.
|
||||
- `error`: Error returned by Dropbox.
|
||||
*/
|
||||
open func copyItem(remoteURL: URL, to toPath: String, completionHandler: @escaping ((_ jobId: String?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
|
||||
if remoteURL.isFileURL {
|
||||
@@ -538,24 +559,35 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
/// Default value for dimension is 64x64, according to Dropbox documentation
|
||||
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
|
||||
let url: URL
|
||||
let thumbAPI: Bool
|
||||
switch (path as NSString).pathExtension.lowercased() {
|
||||
case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff":
|
||||
url = URL(string: "files/get_thumbnail", relativeTo: contentURL)!
|
||||
thumbAPI = true
|
||||
case "doc", "docx", "docm", "xls", "xlsx", "xlsm":
|
||||
fallthrough
|
||||
case "ppt", "pps", "ppsx", "ppsm", "pptx", "pptm":
|
||||
fallthrough
|
||||
case "rtf":
|
||||
url = URL(string: "files/get_preview", relativeTo: contentURL)!
|
||||
thumbAPI = false
|
||||
default:
|
||||
return
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
var requestDictionary: [String: AnyObject] = ["path": path as NSString]
|
||||
requestDictionary["format"] = "jpeg" as NSString
|
||||
if let dimension = dimension {
|
||||
requestDictionary["size"] = "w\(Int(dimension.width))h\(Int(dimension.height))" as NSString
|
||||
if thumbAPI {
|
||||
requestDictionary["format"] = "jpeg" as NSString
|
||||
let size: String
|
||||
switch dimension?.height ?? 64 {
|
||||
case 0...32: size = "w32h32"
|
||||
case 33...64: size = "w64h64"
|
||||
case 65...128: size = "w128h128"
|
||||
case 129...480: size = "w640h480"
|
||||
default: size = "w1024h768"
|
||||
}
|
||||
requestDictionary["size"] = size as NSString
|
||||
}
|
||||
request.setValue(String(jsonDictionary: requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
@@ -570,8 +602,12 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
image = pageImage
|
||||
} else if let contentType = (response as? HTTPURLResponse)?.allHeaderFields["Content-Type"] as? String, contentType.contains("text/html") {
|
||||
// TODO: Implement converting html returned type of get_preview to image
|
||||
} else {
|
||||
image = ImageClass(data: data)
|
||||
} else if let fetchedimage = ImageClass(data: data){
|
||||
if let dimension = dimension {
|
||||
image = DropboxFileProvider.scaleDown(image: fetchedimage, toSize: dimension)
|
||||
} else {
|
||||
image = fetchedimage
|
||||
}
|
||||
}
|
||||
}
|
||||
completionHandler(image, error)
|
||||
|
||||
+27
-53
@@ -17,22 +17,18 @@ public struct FileProviderDropboxError: FileProviderHTTPError {
|
||||
|
||||
/// Containts path, url and attributes of a Dropbox file or resource.
|
||||
public final class DropboxFileObject: FileObject {
|
||||
internal init(name: String, path: String) {
|
||||
super.init(url: URL(string: path) ?? URL(string: "/")!, name: name, path: path)
|
||||
}
|
||||
|
||||
internal convenience init? (jsonStr: String) {
|
||||
guard let json = jsonStr.deserializeJSON() else { return nil }
|
||||
self.init(json: json)
|
||||
}
|
||||
|
||||
internal convenience init? (json: [String: AnyObject]) {
|
||||
internal init? (json: [String: AnyObject]) {
|
||||
guard let name = json["name"] as? String else { return nil }
|
||||
guard let path = json["path_display"] as? String else { return nil }
|
||||
self.init(name: name, path: path)
|
||||
super.init(url: nil, name: name, path: path)
|
||||
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
|
||||
self.serverTime = resolve(dateString: json["server_modified"] as? String ?? "")
|
||||
self.modifiedDate = resolve(dateString: json["client_modified"] as? String ?? "")
|
||||
self.serverTime = Date(rfcString: json["server_modified"] as? String ?? "")
|
||||
self.modifiedDate = Date(rfcString: json["client_modified"] as? String ?? "")
|
||||
self.type = (json[".tag"] as? String) == "folder" ? .directory : .regular
|
||||
self.isReadOnly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false
|
||||
self.id = json["id"] as? String
|
||||
@@ -42,10 +38,10 @@ public final class DropboxFileObject: FileObject {
|
||||
/// The time contents of file has been modified on server, returns nil if not set
|
||||
open internal(set) var serverTime: Date? {
|
||||
get {
|
||||
return allValues[.serverDate] as? Date
|
||||
return allValues[.serverDateKey] as? Date
|
||||
}
|
||||
set {
|
||||
allValues[.serverDate] = newValue
|
||||
allValues[.serverDateKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,40 +117,8 @@ internal extension DropboxFileProvider {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func upload_simple(_ targetPath: String, data: Data, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
if data.count > 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"] = rfc3339utc(of: modifiedDate) as NSString
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue(String(jsonDictionary: requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
request.httpBody = data
|
||||
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderDropboxError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderDropboxError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: operation, tasks: [task])
|
||||
}
|
||||
|
||||
func upload_simple(_ targetPath: String, localFile: URL, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let size = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1
|
||||
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)
|
||||
@@ -166,20 +130,30 @@ internal extension DropboxFileProvider {
|
||||
url = URL(string: "files/upload", relativeTo: contentURL)!
|
||||
requestDictionary["path"] = correctPath(targetPath) as NSString?
|
||||
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
|
||||
requestDictionary["client_modified"] = rfc3339utc(of: modifiedDate) as NSString
|
||||
requestDictionary["client_modified"] = modifiedDate.rfc3339utc() as NSString
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue(String(jsonDictionary: requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
|
||||
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: FileProviderDropboxError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderDropboxError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
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)
|
||||
})
|
||||
self?.delegateNotify(.create(path: targetPath), error: responseError ?? error)
|
||||
}
|
||||
task.taskDescription = operation.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: operation, tasks: [task])
|
||||
@@ -241,11 +215,11 @@ internal extension DropboxFileProvider {
|
||||
DropboxFileProvider.decimalFormatter.numberStyle = .decimal
|
||||
DropboxFileProvider.decimalFormatter.maximumFractionDigits = 5
|
||||
keys.append("Location")
|
||||
let latStr = DropboxFileProvider.decimalFormatter.string(from: NSNumber(value: latitude))
|
||||
let longStr = DropboxFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))
|
||||
let latStr = DropboxFileProvider.decimalFormatter.string(from: NSNumber(value: latitude))!
|
||||
let longStr = DropboxFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))!
|
||||
dic["Location"] = "\(latStr), \(longStr)"
|
||||
}
|
||||
if let timeTakenStr = json["time_taken"] as? String, let timeTaken = resolve(dateString: timeTakenStr) {
|
||||
if let timeTakenStr = json["time_taken"] as? String, let timeTaken = Date(rfcString: timeTakenStr) {
|
||||
keys.append("Date taken")
|
||||
DropboxFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
dic["Date taken"] = DropboxFileProvider.dateFormatter.string(from: timeTaken)
|
||||
|
||||
@@ -414,7 +414,7 @@ public struct LocalFileInformationGenerator {
|
||||
guard let date = date else { return nil }
|
||||
var dateStr = date
|
||||
if dateStr.hasPrefix("D:") {
|
||||
dateStr = date.substring(from: date.characters.index(date.startIndex, offsetBy: 2))
|
||||
dateStr.characters.removeFirst(2)
|
||||
}
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyyMMddHHmmssTZD"
|
||||
|
||||
+479
-313
@@ -12,51 +12,106 @@ private var lasttaskIdAssociated = 1_000_000_000
|
||||
|
||||
|
||||
/// This class is a replica of NSURLSessionStreamTask with same api for iOS 7/8
|
||||
/// while it will fallback to NSURLSessionStreamTask in iOS 9.
|
||||
internal class FPSStreamTask: URLSessionTask, StreamDelegate {
|
||||
/// while it can actually fallback to NSURLSessionStreamTask in iOS 9.
|
||||
public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
|
||||
fileprivate var inputStream: InputStream?
|
||||
fileprivate var outputStream: OutputStream?
|
||||
|
||||
fileprivate var dispatch_queue: DispatchQueue!
|
||||
fileprivate var operation_queue: OperationQueue!
|
||||
internal var _underlyingSession: URLSession
|
||||
fileprivate var streamDelegate: FPSStreamDelegate? {
|
||||
return (_underlyingSession.delegate as? FPSStreamDelegate)
|
||||
}
|
||||
fileprivate var _taskIdentifier: Int
|
||||
fileprivate var _taskDescription: String?
|
||||
|
||||
/// Force using `URLSessionStreamTask` for iOS 9 and later
|
||||
public var useURLSession = true
|
||||
@available(iOS 9.0, OSX 10.11, *)
|
||||
static var streamTasks = [Int: URLSessionStreamTask]()
|
||||
fileprivate static var streamTasks = [Int: URLSessionStreamTask]()
|
||||
|
||||
@available(iOS 9.0, OSX 10.11, *)
|
||||
internal var _underlyingTask: URLSessionStreamTask? {
|
||||
return FPSStreamTask.streamTasks[_taskIdentifier]
|
||||
return FileProviderStreamTask.streamTasks[_taskIdentifier]
|
||||
}
|
||||
|
||||
/**
|
||||
* An identifier uniquely identifies the task within a given session.
|
||||
*
|
||||
* This value is unique only within the context of a single session;
|
||||
* tasks in other sessions may have the same `taskIdentifier` value.
|
||||
*/
|
||||
open override var taskIdentifier: Int {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
return _underlyingTask!.taskIdentifier
|
||||
} else {
|
||||
return _taskIdentifier
|
||||
if self.useURLSession {
|
||||
return _underlyingTask!.taskIdentifier
|
||||
}
|
||||
}
|
||||
|
||||
return _taskIdentifier
|
||||
}
|
||||
|
||||
/// An app-provided description of the current task.
|
||||
///
|
||||
/// This value may be nil. It is intended to contain human-readable strings that you can
|
||||
/// then display to the user as part of your app’s user interface.
|
||||
open override var taskDescription: String? {
|
||||
get {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
if self.useURLSession {
|
||||
return _underlyingTask!.taskDescription
|
||||
}
|
||||
}
|
||||
|
||||
return _taskDescription
|
||||
}
|
||||
set {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.taskDescription = newValue
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_taskDescription = newValue
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var _state: URLSessionTask.State = .suspended
|
||||
/**
|
||||
* The current state of the task—active, suspended, in the process
|
||||
* of being canceled, or completed.
|
||||
*/
|
||||
override open var state: URLSessionTask.State {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
return _underlyingTask!.state
|
||||
} else {
|
||||
return _state
|
||||
if self.useURLSession {
|
||||
return _underlyingTask!.state
|
||||
}
|
||||
}
|
||||
|
||||
return _state
|
||||
}
|
||||
|
||||
/**
|
||||
* The original request object passed when the task was created.
|
||||
* This value is typically the same as the currently active request (`currentRequest`)
|
||||
* except when the server has responded to the initial request with a redirect to a different URL.
|
||||
*/
|
||||
override open var originalRequest: URLRequest? {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
return _underlyingTask!.originalRequest
|
||||
} else {
|
||||
return nil
|
||||
if self.useURLSession {
|
||||
return _underlyingTask!.originalRequest
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL request object currently being handled by the task.
|
||||
* This value is typically the same as the initial request (`originalRequest`)
|
||||
* except when the server has responded to the initial request with a redirect to a different URL.
|
||||
*/
|
||||
override open var currentRequest: URLRequest? {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
return _underlyingTask!.currentRequest
|
||||
@@ -68,398 +123,505 @@ internal class FPSStreamTask: URLSessionTask, StreamDelegate {
|
||||
fileprivate var _countOfBytesSent: Int64 = 0
|
||||
fileprivate var _countOfBytesRecieved: Int64 = 0
|
||||
|
||||
/**
|
||||
* The number of bytes that the task has sent to the server in the request body.
|
||||
*
|
||||
* This byte count includes only the length of the request body itself, not the request headers.
|
||||
*
|
||||
* To be notified when this value changes, implement the
|
||||
* `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)` delegate method.
|
||||
*/
|
||||
override open var countOfBytesSent: Int64 {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
return _underlyingTask!.countOfBytesSent
|
||||
} else {
|
||||
return _countOfBytesSent
|
||||
if self.useURLSession {
|
||||
return _underlyingTask!.countOfBytesSent
|
||||
}
|
||||
}
|
||||
|
||||
return _countOfBytesSent
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of bytes that the task has received from the server in the response body.
|
||||
*
|
||||
* To be notified when this value changes, implement the `urlSession(_:dataTask:didReceive:)` delegate method (for data and upload tasks)
|
||||
* or the `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)` method (for download tasks).
|
||||
*/
|
||||
override open var countOfBytesReceived: Int64 {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
return _underlyingTask!.countOfBytesReceived
|
||||
} else {
|
||||
return _countOfBytesRecieved
|
||||
if self.useURLSession {
|
||||
return _underlyingTask!.countOfBytesReceived
|
||||
}
|
||||
}
|
||||
|
||||
return _countOfBytesRecieved
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of bytes that the task expects to send in the request body.
|
||||
*
|
||||
* The `URL` loading system can determine the length of the upload data in three ways:
|
||||
* - From the length of the `NSData` object provided as the upload body.
|
||||
* - From the length of the file on disk provided as the upload body of an upload task (not a download task).
|
||||
* - From the `Content-Length` in the request object, if you explicitly set it.
|
||||
*
|
||||
* Otherwise, the value is `NSURLSessionTransferSizeUnknown` (`-1`) if you provided a stream or body data object, or zero (`0`) if you did not.
|
||||
*/
|
||||
override open var countOfBytesExpectedToSend: Int64 {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
return _underlyingTask!.countOfBytesExpectedToSend
|
||||
} else {
|
||||
return Int64(dataToBeSent.length)
|
||||
return Int64(dataToBeSent.count)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of bytes that the task expects to receive in the response body.
|
||||
*
|
||||
* This value is determined based on the `Content-Length` header received from the server.
|
||||
* If that header is absent, the value is `NSURLSessionTransferSizeUnknown`.
|
||||
*/
|
||||
override open var countOfBytesExpectedToReceive: Int64 {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
return _underlyingTask!.countOfBytesExpectedToReceive
|
||||
} else {
|
||||
return Int64(dataReceived.length)
|
||||
if self.useURLSession {
|
||||
return _underlyingTask!.countOfBytesExpectedToReceive
|
||||
}
|
||||
}
|
||||
|
||||
return Int64(dataReceived.count)
|
||||
}
|
||||
|
||||
override public init() {
|
||||
fatalError("Use NSURLSession.fpstreamTask() method")
|
||||
}
|
||||
|
||||
var host: (hostname: String, port: Int)?
|
||||
var service: NetService?
|
||||
fileprivate var host: (hostname: String, port: Int)?
|
||||
fileprivate var service: NetService?
|
||||
|
||||
internal init(session: URLSession, host: String, port: Int) {
|
||||
self._underlyingSession = session
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
let task = session.streamTask(withHostName: host, port: port)
|
||||
self._taskIdentifier = task.taskIdentifier
|
||||
FPSStreamTask.streamTasks[_taskIdentifier] = task
|
||||
} else {
|
||||
lasttaskIdAssociated += 1
|
||||
self._taskIdentifier = lasttaskIdAssociated
|
||||
self.host = (host, port)
|
||||
self.dispatch_queue = DispatchQueue(label: "FSPStreamTask", attributes: DispatchQueue.Attributes.concurrent)
|
||||
if self.useURLSession {
|
||||
let task = session.streamTask(withHostName: host, port: port)
|
||||
self._taskIdentifier = task.taskIdentifier
|
||||
FileProviderStreamTask.streamTasks[_taskIdentifier] = task
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lasttaskIdAssociated += 1
|
||||
self._taskIdentifier = lasttaskIdAssociated
|
||||
self.host = (host, port)
|
||||
self.operation_queue = OperationQueue()
|
||||
self.operation_queue.name = "FileProviderStreamTask"
|
||||
self.operation_queue.maxConcurrentOperationCount = 1
|
||||
}
|
||||
|
||||
internal init(session: URLSession, netService: NetService) {
|
||||
self._underlyingSession = session
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
let task = session.streamTask(with: netService)
|
||||
self._taskIdentifier = task.taskIdentifier
|
||||
FPSStreamTask.streamTasks[_taskIdentifier] = task
|
||||
} else {
|
||||
lasttaskIdAssociated += 1
|
||||
self._taskIdentifier = lasttaskIdAssociated
|
||||
self.service = netService
|
||||
self.dispatch_queue = DispatchQueue(label: "FSPStreamTask", attributes: DispatchQueue.Attributes.concurrent)
|
||||
if self.useURLSession {
|
||||
let task = session.streamTask(with: netService)
|
||||
self._taskIdentifier = task.taskIdentifier
|
||||
FileProviderStreamTask.streamTasks[_taskIdentifier] = task
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lasttaskIdAssociated += 1
|
||||
self._taskIdentifier = lasttaskIdAssociated
|
||||
self.service = netService
|
||||
self.operation_queue = OperationQueue()
|
||||
self.operation_queue.name = "FileProviderStreamTask"
|
||||
self.operation_queue.maxConcurrentOperationCount = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the task.
|
||||
*
|
||||
* This method returns immediately, marking the task as being canceled. Once a task is marked as being canceled,
|
||||
* `urlSession(_:task:didCompleteWithError:)` will be sent to the task delegate, passing an error
|
||||
* in the domain NSURLErrorDomain with the code `NSURLErrorCancelled`. A task may, under some circumstances,
|
||||
* send messages to its delegate before the cancelation is acknowledged.
|
||||
*
|
||||
* This method may be called on a task that is suspended.
|
||||
*/
|
||||
override open func cancel() {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
_underlyingTask!.cancel()
|
||||
} else {
|
||||
self._state = .canceling
|
||||
inputStream?.setValue(kCFBooleanTrue, forKey: kCFStreamPropertyShouldCloseNativeSocket as String)
|
||||
outputStream?.setValue(kCFBooleanTrue, forKey: kCFStreamPropertyShouldCloseNativeSocket as String)
|
||||
|
||||
self.inputStream?.close()
|
||||
self.outputStream?.close()
|
||||
|
||||
self.inputStream?.remove(from: RunLoop.current, forMode: RunLoopMode.defaultRunLoopMode)
|
||||
self.outputStream?.remove(from: RunLoop.current, forMode: RunLoopMode.defaultRunLoopMode)
|
||||
|
||||
self.inputStream?.delegate = nil
|
||||
self.outputStream?.delegate = nil
|
||||
|
||||
self.inputStream = nil
|
||||
self.outputStream = nil
|
||||
|
||||
self._state = .completed
|
||||
self._countOfBytesSent = 0
|
||||
self._countOfBytesRecieved = 0
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self._state = .canceling
|
||||
inputStream?.setValue(kCFBooleanTrue, forKey: kCFStreamPropertyShouldCloseNativeSocket as String)
|
||||
outputStream?.setValue(kCFBooleanTrue, forKey: kCFStreamPropertyShouldCloseNativeSocket as String)
|
||||
|
||||
self.inputStream?.close()
|
||||
self.outputStream?.close()
|
||||
|
||||
self.inputStream?.remove(from: RunLoop.main, forMode: .defaultRunLoopMode)
|
||||
self.outputStream?.remove(from: RunLoop.main, forMode: .defaultRunLoopMode)
|
||||
|
||||
self.inputStream?.delegate = nil
|
||||
self.outputStream?.delegate = nil
|
||||
|
||||
self.inputStream = nil
|
||||
self.outputStream = nil
|
||||
|
||||
self._state = .completed
|
||||
self._countOfBytesSent = 0
|
||||
self._countOfBytesRecieved = 0
|
||||
}
|
||||
|
||||
var _error: Error? = nil
|
||||
|
||||
/**
|
||||
* An error object that indicates why the task failed.
|
||||
*
|
||||
* This value is `NULL` if the task is still active or if the transfer completed successfully.
|
||||
*/
|
||||
override open var error: Error? {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
return _underlyingTask!.error
|
||||
} else {
|
||||
return _error
|
||||
if useURLSession {
|
||||
return _underlyingTask!.error
|
||||
}
|
||||
}
|
||||
|
||||
return _error
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporarily suspends a task.
|
||||
*
|
||||
* A task, while suspended, produces no network traffic and is not subject to timeouts.
|
||||
* A download task can continue transferring data at a later time.
|
||||
* All other tasks must start over when resumed.
|
||||
*/
|
||||
override open func suspend() {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
_underlyingTask!.suspend()
|
||||
} else {
|
||||
self._state = .suspended
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.suspend()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self._state = .suspended
|
||||
self.operation_queue.isSuspended = true
|
||||
}
|
||||
|
||||
// Resumes the task, if it is suspended.
|
||||
override open func resume() {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
_underlyingTask!.resume()
|
||||
} else {
|
||||
var readStream : Unmanaged<CFReadStream>?
|
||||
var writeStream : Unmanaged<CFWriteStream>?
|
||||
|
||||
if inputStream == nil || outputStream == nil {
|
||||
if let host = host {
|
||||
let hostRef: CFString = NSString(string: host.hostname)
|
||||
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, hostRef, UInt32(host.port), &readStream, &writeStream)
|
||||
} else if let service = service {
|
||||
let cfnetService = CFNetServiceCreate(kCFAllocatorDefault, service.domain as CFString, service.type as CFString, service.name as CFString, Int32(service.port))
|
||||
CFStreamCreatePairWithSocketToNetService(kCFAllocatorDefault, cfnetService.takeRetainedValue(), &readStream, &writeStream)
|
||||
}
|
||||
|
||||
inputStream = readStream?.takeRetainedValue()
|
||||
outputStream = writeStream?.takeRetainedValue()
|
||||
guard let inputStream = inputStream, let outputStream = outputStream else {
|
||||
return
|
||||
}
|
||||
streamDelegate?.urlSession?(self._underlyingSession, streamTask: self, didBecome: inputStream, outputStream: outputStream)
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.resume()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var readStream : Unmanaged<CFReadStream>?
|
||||
var writeStream : Unmanaged<CFWriteStream>?
|
||||
|
||||
if inputStream == nil || outputStream == nil {
|
||||
if let host = host {
|
||||
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, host.hostname as CFString, UInt32(host.port), &readStream, &writeStream)
|
||||
} else if let service = service {
|
||||
let cfnetService = CFNetServiceCreate(kCFAllocatorDefault, service.domain as CFString, service.type as CFString, service.name as CFString, Int32(service.port))
|
||||
CFStreamCreatePairWithSocketToNetService(kCFAllocatorDefault, cfnetService.takeRetainedValue(), &readStream, &writeStream)
|
||||
}
|
||||
|
||||
inputStream = readStream?.takeRetainedValue()
|
||||
outputStream = writeStream?.takeRetainedValue()
|
||||
guard let inputStream = inputStream, let outputStream = outputStream else {
|
||||
return
|
||||
}
|
||||
|
||||
inputStream.delegate = self
|
||||
outputStream.delegate = self
|
||||
|
||||
dispatch_queue.sync(execute: {
|
||||
inputStream.schedule(in: RunLoop.current, forMode: RunLoopMode.defaultRunLoopMode)
|
||||
outputStream.schedule(in: RunLoop.current, forMode: RunLoopMode.defaultRunLoopMode)
|
||||
})
|
||||
|
||||
inputStream.open()
|
||||
outputStream.open()
|
||||
|
||||
_state = .running
|
||||
streamDelegate?.urlSession?(self._underlyingSession, streamTask: self, didBecome: inputStream, outputStream: outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate let dataToBeSent: NSMutableData = NSMutableData()
|
||||
fileprivate let dataReceived: NSMutableData = NSMutableData()
|
||||
|
||||
/* Read minBytes, or at most maxBytes bytes and invoke the completion
|
||||
* handler on the sessions delegate queue with the data or an error.
|
||||
* If an error occurs, any outstanding reads will also fail, and new
|
||||
* read requests will error out immediately.
|
||||
*/
|
||||
open func readData(OfMinLength minBytes: Int, maxLength maxBytes: Int, timeout: TimeInterval, completionHandler: @escaping (Data?, Bool, NSError?) -> Void) {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
_underlyingTask!.readData(ofMinLength: minBytes, maxLength: maxBytes, timeout: timeout, completionHandler: completionHandler as! (Data?, Bool, Error?) -> Void)
|
||||
} else {
|
||||
guard let inputStream = inputStream else {
|
||||
return
|
||||
}
|
||||
var timedOut: Bool = false
|
||||
dispatch_queue.async {
|
||||
if timeout > 0 {
|
||||
self.dispatch_queue.asyncAfter(deadline: .now() + 1) {
|
||||
timedOut = true
|
||||
completionHandler(nil, inputStream.streamStatus == .atEnd, inputStream.streamError as NSError?)
|
||||
}
|
||||
}
|
||||
while (self.dataReceived.length == 0 || self.dataReceived.length < minBytes) && !timedOut {
|
||||
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1));
|
||||
Thread.sleep(forTimeInterval: 0.1)
|
||||
}
|
||||
let dR = NSMutableData()
|
||||
if self.dataReceived.length > maxBytes {
|
||||
let range = NSRange(location: 0, length: maxBytes - 1)
|
||||
dR.append(self.dataReceived.subdata(with: range))
|
||||
self.dataReceived.replaceBytes(in: range, withBytes: nil, length: 0)
|
||||
} else {
|
||||
dR.append(self.dataReceived as Data)
|
||||
self.dataReceived.length = 0
|
||||
}
|
||||
completionHandler(dR as Data, inputStream.streamStatus == .atEnd, inputStream.streamError as NSError?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Write the data completely to the underlying socket. If all the
|
||||
* bytes have not been written by the timeout, a timeout error will
|
||||
* occur. Note that invocation of the completion handler does not
|
||||
* guarantee that the remote side has received all the bytes, only
|
||||
* that they have been written to the kernel. */
|
||||
open func writeData(_ data: Data, timeout: TimeInterval, completionHandler: @escaping (Error?) -> Void) {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
_underlyingTask!.write(data, timeout: timeout, completionHandler: completionHandler)
|
||||
} else {
|
||||
guard let outputStream = outputStream else {
|
||||
return
|
||||
}
|
||||
var timedOut: Bool = false
|
||||
dispatch_queue.async {
|
||||
if timeout > 0 {
|
||||
self.dispatch_queue.asyncAfter(deadline: .now() + 1) {
|
||||
timedOut = true
|
||||
completionHandler(self._error)
|
||||
}
|
||||
}
|
||||
|
||||
self.dataToBeSent.append(data)
|
||||
while !outputStream.hasSpaceAvailable && !timedOut {
|
||||
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1));
|
||||
Thread.sleep(forTimeInterval: 0.1)
|
||||
}
|
||||
if self.dataToBeSent.length > 0 {
|
||||
let bytesWritten = outputStream.write(self.dataToBeSent.bytes.bindMemory(to: UInt8.self, capacity: self.dataToBeSent.length), maxLength: self.dataToBeSent.length)
|
||||
if bytesWritten > 0 {
|
||||
let range = NSRange(location: 0, length: bytesWritten)
|
||||
self.dataToBeSent.replaceBytes(in: range, withBytes: nil, length: 0)
|
||||
self._countOfBytesSent += bytesWritten
|
||||
completionHandler(nil)
|
||||
} else {
|
||||
self._error = outputStream.streamError
|
||||
completionHandler(outputStream.streamError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -captureStreams completes any already enqueued reads
|
||||
* and writes, and then invokes the
|
||||
* URLSession:streamTask:didBecomeInputStream:outputStream: delegate
|
||||
* message. When that message is received, the task object is
|
||||
* considered completed and will not receive any more delegate
|
||||
* messages. */
|
||||
open func captureStreams() {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
_underlyingTask!.captureStreams()
|
||||
} else {
|
||||
guard let outputStream = outputStream, let inputStream = inputStream else {
|
||||
return
|
||||
}
|
||||
dispatch_queue.async {
|
||||
self.write(false)
|
||||
while inputStream.streamStatus != .atEnd {
|
||||
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1));
|
||||
Thread.sleep(forTimeInterval: 0.1)
|
||||
}
|
||||
self.streamDelegate?.urlSession?(self._underlyingSession, streamTask: self, didBecome: inputStream, outputStream: outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Enqueue a request to close the write end of the underlying socket.
|
||||
* All outstanding IO will complete before the write side of the
|
||||
* socket is closed. The server, however, may continue to write bytes
|
||||
* back to the client, so best practice is to continue reading from
|
||||
* the server until you receive EOF.
|
||||
*/
|
||||
open func closeWrite() {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
_underlyingTask!.closeWrite()
|
||||
} else {
|
||||
dispatch_queue.async(execute: {
|
||||
self.write(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func write(_ close: Bool) {
|
||||
guard let outputStream = outputStream else {
|
||||
|
||||
guard let inputStream = inputStream, let outputStream = outputStream else {
|
||||
return
|
||||
}
|
||||
while self.dataToBeSent.length > 0 {
|
||||
let bytesWritten = outputStream.write(self.dataToBeSent.bytes.bindMemory(to: UInt8.self, capacity: self.dataToBeSent.length), maxLength: self.dataToBeSent.length)
|
||||
if bytesWritten > 0 {
|
||||
let range = NSRange(location: 0, length: bytesWritten)
|
||||
self.dataToBeSent.replaceBytes(in: range, withBytes: nil, length: 0)
|
||||
self._countOfBytesSent += bytesWritten
|
||||
} else {
|
||||
self._error = outputStream.streamError as NSError?
|
||||
|
||||
inputStream.delegate = self
|
||||
outputStream.delegate = self
|
||||
|
||||
operation_queue.addOperation {
|
||||
inputStream.schedule(in: RunLoop.main, forMode: .defaultRunLoopMode)
|
||||
outputStream.schedule(in: RunLoop.main, forMode: .defaultRunLoopMode)
|
||||
}
|
||||
|
||||
inputStream.open()
|
||||
outputStream.open()
|
||||
|
||||
operation_queue.isSuspended = false
|
||||
_state = .running
|
||||
}
|
||||
|
||||
fileprivate var dataToBeSent: Data = Data()
|
||||
fileprivate var dataReceived: Data = Data()
|
||||
|
||||
/**
|
||||
* Asynchronously reads a number of bytes from the stream, and calls a handler upon completion.
|
||||
*
|
||||
* - Parameter minBytes: The minimum number of bytes to read.
|
||||
* - ParametermaxBytes: The maximum number of bytes to read.
|
||||
* - Parameter timeout: A timeout for reading bytes. If the read is not completed within the specified interval,
|
||||
* the read is canceled and the completionHandler is called with an error. Pass `0` to prevent a read from timing out.
|
||||
* - Parameter completionHandler: The completion handler to call when all bytes are read, or an error occurs.
|
||||
* This handler is executed on the delegate queue. This completion handler takes the following parameters:
|
||||
* - data: The data read from the stream.
|
||||
* - atEOF: Whether or not the stream reached end-of-file (`EOF`), such that no more data can be read.
|
||||
* - error: An error object that indicates why the read failed, or `nil` if the read was successful.
|
||||
*/
|
||||
open func readData(ofMinLength minBytes: Int, maxLength maxBytes: Int, timeout: TimeInterval, completionHandler: @escaping (_ data: Data?, _ atEOF: Bool, _ error :Error?) -> Void) {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.readData(ofMinLength: minBytes, maxLength: maxBytes, timeout: timeout, completionHandler: completionHandler)
|
||||
return
|
||||
}
|
||||
if self.dataToBeSent.length == 0 {
|
||||
}
|
||||
|
||||
guard let inputStream = inputStream else {
|
||||
return
|
||||
}
|
||||
|
||||
let expireDate = Date(timeIntervalSinceNow: timeout)
|
||||
operation_queue.addOperation {
|
||||
var timedOut: Bool = false
|
||||
while (self.dataReceived.count == 0 || self.dataReceived.count < minBytes) && !timedOut {
|
||||
Thread.sleep(forTimeInterval: 0.1)
|
||||
timedOut = expireDate < Date()
|
||||
}
|
||||
var dR: Data?
|
||||
if self.dataReceived.count > maxBytes {
|
||||
let range: Range = 0..<maxBytes
|
||||
dR = self.dataReceived.subdata(in: range)
|
||||
self.dataReceived.replaceSubrange(range, with: Data())
|
||||
} else {
|
||||
if self.dataReceived.count > 0 {
|
||||
dR = self.dataReceived
|
||||
self.dataReceived.count = 0
|
||||
}
|
||||
}
|
||||
let isEOF = inputStream.streamStatus == .atEnd && self.dataReceived.count == 0
|
||||
completionHandler(dR, isEOF, dR == nil ? inputStream.streamError : nil)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously writes the specified data to the stream, and calls a handler upon completion.
|
||||
*
|
||||
* There is no guarantee that the remote side of the stream has received all of the written bytes
|
||||
* at the time that `completionHandler` is called, only that all of the data has been written to the kernel.
|
||||
*
|
||||
* - Parameter data: The data to be written.
|
||||
* - Parameter timeout: A timeout for writing bytes. If the write is not completed within the specified interval,
|
||||
* the write is canceled and the `completionHandler` is called with an error.
|
||||
* Pass `0` to prevent a write from timing out.
|
||||
* - Parameter completionHandler: The completion handler to call when all bytes are written, or an error occurs.
|
||||
* This handler is executed on the delegate queue.
|
||||
* This completion handler takes the following parameter:
|
||||
* - error: An error object that indicates why the write failed, or nil if the write was successful.
|
||||
*/
|
||||
open func write(_ data: Data, timeout: TimeInterval, completionHandler: @escaping (_ error: Error?) -> Void) {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.write(data, timeout: timeout, completionHandler: completionHandler)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
guard outputStream != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
operation_queue.addOperation {
|
||||
self.dataToBeSent.append(data)
|
||||
let result = self.write(timeout: timeout, close: false)
|
||||
if result < 0 {
|
||||
let error = self.outputStream?.streamError ?? NSError(domain: URLError.errorDomain, code: URLError.cannotWriteToFile.rawValue, userInfo: nil)
|
||||
completionHandler(error)
|
||||
} else {
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes any already enqueued reads and writes, and then invokes the
|
||||
* `urlSession(_:streamTask:didBecome:outputStream:)` delegate message.
|
||||
*/
|
||||
open func captureStreams() {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.captureStreams()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
guard let outputStream = outputStream, let inputStream = inputStream else {
|
||||
return
|
||||
}
|
||||
self.operation_queue.addOperation {
|
||||
_=self.write(close: false)
|
||||
while inputStream.streamStatus != .atEnd || outputStream.streamStatus == .writing {
|
||||
Thread.sleep(forTimeInterval: 0.1)
|
||||
}
|
||||
self.streamDelegate?.urlSession?(self._underlyingSession, streamTask: self, didBecome: inputStream, outputStream: outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes any enqueued reads and writes, and then closes the write side of the underlying socket.
|
||||
*
|
||||
* You may continue to read data using the `readData(ofMinLength:maxLength:timeout:completionHandler:)`
|
||||
* method after calling this method. Any calls to `write(_:timeout:completionHandler:)` after calling
|
||||
* this method will result in an error.
|
||||
*
|
||||
* Because the server may continue to write bytes to the client, it is recommended that
|
||||
* you continue reading until the stream reaches end-of-file (EOF).
|
||||
*/
|
||||
open func closeWrite() {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.closeWrite()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
operation_queue.addOperation {
|
||||
_ = self.write(close: true)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func write(timeout: TimeInterval = 0, close: Bool) -> Int {
|
||||
guard let outputStream = outputStream else {
|
||||
return -1
|
||||
}
|
||||
|
||||
var byteSent: Int = 0
|
||||
let expireDate = Date(timeIntervalSinceNow: timeout)
|
||||
while self.dataToBeSent.count > 0 && (timeout == 0 || expireDate > Date()) {
|
||||
let bytesWritten = self.dataToBeSent.withUnsafeBytes {
|
||||
outputStream.write($0, maxLength: self.dataToBeSent.count)
|
||||
}
|
||||
|
||||
if bytesWritten > 0 {
|
||||
let range = 0..<bytesWritten
|
||||
self.dataToBeSent.replaceSubrange(range, with: Data())
|
||||
byteSent += bytesWritten
|
||||
} else if bytesWritten < 0 {
|
||||
self._error = outputStream.streamError
|
||||
return bytesWritten
|
||||
}
|
||||
if self.dataToBeSent.count == 0 {
|
||||
break
|
||||
}
|
||||
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1));
|
||||
Thread.sleep(forTimeInterval: 0.1)
|
||||
}
|
||||
self._countOfBytesSent += Int64(byteSent)
|
||||
if close {
|
||||
outputStream.close()
|
||||
self.streamDelegate?.urlSession?(self._underlyingSession, writeClosedFor: self)
|
||||
}
|
||||
return byteSent
|
||||
}
|
||||
|
||||
/* Enqueue a request to close the read side of the underlying socket.
|
||||
* All outstanding IO will complete before the read side is closed.
|
||||
* You may continue writing to the server.
|
||||
*/
|
||||
/**
|
||||
* Completes any enqueued reads and writes, and then closes the read side of the underlying socket.
|
||||
*
|
||||
* You may continue to write data using the `write(_:timeout:completionHandler:)` method after
|
||||
* calling this method. Any calls to `readData(ofMinLength:maxLength:timeout:completionHandler:)`
|
||||
* after calling this method will result in an error.
|
||||
*/
|
||||
open func closeRead() {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
_underlyingTask!.closeRead()
|
||||
} else {
|
||||
guard let inputStream = inputStream else {
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.closeRead()
|
||||
return
|
||||
}
|
||||
dispatch_queue.async {
|
||||
while inputStream.streamStatus != .atEnd {
|
||||
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1));
|
||||
Thread.sleep(forTimeInterval: 0.1)
|
||||
}
|
||||
inputStream.close()
|
||||
self.streamDelegate?.urlSession?(self._underlyingSession, readClosedFor: self)
|
||||
}
|
||||
|
||||
guard let inputStream = inputStream else {
|
||||
return
|
||||
}
|
||||
operation_queue.addOperation {
|
||||
while inputStream.streamStatus != .atEnd {
|
||||
Thread.sleep(forTimeInterval: 0.1)
|
||||
}
|
||||
inputStream.close()
|
||||
self.streamDelegate?.urlSession?(self._underlyingSession, readClosedFor: self)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Begin encrypted handshake. The hanshake begins after all pending
|
||||
* IO has completed. TLS authentication callbacks are sent to the
|
||||
* session's -URLSession:task:didReceiveChallenge:completionHandler:
|
||||
*/
|
||||
/**
|
||||
* Completes any enqueued reads and writes, and establishes a secure connection.
|
||||
*
|
||||
* Authentication callbacks are sent to the session's delegate using the
|
||||
* `urlSession(_:task:didReceive:completionHandler:)` method.
|
||||
*/
|
||||
open func startSecureConnection() {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
_underlyingTask!.startSecureConnection()
|
||||
} else {
|
||||
inputStream!.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: .socketSecurityLevelKey)
|
||||
outputStream!.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: .socketSecurityLevelKey)
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.startSecureConnection()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
operation_queue.addOperation {
|
||||
self.inputStream!.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: .socketSecurityLevelKey)
|
||||
self.outputStream!.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: .socketSecurityLevelKey)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Cleanly close a secure connection after all pending secure IO has
|
||||
* completed.
|
||||
*/
|
||||
/**
|
||||
* Completes any enqueued reads and writes, and closes the secure connection.
|
||||
*/
|
||||
open func stopSecureConnection() {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
_underlyingTask!.stopSecureConnection()
|
||||
} else {
|
||||
inputStream!.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey)
|
||||
outputStream!.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey)
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.stopSecureConnection()
|
||||
return
|
||||
}
|
||||
}
|
||||
operation_queue.addOperation {
|
||||
self.inputStream!.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey)
|
||||
self.outputStream!.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey)
|
||||
}
|
||||
}
|
||||
|
||||
open func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
|
||||
switch (eventCode) {
|
||||
case Stream.Event.errorOccurred:
|
||||
self._error = aStream.streamError as NSError?
|
||||
if eventCode.contains(.errorOccurred) {
|
||||
self._error = aStream.streamError
|
||||
streamDelegate?.urlSession?(_underlyingSession, task: self, didCompleteWithError: error)
|
||||
case Stream.Event.endEncountered:
|
||||
break
|
||||
case Stream.Event():
|
||||
break
|
||||
case Stream.Event.openCompleted:
|
||||
break
|
||||
case Stream.Event.hasBytesAvailable:
|
||||
var buffer = [UInt8](repeating: 0, count: 2048)
|
||||
if (aStream == inputStream) {
|
||||
while (inputStream!.hasBytesAvailable) {
|
||||
let len = inputStream!.read(&buffer, maxLength: buffer.count)
|
||||
if len > 0 {
|
||||
dataReceived.append(&buffer, length: len)
|
||||
self._countOfBytesRecieved += len
|
||||
}
|
||||
}
|
||||
|
||||
if aStream == inputStream && eventCode.contains(.hasBytesAvailable) {
|
||||
while (inputStream!.hasBytesAvailable) {
|
||||
var buffer = [UInt8](repeating: 0, count: 2048)
|
||||
let len = inputStream!.read(&buffer, maxLength: buffer.count)
|
||||
if len > 0 {
|
||||
dataReceived.append(&buffer, count: len)
|
||||
self._countOfBytesRecieved += Int64(len)
|
||||
}
|
||||
}
|
||||
case Stream.Event.hasSpaceAvailable:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal extension URLSession {
|
||||
/* Creates a bidirectional stream task to a given host and port.
|
||||
*/
|
||||
func fpstreamTaskWithHostName(_ hostname: String, port: Int) -> FPSStreamTask {
|
||||
return FPSStreamTask(session: self, host: hostname, port: port)
|
||||
public extension URLSession {
|
||||
/// Creates a bidirectional stream task to a given host and port.
|
||||
func fpstreamTask(withHostName hostname: String, port: Int) -> FileProviderStreamTask {
|
||||
return FileProviderStreamTask(session: self, host: hostname, port: port)
|
||||
}
|
||||
|
||||
/* Creates a bidirectional stream task with an NSNetService to identify the endpoint.
|
||||
/**
|
||||
* Creates a bidirectional stream task with an NSNetService to identify the endpoint.
|
||||
* The NSNetService will be resolved before any IO completes.
|
||||
*/
|
||||
func fpstreamTaskWithNetService(_ service: NetService) -> FPSStreamTask {
|
||||
return fpstreamTaskWithNetService(service)
|
||||
*/
|
||||
func fpstreamTask(withNetService service: NetService) -> FileProviderStreamTask {
|
||||
return FileProviderStreamTask(session: self, netService: service)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,42 +629,46 @@ internal extension URLSession {
|
||||
internal protocol FPSStreamDelegate : URLSessionTaskDelegate {
|
||||
|
||||
|
||||
/* Indiciates that the read side of a connection has been closed. Any
|
||||
/**
|
||||
* Indiciates that the read side of a connection has been closed. Any
|
||||
* outstanding reads complete, but future reads will immediately fail.
|
||||
* This may be sent even when no reads are in progress. However, when
|
||||
* this delegate message is received, there may still be bytes
|
||||
* available. You only know that no more bytes are available when you
|
||||
* are able to read until EOF. */
|
||||
@objc optional func urlSession(_ session: URLSession, readClosedFor streamTask: FPSStreamTask)
|
||||
@objc optional func urlSession(_ session: URLSession, readClosedFor streamTask: FileProviderStreamTask)
|
||||
|
||||
|
||||
/* Indiciates that the write side of a connection has been closed.
|
||||
/**
|
||||
* Indiciates that the write side of a connection has been closed.
|
||||
* Any outstanding writes complete, but future writes will immediately
|
||||
* fail.
|
||||
*/
|
||||
@objc optional func urlSession(_ session: URLSession, writeClosedFor streamTask: FPSStreamTask)
|
||||
@objc optional func urlSession(_ session: URLSession, writeClosedFor streamTask: FileProviderStreamTask)
|
||||
|
||||
|
||||
/* A notification that the system has determined that a better route
|
||||
/**
|
||||
* A notification that the system has determined that a better route
|
||||
* to the host has been detected (eg, a wi-fi interface becoming
|
||||
* available.) This is a hint to the delegate that it may be
|
||||
* desirable to create a new task for subsequent work. Note that
|
||||
* there is no guarantee that the future task will be able to connect
|
||||
* to the host, so callers should should be prepared for failure of
|
||||
* reads and writes over any new interface. */
|
||||
@objc optional func urlSession(_ session: URLSession, betterRouteDiscoveredFor streamTask: FPSStreamTask)
|
||||
@objc optional func urlSession(_ session: URLSession, betterRouteDiscoveredFor streamTask: FileProviderStreamTask)
|
||||
|
||||
|
||||
/* The given task has been completed, and unopened NSInputStream and
|
||||
/**
|
||||
* The given task has been completed, and unopened NSInputStream and
|
||||
* NSOutputStream objects are created from the underlying network
|
||||
* connection. This will only be invoked after all enqueued IO has
|
||||
* completed (including any necessary handshakes.) The streamTask
|
||||
* will not receive any further delegate messages.
|
||||
*/
|
||||
@objc optional func urlSession(_ session: URLSession, streamTask: FPSStreamTask, didBecome inputStream: InputStream, outputStream: OutputStream)
|
||||
@objc optional func urlSession(_ session: URLSession, streamTask: FileProviderStreamTask, didBecome inputStream: InputStream, outputStream: OutputStream)
|
||||
}
|
||||
|
||||
private let ports: [String: Int] = ["http": 80, "https": 443, "smb": 445,"ftp": 21,"ftps": 22, "sftp": 2121,
|
||||
private let ports: [String: Int] = ["http": 80, "https": 443, "smb": 445,"ftp": 21,
|
||||
"telnet": 23, "pop": 110, "smtp": 25, "imap": 143]
|
||||
private let securePorts: [String: Int] = ["https": 443, "smb": 445, "ftps": 22, "sftp": 2121,
|
||||
"telnet": 992, "pop": 995, "smtp": 465, "imap": 993]
|
||||
private let securePorts: [String: Int] = ["ssh": 22, "https": 443, "smb": 445, "smtp": 465,
|
||||
"ftps": 990,"telnet": 992, "imap": 993, "pop": 995]
|
||||
|
||||
@@ -0,0 +1,786 @@
|
||||
//
|
||||
// FTPFileProvider.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
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 var type: String { return "FTP" }
|
||||
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
|
||||
|
||||
/// Determine either FTP session is in passive or active mode.
|
||||
/// - Note: Due to `URLSessionStreamTask` restrictions for determining listening port,
|
||||
/// only passive sessions are available in current implementation.
|
||||
public let passiveMode = true
|
||||
|
||||
/// Force to use URLSessionDownloadTask/URLSessionDataTask when possible
|
||||
public var useAppleImplementation = true
|
||||
|
||||
fileprivate var _session: URLSession?
|
||||
internal var sessionDelegate: SessionDelegate?
|
||||
public var session: URLSession {
|
||||
get {
|
||||
if _session == nil {
|
||||
self.sessionDelegate = SessionDelegate(fileProvider: self)
|
||||
let config = URLSessionConfiguration.default
|
||||
config.urlCache = cache
|
||||
config.requestCachePolicy = .returnCacheDataElseLoad
|
||||
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: self.operation_queue)
|
||||
_session?.sessionDescription = UUID().uuidString
|
||||
initEmptySessionHandler(_session!.sessionDescription!)
|
||||
}
|
||||
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 FTP provider with given username and password.
|
||||
|
||||
- Parameter baseURL: a url with `ftp://hostaddress/` format.
|
||||
- Parameter credential: a `URLCredential` object contains user and password.
|
||||
- Parameter cache: A URLCache to cache downloaded files and contents. (unimplemented for FTP and should be nil)
|
||||
*/
|
||||
public init? (baseURL: URL, credential: URLCredential? = nil, cache: URLCache? = nil) {
|
||||
guard (baseURL.scheme ?? "ftp").lowercased().hasPrefix("ftp") else { return nil }
|
||||
guard baseURL.host != nil else { return nil }
|
||||
var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: true)!
|
||||
urlComponents.port = urlComponents.port ?? 21
|
||||
urlComponents.scheme = urlComponents.scheme ?? "ftp"
|
||||
|
||||
self.baseURL = (urlComponents.url!.path.hasSuffix("/") ? urlComponents.url! : urlComponents.url!.appendingPathComponent("")).absoluteURL
|
||||
self.currentPath = ""
|
||||
self.useCache = false
|
||||
self.validatingCache = true
|
||||
self.cache = cache
|
||||
self.credential = credential
|
||||
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: [])
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
|
||||
}
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else { return nil }
|
||||
self.init(baseURL: baseURL, credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
|
||||
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
|
||||
self.useCache = aDecoder.decodeBool(forKey: "useCache")
|
||||
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
|
||||
self.useAppleImplementation = aDecoder.decodeBool(forKey: "useAppleImplementation")
|
||||
}
|
||||
|
||||
public func encode(with aCoder: NSCoder) {
|
||||
aCoder.encode(self.baseURL, forKey: "baseURL")
|
||||
aCoder.encode(self.credential, forKey: "credential")
|
||||
aCoder.encode(self.currentPath, forKey: "currentPath")
|
||||
aCoder.encode(self.useCache, forKey: "useCache")
|
||||
aCoder.encode(self.validatingCache, forKey: "validatingCache")
|
||||
aCoder.encode(self.useAppleImplementation, forKey: "useAppleImplementation")
|
||||
}
|
||||
|
||||
public static var supportsSecureCoding: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = FTPFileProvider(baseURL: self.baseURL!, credential: self.credential, cache: self.cache)!
|
||||
copy.currentPath = self.currentPath
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy.useCache = self.useCache
|
||||
copy.validatingCache = self.validatingCache
|
||||
copy.useAppleImplementation = self.useAppleImplementation
|
||||
return copy
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let sessionuuid = _session?.sessionDescription {
|
||||
removeSessionHandler(for: sessionuuid)
|
||||
}
|
||||
|
||||
if fileProviderCancelTasksOnInvalidating {
|
||||
_session?.invalidateAndCancel()
|
||||
} else {
|
||||
_session?.finishTasksAndInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
public func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) {
|
||||
self.contentsOfDirectory(path: path, rfc3659enabled: true, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler.
|
||||
|
||||
If the directory contains no entries or an error is occured, this method will return the empty array.
|
||||
|
||||
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
|
||||
- Parameter rfc3659enabled: uses MLST command instead of old LIST to get files attributes, default is `true`.
|
||||
- Parameter completionHandler: a closure with result of directory entries or error.
|
||||
`contents`: An array of `FileObject` identifying the the directory entries.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
|
||||
open func contentsOfDirectory(path apath: String, rfc3659enabled: Bool , completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
let path = ftpPath(apath)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.ftpList(task, of: self.ftpPath(path), useMLST: rfc3659enabled, completionHandler: { (contents, error) in
|
||||
defer {
|
||||
self.ftpQuit(task)
|
||||
}
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let files: [FileObject] = contents.flatMap {
|
||||
rfc3659enabled ? self.parseMLST($0, in: path) : self.parseUnixList($0, in: path)
|
||||
}
|
||||
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(files, nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func attributesOfItem(path: String, completionHandler: @escaping ((FileObject?, Error?) -> Void)) {
|
||||
self.attributesOfItem(path: path, rfc3659enabled: true, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a `FileObject` containing the attributes of the item (file, directory, symlink, etc.) at the path in question via asynchronous completion handler.
|
||||
|
||||
If the directory contains no entries or an error is occured, this method will return the empty `FileObject`.
|
||||
|
||||
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
|
||||
- Parameter rfc3659enabled: uses MLST command instead of old LIST to get files attributes, default is true.
|
||||
- Parameter completionHandler: a closure with result of directory entries or error.
|
||||
`attributes`: A `FileObject` containing the attributes of the item.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
open func attributesOfItem(path apath: String, rfc3659enabled: Bool, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
let path = ftpPath(apath)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let command = rfc3659enabled ? "MLST \(path)" : "LIST \(path)"
|
||||
self.execute(command: command, on: task, completionHandler: { (response, error) in
|
||||
defer {
|
||||
self.ftpQuit(task)
|
||||
}
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response, response.hasPrefix("250") || (response.hasPrefix("50") && rfc3659enabled) else {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, self.throwError(path, code: URLError.badServerResponse))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if response.hasPrefix("500") {
|
||||
self.attributesOfItem(path: path, rfc3659enabled: false, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
let lines = response.components(separatedBy: "\n").flatMap { $0.isEmpty ? nil : $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
guard lines.count > 2 else {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, self.throwError(path, code: URLError.badServerResponse))
|
||||
}
|
||||
return
|
||||
}
|
||||
let file = rfc3659enabled ? self.parseMLST(lines[1], in: path) : self.parseUnixList(lines[1], in: path)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(file, nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
completionHandler(-1, 0)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}, completionHandler: {files, error in
|
||||
if let error = error {
|
||||
completionHandler([], error)
|
||||
return
|
||||
}
|
||||
|
||||
let foundFiles = files.filter { query.evaluate(with: $0.mapPredicate()) }
|
||||
completionHandler(foundFiles, nil)
|
||||
})
|
||||
}
|
||||
|
||||
public func url(of path: String?) -> URL {
|
||||
let path = (path ?? self.currentPath).trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? (path ?? self.currentPath)
|
||||
|
||||
var baseUrlComponent = URLComponents(url: self.baseURL!, resolvingAgainstBaseURL: true)
|
||||
baseUrlComponent?.user = credential?.user
|
||||
baseUrlComponent?.password = credential?.password
|
||||
return URL(string: path, relativeTo: baseUrlComponent?.url ?? baseURL) ?? baseUrlComponent?.url ?? baseURL!
|
||||
}
|
||||
|
||||
public func relativePathOf(url: URL) -> String {
|
||||
// check if url derieved from current base url
|
||||
let relativePath = url.relativePath
|
||||
if !relativePath.isEmpty, url.baseURL == self.baseURL {
|
||||
return (relativePath.removingPercentEncoding ?? relativePath).replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
}
|
||||
|
||||
if !relativePath.isEmpty, self.baseURL == self.url(of: "/") {
|
||||
return (relativePath.removingPercentEncoding ?? relativePath).replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
}
|
||||
|
||||
return relativePath.replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
}
|
||||
|
||||
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
self.attributesOfItem(path: "/") { (file, error) in
|
||||
completionHandler(file != nil)
|
||||
}
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate: FileOperationDelegate?
|
||||
}
|
||||
|
||||
extension FTPFileProvider: 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(_ 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? {
|
||||
// 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 opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let operation = 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.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)))
|
||||
}
|
||||
}, completionHandler: { (error) in
|
||||
self.ftpQuit(task)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(opType, error: error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return operation
|
||||
}
|
||||
|
||||
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 operation = RemoteOperationHandle(operationType: opType, tasks: [])
|
||||
|
||||
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)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if file?.isDirectory ?? false {
|
||||
self.dispatch_queue.async {
|
||||
let error = self.throwError(path, code: URLError.fileIsDirectory)
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(opType, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let task = self.session.downloadTask(with: self.url(of: path))
|
||||
completionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = completionHandler
|
||||
downloadCompletionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
|
||||
do {
|
||||
try FileManager.default.moveItem(at: tempURL, to: destURL)
|
||||
completionHandler?(nil)
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
}
|
||||
}
|
||||
operation.add(task: task)
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
})
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.ftpRetrieveFile(task, filePath: self.ftpPath(path), onTask: {
|
||||
operation.add(task: $0)
|
||||
}, onProgress: { recevied, totalReceived, totalSize in
|
||||
let progress = Double(totalReceived) / Double(totalSize)
|
||||
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress))
|
||||
}) { (tmpurl, error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(opType, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let tmpurl = tmpurl {
|
||||
try? FileManager.default.moveItem(at: tmpurl, to: destURL)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(nil)
|
||||
self.delegateNotify(opType, error: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return operation
|
||||
}
|
||||
}
|
||||
|
||||
extension FTPFileProvider: FileProviderReadWrite {
|
||||
public 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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if self.useAppleImplementation {
|
||||
let task = session.downloadTask(with: url(of: path))
|
||||
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
|
||||
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])
|
||||
} 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)
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
self.delegateNotify(opType, error: nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
let operation = 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(nil, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.ftpRetrieveData(task, filePath: self.ftpPath(path), from: offset, length: length, onTask: {
|
||||
operation.add(task: $0)
|
||||
}, onProgress: { recevied, totalReceived, totalSize in
|
||||
let progress = Double(totalReceived) / Double(totalSize)
|
||||
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress))
|
||||
}) { (data, error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
self.delegateNotify(opType, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let data = data {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(data, nil)
|
||||
self.delegateNotify(opType, error: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return operation
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
let operation = 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
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
}, completionHandler: { (error) in
|
||||
self.ftpQuit(task)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(opType, error: error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if overwrite {
|
||||
storeHandler()
|
||||
} else {
|
||||
self.attributesOfItem(path: path, completionHandler: { (file, erroe) in
|
||||
if file == nil {
|
||||
storeHandler()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return operation
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
to create symbolic links to locations that do not yet exist.
|
||||
Also, if the final path component is a symbolic link, that link is not followed.
|
||||
|
||||
- Note: Many servers does't support this functionality.
|
||||
|
||||
- Parameters:
|
||||
- symbolicLink: The file path at which to create the new symbolic link. The last component of the path issued as the name of the link.
|
||||
- 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)
|
||||
}
|
||||
}
|
||||
|
||||
extension FTPFileProvider: FileProvider { }
|
||||
@@ -0,0 +1,921 @@
|
||||
//
|
||||
// FTPHelper.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
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 {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
var receivedData = receivedData
|
||||
if let data = data {
|
||||
if receivedData != nil {
|
||||
receivedData!.append(data)
|
||||
} else {
|
||||
receivedData = data
|
||||
}
|
||||
}
|
||||
|
||||
if eof {
|
||||
completionHandler(receivedData, nil)
|
||||
} else {
|
||||
self.readDataUntilEOF(of: task, minLength: 0, receivedData: receivedData, timeout: timeout, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func execute(command: String, on task: FileProviderStreamTask, minLength: Int = 4, afterSend: ((_ error: Error?) -> Void)? = nil, completionHandler: @escaping (_ response: String?, _ error: Error?) -> Void) {
|
||||
let timeout = session.configuration.timeoutIntervalForRequest
|
||||
let terminalcommand = command + "\r\n"
|
||||
task.write(terminalcommand.data(using: .utf8)!, timeout: timeout) { (error) in
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
afterSend?(error)
|
||||
|
||||
if task.state == .suspended {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
task.readData(ofMinLength: minLength, maxLength: 1024, timeout: timeout) { (data, eof, error) in
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
if let data = data, let response = String(data: data, encoding: .utf8) {
|
||||
completionHandler(response.trimmingCharacters(in: CharacterSet(charactersIn: "\r\n")), nil)
|
||||
} else {
|
||||
completionHandler(nil, self.throwError("", code: URLError.cannotParseResponse))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpLogin(_ task: FileProviderStreamTask, completionHandler: @escaping (_ error: Error?) -> Void) {
|
||||
let timeout = session.configuration.timeoutIntervalForRequest
|
||||
if task.state == .suspended {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
let credential = self.credential
|
||||
|
||||
task.readData(ofMinLength: 4, maxLength: 2048, timeout: timeout) { (data, eof, error) in
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let response = String(data: data, encoding: .utf8) else {
|
||||
completionHandler(self.throwError("", code: URLError.cannotParseResponse))
|
||||
return
|
||||
}
|
||||
|
||||
guard response.hasPrefix("22") else {
|
||||
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
|
||||
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
|
||||
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
let loginHandle = {
|
||||
self.execute(command: "USER \(credential?.user ?? "anonymous")", on: task) { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
completionHandler(self.throwError("", code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
// successfully logged in
|
||||
if response.hasPrefix("23") {
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// needs password
|
||||
if response.hasPrefix("33") {
|
||||
self.execute(command: "PASS \(credential?.password ?? "fileprovider@")", on: task) { (response, error) in
|
||||
if response?.hasPrefix("2") ?? false {
|
||||
completionHandler(nil)
|
||||
} else {
|
||||
completionHandler(self.throwError("", code: URLError.userAuthenticationRequired))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
|
||||
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
|
||||
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
|
||||
self.execute(command: "AUTH TLS", on: task, minLength: 0, completionHandler: { (response, error) in
|
||||
task.startSecureConnection()
|
||||
loginHandle()
|
||||
})
|
||||
} else {
|
||||
loginHandle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpCwd(_ task: FileProviderStreamTask, to path: String, completionHandler: @escaping (_ error: Error?) -> Void) {
|
||||
self.execute(command: "CWD \(path)", on: task) { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
completionHandler(self.throwError(path, code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
// successfully logged in
|
||||
if response.hasPrefix("25") {
|
||||
completionHandler(nil)
|
||||
}
|
||||
// not logged in
|
||||
else if response.hasPrefix("55") {
|
||||
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
|
||||
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
|
||||
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpPassive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
|
||||
func trimmedNumber(_ s : String) -> String {
|
||||
let characterSet = Set("+*#0123456789".characters)
|
||||
return String(s.characters.lazy.filter(characterSet.contains))
|
||||
}
|
||||
|
||||
self.execute(command: "PASV", on: task) { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response, let destString = response.components(separatedBy: " ").flatMap({ $0 }).last else {
|
||||
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
let destArray = destString.components(separatedBy: ",").flatMap({ UInt32(trimmedNumber($0)) })
|
||||
guard destArray.count == 6 else {
|
||||
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
// first 4 elements are ip, 2 next are port, as byte
|
||||
var host = destArray.prefix(4).flatMap({ String($0) }).joined(separator: ".")
|
||||
let port = Int(destArray[4] << 8 + destArray[5])
|
||||
// IPv6 workaround
|
||||
if host == "127.555.555.555" {
|
||||
host = self.baseURL!.host!
|
||||
}
|
||||
|
||||
let passiveTask = self.session.fpstreamTask(withHostName: host, port: port)
|
||||
passiveTask.resume()
|
||||
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
|
||||
task.startSecureConnection()
|
||||
}
|
||||
completionHandler(passiveTask, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func ftpActive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
|
||||
var port: Int32 = 0
|
||||
var _activeTask: FileProviderStreamTask?
|
||||
while (_activeTask?.state ?? .suspended) == .suspended {
|
||||
port = 32000 + Int32(arc4random_uniform(16384))
|
||||
let service = NetService(domain: "", type: "_tcp.", name: "", port: port)
|
||||
_activeTask = self.session.fpstreamTask(withNetService: service)
|
||||
_activeTask?.resume()
|
||||
}
|
||||
guard let activeTask = _activeTask else { return }
|
||||
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
|
||||
task.startSecureConnection()
|
||||
}
|
||||
self.execute(command: "PORT \(port)", on: task) { (response, error) in
|
||||
if let error = error {
|
||||
activeTask.cancel()
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
guard !response.hasPrefix("5") else {
|
||||
completionHandler(nil, self.throwError("", code: URLError.cannotConnectToHost))
|
||||
return
|
||||
}
|
||||
|
||||
completionHandler(activeTask, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func ftpDataConnect(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
|
||||
if self.passiveMode {
|
||||
self.ftpPassive(task, completionHandler: completionHandler)
|
||||
} else {
|
||||
self.ftpActive(task, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
func ftpRest(_ task: FileProviderStreamTask, startPosition: Int64, completionHandler: @escaping (_ error: Error?) -> Void) {
|
||||
self.execute(command: "REST \(startPosition)", on: task) { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
// Successful
|
||||
if response?.hasPrefix("35") ?? false {
|
||||
completionHandler(nil)
|
||||
} else {
|
||||
let spaceIndex = response?.characters.index(of: "-") ?? response?.startIndex
|
||||
let code = Int((response?.substring(to: spaceIndex!).trimmingCharacters(in: .whitespacesAndNewlines))!) ?? -1
|
||||
let description = response?.substring(from: spaceIndex!).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpList(_ task: FileProviderStreamTask, of path: String, useMLST: Bool, completionHandler: @escaping (_ contents: [String], _ error: Error?) -> Void) {
|
||||
self.ftpDataConnect(task) { (dataTask, error) in
|
||||
if let error = error {
|
||||
completionHandler([], error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let dataTask = dataTask else {
|
||||
completionHandler([], self.throwError(path, code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
var success = false
|
||||
let command = useMLST ? "MLSD \(path)" : "LIST \(path)"
|
||||
self.execute(command: command, on: task, minLength: 70, afterSend: { error in
|
||||
// starting passive task
|
||||
let timeout = self.session.configuration.timeoutIntervalForRequest
|
||||
|
||||
DispatchQueue.global().async {
|
||||
var finalData = Data()
|
||||
var eof = false
|
||||
var error: Error?
|
||||
while !eof {
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
dataTask.readData(ofMinLength: 0, maxLength: 65535, timeout: timeout, completionHandler: { (data, seof, serror) in
|
||||
if let data = data {
|
||||
finalData.append(data)
|
||||
}
|
||||
eof = seof
|
||||
error = serror
|
||||
group.leave()
|
||||
})
|
||||
let waitResult = group.wait(timeout: .now() + timeout)
|
||||
|
||||
if let error = error {
|
||||
completionHandler([], error)
|
||||
return
|
||||
}
|
||||
|
||||
if waitResult == .timedOut {
|
||||
completionHandler([], self.throwError(path, code: URLError.timedOut))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
guard let response = String(data: finalData, encoding: .utf8) else {
|
||||
completionHandler([], self.throwError(path, code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
let contents = response.components(separatedBy: "\n").flatMap({ $0.trimmingCharacters(in: .whitespacesAndNewlines) })
|
||||
success = true
|
||||
completionHandler(contents, nil)
|
||||
return
|
||||
}
|
||||
}) { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler([], error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
completionHandler([], self.throwError(path, code: URLError.cannotParseResponse))
|
||||
return
|
||||
}
|
||||
|
||||
if response.hasPrefix("50") && useMLST {
|
||||
self.ftpList(task, of: path, useMLST: false, completionHandler: completionHandler)
|
||||
return
|
||||
}
|
||||
|
||||
if !success && !(response.hasPrefix("25") || response.hasPrefix("15")) {
|
||||
let spaceIndex = response.characters.index(of: " ") ?? response.startIndex
|
||||
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
|
||||
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let error = FileProviderFTPError(code: code, path: path, errorDescription: description)
|
||||
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], error)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func recursiveList(path: String, useMLST: Bool, foundItemsHandler: ((_ contents: [FileObject]) -> Void)? = nil, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
|
||||
let queue = DispatchQueue(label: "test")
|
||||
queue.async {
|
||||
let group = DispatchGroup()
|
||||
var result = [FileObject]()
|
||||
var success = true
|
||||
group.enter()
|
||||
self.contentsOfDirectory(path: path, completionHandler: { (files, error) in
|
||||
success = success && (error == nil)
|
||||
if let error = error {
|
||||
completionHandler([], error)
|
||||
group.leave()
|
||||
return
|
||||
}
|
||||
|
||||
result.append(contentsOf: files)
|
||||
foundItemsHandler?(files)
|
||||
|
||||
let directories: [FileObject] = files.filter { $0.isDirectory }
|
||||
for dir in directories {
|
||||
group.enter()
|
||||
self.recursiveList(path: dir.path, useMLST: useMLST, foundItemsHandler: foundItemsHandler, completionHandler: { (contents, error) in
|
||||
success = success && (error == nil)
|
||||
if let error = error {
|
||||
completionHandler([], error)
|
||||
group.leave()
|
||||
return
|
||||
}
|
||||
|
||||
foundItemsHandler?(files)
|
||||
result.append(contentsOf: contents)
|
||||
group.leave()
|
||||
})
|
||||
}
|
||||
group.leave()
|
||||
})
|
||||
group.wait()
|
||||
|
||||
if success {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(result, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(cachedResponse.data, nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.attributesOfItem(path: filePath) { (file, error) in
|
||||
let totalSize = file?.size ?? -1
|
||||
// Retreive data from server
|
||||
self.ftpDataConnect(task) { (dataTask, error) in
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let dataTask = dataTask else {
|
||||
completionHandler(nil, self.throwError(filePath, code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
// Send retreive command
|
||||
let len = 19 /* TYPE response */ + 65 + String(position).characters.count /* REST Response */ + 53 + filePath.characters.count + String(totalSize).characters.count /* RETR open response */ + 26 /* RETR Transfer complete message. */
|
||||
self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "RETR \(filePath)", on: task, minLength: len, afterSend: { error in
|
||||
// starting passive task
|
||||
onTask?(dataTask)
|
||||
|
||||
let timeout = self.session.configuration.timeoutIntervalForRequest
|
||||
DispatchQueue.global().async {
|
||||
var finalData = Data()
|
||||
var eof = false
|
||||
var error: Error?
|
||||
while !eof {
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
dataTask.readData(ofMinLength: 0, maxLength: 65535, timeout: timeout, completionHandler: { (data, seof, serror) in
|
||||
if let data = data {
|
||||
finalData.append(data)
|
||||
onProgress?(Int64(data.count), Int64(finalData.count), totalSize)
|
||||
}
|
||||
eof = seof || (length > 0 && finalData.count >= length)
|
||||
if length > 0 && finalData.count > length {
|
||||
finalData.count = length
|
||||
}
|
||||
error = serror
|
||||
group.leave()
|
||||
})
|
||||
let waitResult = group.wait(timeout: .now() + timeout)
|
||||
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
if waitResult == .timedOut {
|
||||
completionHandler(nil, self.throwError(filePath, code: URLError.timedOut))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? 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)
|
||||
self.cache?.storeCachedResponse(cachedResponse, for: request)
|
||||
}
|
||||
|
||||
completionHandler(finalData, nil)
|
||||
return
|
||||
}
|
||||
}) { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
completionHandler(nil, self.throwError(filePath, code: URLError.cannotParseResponse))
|
||||
return
|
||||
}
|
||||
|
||||
if !(response.hasPrefix("1") || !response.hasPrefix("2")) {
|
||||
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
|
||||
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
|
||||
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
|
||||
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpRetrieveFile(_ 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 (_ file: URL?, _ error: Error?) -> Void) {
|
||||
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 {
|
||||
dispatch_queue.async {
|
||||
do {
|
||||
try cachedResponse.data.write(to: tempURL)
|
||||
completionHandler(tempURL, nil)
|
||||
} catch {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
try? FileManager.default.removeItem(at: tempURL)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.attributesOfItem(path: filePath) { (file, error) in
|
||||
let totalSize = file?.size ?? -1
|
||||
// Retreive data from server
|
||||
self.ftpDataConnect(task) { (dataTask, error) in
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let dataTask = dataTask else {
|
||||
completionHandler(nil, self.throwError(filePath, code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
// Send retreive command
|
||||
let len = 19 /* TYPE response */ + 65 + String(position).characters.count /* REST Response */ + 53 + filePath.characters.count + String(totalSize).characters.count /* RETR open response */ + 26 /* RETR Transfer complete message. */
|
||||
self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "RETR \(filePath)", on: task, minLength: len, afterSend: { error in
|
||||
// starting passive task
|
||||
onTask?(dataTask)
|
||||
|
||||
let timeout = self.session.configuration.timeoutIntervalForRequest
|
||||
DispatchQueue.global().async {
|
||||
var finalData = Data()
|
||||
var eof = false
|
||||
var error: Error?
|
||||
while !eof {
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
dataTask.readData(ofMinLength: 0, maxLength: 65535, timeout: timeout, completionHandler: { (data, seof, serror) in
|
||||
if let data = data {
|
||||
finalData.append(data)
|
||||
onProgress?(Int64(data.count), Int64(finalData.count), totalSize)
|
||||
}
|
||||
eof = seof || (length > 0 && finalData.count >= length)
|
||||
if length > 0 && finalData.count > length {
|
||||
finalData.count = length
|
||||
}
|
||||
error = serror
|
||||
group.leave()
|
||||
})
|
||||
let waitResult = group.wait(timeout: .now() + timeout)
|
||||
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
if waitResult == .timedOut {
|
||||
error = self.throwError("", code: URLError.timedOut)
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? 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)
|
||||
self.cache?.storeCachedResponse(cachedResponse, for: request)
|
||||
}
|
||||
|
||||
self.dispatch_queue.async {
|
||||
do {
|
||||
try finalData.write(to: tempURL)
|
||||
completionHandler(tempURL, nil)
|
||||
} catch {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
try? FileManager.default.removeItem(at: tempURL)
|
||||
}
|
||||
return
|
||||
}
|
||||
}) { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
completionHandler(nil, self.throwError(filePath, code: URLError.cannotParseResponse))
|
||||
return
|
||||
}
|
||||
|
||||
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
|
||||
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
|
||||
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
|
||||
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
|
||||
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpStore(_ task: FileProviderStreamTask, filePath: String, fromData: Data?, fromFile: URL?, onTask: ((_ task: FileProviderStreamTask) -> Void)?, onProgress: ((_ bytesSent: Int64, _ totalSent: Int64, _ expectedBytes: Int64) -> Void)?, completionHandler: @escaping (_ error: Error?) -> Void) {
|
||||
let timeout = self.session.configuration.timeoutIntervalForRequest
|
||||
operation_queue.addOperation {
|
||||
guard let size: Int64 = (fromData != nil ? Int64(fromData!.count) : nil) ?? fromFile?.fileSize else { return }
|
||||
|
||||
var error: Error?
|
||||
let chunkSize: Int
|
||||
switch size {
|
||||
case 0..<262_144: chunkSize = 32_768 // 0KB To 256KB, page size is 32KB
|
||||
case 262_144..<1_048_576: chunkSize = 65_536 // 256KB To 1MB, page size is 64KB
|
||||
case 1_048_576..<10_485_760: chunkSize = 131_072 // 1MB To 10MB, page size is 128KB
|
||||
case 10_048_576..<33_554_432: chunkSize = 262_144 // 1MB To 10MB, page size is 256KB
|
||||
default: chunkSize = 524_288 // Larger than 32MB, page size is 512KB
|
||||
}
|
||||
|
||||
var fileHandle: FileHandle?
|
||||
if let file = fromFile {
|
||||
fileHandle = FileHandle(forReadingAtPath: file.path)
|
||||
}
|
||||
defer {
|
||||
fileHandle?.closeFile()
|
||||
}
|
||||
|
||||
var eof = false
|
||||
var sent: Int64 = 0
|
||||
|
||||
while !eof {
|
||||
let subdata: Data
|
||||
if let data = fromData {
|
||||
let endIndex = min(data.count, Int(sent) + chunkSize)
|
||||
eof = endIndex == data.count
|
||||
subdata = data.subdata(in: Int(sent)..<endIndex)
|
||||
}else if let fileHandle = fileHandle {
|
||||
subdata = fileHandle.readData(ofLength: chunkSize)
|
||||
eof = Int64(fileHandle.offsetInFile) == size
|
||||
} else {
|
||||
return
|
||||
}
|
||||
if subdata.count == 0 { continue }
|
||||
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
self.ftpStore(task, data: subdata, to: filePath, from: sent, onTask: onTask, completionHandler: { (serror) in
|
||||
error = serror
|
||||
sent += Int64(subdata.count)
|
||||
group.leave()
|
||||
onProgress?(Int64(subdata.count), sent, size)
|
||||
})
|
||||
let waitResult = group.wait(timeout: .now() + timeout)
|
||||
|
||||
if waitResult == .timedOut {
|
||||
error = self.throwError(filePath, code: URLError.timedOut)
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func ftpStore(_ task: FileProviderStreamTask, data: Data, to filePath: String, from position: Int64, onTask: ((_ task: FileProviderStreamTask) -> Void)?, completionHandler: @escaping (_ error: Error?) -> Void) {
|
||||
self.ftpDataConnect(task) { (dataTask, error) in
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let dataTask = dataTask else {
|
||||
completionHandler(self.throwError(filePath, code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
// Send retreive command
|
||||
var success = false
|
||||
let len = 19 /* TYPE response */ + 65 + String(position).characters.count /* REST Response */ + 44 + filePath.characters.count /* STOR open response */ + 10 /* RETR Transfer complete message. */
|
||||
self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "STOR \(filePath)", on: task, minLength: len, afterSend: { error in
|
||||
// starting passive task
|
||||
let timeout = self.session.configuration.timeoutIntervalForRequest
|
||||
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
|
||||
task.startSecureConnection()
|
||||
}
|
||||
onTask?(dataTask)
|
||||
|
||||
if data.count == 0 { return }
|
||||
|
||||
dataTask.write(data, timeout: timeout, completionHandler: { (error) in
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
success = true
|
||||
|
||||
dataTask.closeRead()
|
||||
dataTask.closeWrite()
|
||||
})
|
||||
}) { (response, error) in
|
||||
guard success else { return }
|
||||
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
completionHandler(self.throwError(filePath, code: URLError.cannotParseResponse))
|
||||
return
|
||||
}
|
||||
|
||||
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
|
||||
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
|
||||
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
|
||||
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
|
||||
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpQuit(_ task: FileProviderStreamTask) {
|
||||
self.execute(command: "QUIT", on: task) { (_, _) in
|
||||
//task.closeRead()
|
||||
//task.closeWrite()
|
||||
}
|
||||
}
|
||||
|
||||
func ftpPath(_ apath: String) -> String {
|
||||
var path = apath.isEmpty ? self.currentPath : apath
|
||||
|
||||
// path of base url should be concreted into file path!
|
||||
path = baseURL!.appendingPathComponent(path).path
|
||||
|
||||
// Fixing slashes
|
||||
if !path.hasPrefix("/") {
|
||||
path = "/" + path
|
||||
}
|
||||
if path.hasSuffix("/"){
|
||||
path.characters.removeLast()
|
||||
}
|
||||
|
||||
if path.isEmpty {
|
||||
path = "/"
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
func parseUnixList(_ text: String, in path: String) -> FileObject? {
|
||||
let gregorian = Calendar(identifier: .gregorian)
|
||||
let nearDateFormatter = DateFormatter()
|
||||
nearDateFormatter.calendar = gregorian
|
||||
nearDateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
nearDateFormatter.dateFormat = "MMM dd hh:ss yyyy"
|
||||
let farDateFormatter = DateFormatter()
|
||||
farDateFormatter.calendar = gregorian
|
||||
farDateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
farDateFormatter.dateFormat = "MMM dd yyyy"
|
||||
let thisYear = gregorian.component(.year, from: Date())
|
||||
|
||||
let components = text.components(separatedBy: " ").flatMap { $0.isEmpty ? nil : $0 }
|
||||
guard components.count >= 9 else { return nil }
|
||||
let posixPermission = components[0]
|
||||
let linksCount = Int(components[1]) ?? 0
|
||||
//let owner = components[2]
|
||||
//let groupOwner = components[3]
|
||||
let size = Int64(components[4]) ?? -1
|
||||
let date = components[5..<8].joined(separator: " ")
|
||||
let name = components[8..<components.count].joined(separator: " ")
|
||||
|
||||
guard name != "." && name != ".." else { return nil }
|
||||
var path = (path as NSString).appendingPathComponent(name)
|
||||
if path.hasPrefix("/") {
|
||||
path.characters.removeFirst()
|
||||
}
|
||||
|
||||
let file = FileObject(url: url(of: path), name: name, path: path)
|
||||
switch String(posixPermission.characters.first!) {
|
||||
case "d": file.type = .directory
|
||||
case "l": file.type = .symbolicLink
|
||||
default: file.type = .regular
|
||||
}
|
||||
file.isReadOnly = !posixPermission.contains("w")
|
||||
file.size = file.isDirectory ? -1 : size
|
||||
file.allValues[.linkCountKey] = linksCount
|
||||
|
||||
if let parsedDate = nearDateFormatter.date(from: date + " " + String(thisYear)) {
|
||||
if parsedDate > Date() {
|
||||
file.modifiedDate = gregorian.date(byAdding: .year, value: -1, to: parsedDate)
|
||||
} else {
|
||||
file.modifiedDate = parsedDate
|
||||
}
|
||||
} else if let parsedDate = farDateFormatter.date(from: date) {
|
||||
file.modifiedDate = parsedDate
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
func parseMLST(_ text: String, in path: String) -> FileObject? {
|
||||
var components = text.components(separatedBy: ";").flatMap { $0.isEmpty ? nil : $0 }
|
||||
guard components.count > 1 else { return nil }
|
||||
|
||||
let nameOrPath = components.removeLast().trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
var correctedPath: String
|
||||
let name: String
|
||||
if nameOrPath.hasPrefix("/") {
|
||||
correctedPath = nameOrPath.replacingOccurrences(of: baseURL!.path, with: "", options: .anchored)
|
||||
name = (nameOrPath as NSString).lastPathComponent
|
||||
} else {
|
||||
name = nameOrPath
|
||||
correctedPath = (path as NSString).appendingPathComponent(nameOrPath)
|
||||
}
|
||||
if correctedPath.hasPrefix("/") {
|
||||
correctedPath.characters.removeFirst()
|
||||
}
|
||||
|
||||
var attributes = [String: String]()
|
||||
for component in components {
|
||||
let keyValue = component.components(separatedBy: "=") .flatMap { $0.isEmpty ? nil : $0 }
|
||||
guard keyValue.count >= 2, !keyValue[0].isEmpty else { continue }
|
||||
attributes[keyValue[0].lowercased()] = keyValue.dropFirst().joined(separator: "=")
|
||||
}
|
||||
|
||||
let file = FileObject(url: url(of: correctedPath), name: name, path: correctedPath)
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.calendar = Calendar(identifier: .gregorian)
|
||||
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
dateFormatter.dateFormat = "yyyyMMddhhmmss"
|
||||
for (key, attribute) in attributes {
|
||||
switch key {
|
||||
case "type":
|
||||
switch attribute.lowercased() {
|
||||
case "file": file.type = .regular
|
||||
case "dir": file.type = .directory
|
||||
case "link": file.type = .symbolicLink
|
||||
case "os.unix=block": file.type = .blockSpecial
|
||||
case "cdir", "pdir": return nil // . and .. files are redundant in listing
|
||||
default: file.type = .unknown
|
||||
}
|
||||
|
||||
case "unique":
|
||||
file.allValues[.fileResourceIdentifierKey] = attribute
|
||||
|
||||
case "modify":
|
||||
file.modifiedDate = dateFormatter.date(from: attribute)
|
||||
|
||||
case "create":
|
||||
file.creationDate = dateFormatter.date(from: attribute)
|
||||
|
||||
case "perm":
|
||||
file.allValues[.isReadableKey] = attribute.contains("r") || attribute.contains("l")
|
||||
file.allValues[.isWritableKey] = attribute.contains("w") || attribute.contains("a")
|
||||
|
||||
case "size":
|
||||
file.size = Int64(attribute) ?? -1
|
||||
|
||||
case "media-type":
|
||||
file.allValues[.mimeTypeKey] = attribute
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains error code and description returned by FTP/S provider.
|
||||
public struct FileProviderFTPError: Error {
|
||||
/// HTTP status code returned for error by server.
|
||||
public let code: Int
|
||||
/// Path of file/folder casued that error
|
||||
public let path: String
|
||||
/// Contents returned by server as error description
|
||||
public let errorDescription: String?
|
||||
}
|
||||
+19
-48
@@ -17,27 +17,28 @@ open class FileObject: Equatable {
|
||||
self.allValues = allValues
|
||||
}
|
||||
|
||||
internal init(url: URL, name: String, path: String) {
|
||||
internal init(url: URL?, name: String, path: String) {
|
||||
self.allValues = [URLResourceKey: Any]()
|
||||
self.url = url
|
||||
if let url = url {
|
||||
self.url = url
|
||||
}
|
||||
self.name = name
|
||||
self.path = path
|
||||
}
|
||||
|
||||
/// url to access the resource, not supported by Dropbox provider
|
||||
@available(*, obsoleted: 1.0, renamed: "url", message: "Use url.absoluteURL instead.")
|
||||
open var absoluteURL: URL? {
|
||||
return url?.absoluteURL
|
||||
}
|
||||
|
||||
/// URL to access the resource, can be a relative URL against base URL.
|
||||
/// not supported by Dropbox provider.
|
||||
open internal(set) var url: URL? {
|
||||
open internal(set) var url: URL {
|
||||
get {
|
||||
return allValues[.fileURL] as? URL
|
||||
if let url = allValues[.fileURLKey] as? URL {
|
||||
return url
|
||||
} else {
|
||||
let path = self.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self.path
|
||||
return URL(string: path) ?? URL(string: "/")!
|
||||
}
|
||||
}
|
||||
set {
|
||||
allValues[.fileURL] = newValue
|
||||
allValues[.fileURLKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,21 +140,21 @@ open class FileObject: Equatable {
|
||||
|
||||
/// Check `FileObject` equality
|
||||
public static func ==(lhs: FileObject, rhs: FileObject) -> Bool {
|
||||
if rhs === lhs {
|
||||
if rhs === lhs {
|
||||
return true
|
||||
}
|
||||
if type(of: lhs) != type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
if let rurl = rhs.url, let lurl = lhs.url {
|
||||
if let rurl = rhs.allValues[.fileURLKey] as? URL, let lurl = lhs.allValues[.fileURLKey] as? URL {
|
||||
return rurl == lurl
|
||||
}
|
||||
return rhs.path == lhs.path && rhs.size == lhs.size && rhs.modifiedDate == lhs.modifiedDate
|
||||
}
|
||||
|
||||
internal func mapPredicate() -> [String: Any] {
|
||||
let mapDict: [URLResourceKey: String] = [.fileURL: "url", .nameKey: "name", .pathKey: "path", .fileSizeKey: "filesize", .creationDateKey: "creationDate",
|
||||
.contentModificationDateKey: "modifiedDate", .isHiddenKey: "isHidden", .isWritableKey: "isWritable", .serverDate: "serverDate", .entryTag: "entryTag", .mimeType: "mimeType"]
|
||||
let mapDict: [URLResourceKey: String] = [.fileURLKey: "url", .nameKey: "name", .pathKey: "path", .fileSizeKey: "filesize", .creationDateKey: "creationDate",
|
||||
.contentModificationDateKey: "modifiedDate", .isHiddenKey: "isHidden", .isWritableKey: "isWritable", .serverDateKey: "serverDate", .entryTagKey: "entryTag", .mimeTypeKey: "mimeType"]
|
||||
let typeDict: [URLFileResourceType: String] = [.directory: "directory", .regular: "regular", .symbolicLink: "symbolicLink", .unknown: "unknown"]
|
||||
var result = [String: Any]()
|
||||
for (key, value) in allValues {
|
||||
@@ -170,10 +171,11 @@ open class FileObject: Equatable {
|
||||
return result
|
||||
}
|
||||
|
||||
/// Converts macOS spotlight query for searching files to a query that can be used for `searchFiles()` method
|
||||
static public func convertPredicate(fromSpotlight query: NSPredicate) -> NSPredicate {
|
||||
let mapDict: [String: URLResourceKey] = [NSMetadataItemURLKey: .fileURL, NSMetadataItemFSNameKey: .nameKey, NSMetadataItemPathKey: .pathKey,
|
||||
let mapDict: [String: URLResourceKey] = [NSMetadataItemURLKey: .fileURLKey, NSMetadataItemFSNameKey: .nameKey, NSMetadataItemPathKey: .pathKey,
|
||||
NSMetadataItemFSSizeKey: .fileSizeKey, NSMetadataItemFSCreationDateKey: .creationDateKey,
|
||||
NSMetadataItemFSContentChangeDateKey: .contentModificationDateKey, "kMDItemFSInvisible": .isHiddenKey, "kMDItemFSIsWriteable": .isWritableKey, "kMDItemKind": .mimeType]
|
||||
NSMetadataItemFSContentChangeDateKey: .contentModificationDateKey, "kMDItemFSInvisible": .isHiddenKey, "kMDItemFSIsWriteable": .isWritableKey, "kMDItemKind": .mimeTypeKey]
|
||||
|
||||
if let cQuery = query as? NSCompoundPredicate {
|
||||
let newSub = cQuery.subpredicates.map { convertPredicate(fromSpotlight: $0 as! NSPredicate) }
|
||||
@@ -198,37 +200,6 @@ open class FileObject: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
internal func resolve(dateString: String) -> Date? {
|
||||
let dateFor: DateFormatter = DateFormatter()
|
||||
dateFor.locale = Locale(identifier: "en_US")
|
||||
dateFor.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"
|
||||
if let rfc3339 = dateFor.date(from: dateString) {
|
||||
return rfc3339
|
||||
}
|
||||
dateFor.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss z"
|
||||
if let rfc1123 = dateFor.date(from: dateString) {
|
||||
return rfc1123
|
||||
}
|
||||
dateFor.dateFormat = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
|
||||
if let rfc850 = dateFor.date(from: dateString) {
|
||||
return rfc850
|
||||
}
|
||||
dateFor.dateFormat = "EEE MMM d HH':'mm':'ss yyyy"
|
||||
if let asctime = dateFor.date(from: dateString) {
|
||||
return asctime
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
internal func rfc3339utc(of date:Date) -> String {
|
||||
let fm = DateFormatter()
|
||||
fm.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
|
||||
fm.timeZone = TimeZone(identifier:"UTC")
|
||||
fm.locale = Locale(identifier:"en_US_POSIX")
|
||||
return fm.string(from:date)
|
||||
}
|
||||
|
||||
/// Sorting FileObject array by given criteria, **not thread-safe**
|
||||
public struct FileObjectSorting {
|
||||
|
||||
|
||||
+85
-74
@@ -51,13 +51,13 @@ public protocol FileProviderBasic: class, NSCoding, NSSecureCoding {
|
||||
|
||||
**Example initialization:**
|
||||
````
|
||||
let credential = URLCredential(user: "user", password: "password", persistence: .forSeession)
|
||||
provider.credential = URLCredential(user: "user", password: "password", persistence: .forSeession)
|
||||
````
|
||||
|
||||
- Note: In OAuth based providers like `DropboxFileProvider` and `OneDriveFileProvider`, password is Token.
|
||||
use [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) library to fetch clientId and Token of user.
|
||||
*/
|
||||
var credential: URLCredential? { get }
|
||||
var credential: URLCredential? { get set }
|
||||
|
||||
/**
|
||||
Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler.
|
||||
@@ -66,8 +66,8 @@ public protocol FileProviderBasic: class, NSCoding, NSSecureCoding {
|
||||
|
||||
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
|
||||
- Parameter completionHandler: a closure with result of directory entries or error.
|
||||
`contents`: An array of `FileObject` identifying the the directory entries.
|
||||
`error`: Error returned by system.
|
||||
- `contents`: An array of `FileObject` identifying the the directory entries.
|
||||
- `error`: Error returned by system.
|
||||
*/
|
||||
func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void))
|
||||
|
||||
@@ -78,8 +78,8 @@ public protocol FileProviderBasic: class, NSCoding, NSSecureCoding {
|
||||
|
||||
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
|
||||
- Parameter completionHandler: a closure with result of directory entries or error.
|
||||
`attributes`: A `FileObject` containing the attributes of the item.
|
||||
`error`: Error returned by system.
|
||||
- `attributes`: A `FileObject` containing the attributes of the item.
|
||||
- `error`: Error returned by system.
|
||||
*/
|
||||
func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void))
|
||||
|
||||
@@ -132,6 +132,15 @@ public protocol FileProviderBasic: class, NSCoding, NSSecureCoding {
|
||||
*/
|
||||
func url(of path: String?) -> URL
|
||||
|
||||
|
||||
/// Returns the relative path of url, wothout percent encoding. Even if url is absolute or
|
||||
/// retrieved from another provider, it will try to resolve the url against `baseURL` of
|
||||
/// current provider. It's highly recomended to use this method for displaying purposes.
|
||||
///
|
||||
/// - Parameter url: Absolute url to file or directory.
|
||||
/// - Returns: A `String` contains relative path of url against base url.
|
||||
func relativePathOf(url: URL) -> String
|
||||
|
||||
/// Checks the connection to server or permission on local
|
||||
func isReachable(completionHandler: @escaping(_ success: Bool) -> Void)
|
||||
}
|
||||
@@ -195,7 +204,13 @@ public protocol FileProviderBasicRemote: FileProviderBasic {
|
||||
var validatingCache: Bool { get set }
|
||||
}
|
||||
|
||||
internal extension FileProviderBasicRemote {
|
||||
internal protocol FileProviderBasicRemoteInternal: FileProviderBasic {
|
||||
var completionHandlersForTasks: [Int: SimpleCompletionHandler] { get set }
|
||||
var downloadCompletionHandlersForTasks: [Int: (URL) -> Void] { get set }
|
||||
var dataCompletionHandlersForTasks: [Int: (Data) -> Void] { get set }
|
||||
}
|
||||
|
||||
internal extension FileProviderBasicRemote {
|
||||
func returnCachedDate(with request: URLRequest, validatingCache: Bool, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) -> Bool {
|
||||
guard let cache = self.cache else { return false }
|
||||
if let response = cache.cachedResponse(for: request) {
|
||||
@@ -261,20 +276,6 @@ public protocol FileProviderOperations: FileProviderBasic {
|
||||
@discardableResult
|
||||
func create(folder: String, at: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Creates an new file with data passed to method asynchronously.
|
||||
Returns error via completionHandler if file is already exists.
|
||||
|
||||
- Parameters:
|
||||
- file: New file name with extension separated by period.
|
||||
- at: Parent path of new file.
|
||||
- data: Data of new files. Pass nil or `Data()` to create empty file.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Moves a file or directory from `path` to designated path asynchronously.
|
||||
When you want move a file, destination path should also consists of file name.
|
||||
@@ -349,6 +350,8 @@ public protocol FileProviderOperations: FileProviderBasic {
|
||||
Uploads a file from local file url to designated path asynchronously.
|
||||
Method will fail if source is not a local url with `file://` scheme.
|
||||
|
||||
- Note: It's safe to assume that this method only works on individual files and **won't** copy folders recursively.
|
||||
|
||||
- Parameters:
|
||||
- localFile: a file url to file.
|
||||
- to: destination path of file, including file/directory name.
|
||||
@@ -362,6 +365,8 @@ public protocol FileProviderOperations: FileProviderBasic {
|
||||
Uploads a file from local file url to designated path asynchronously.
|
||||
Method will fail if source is not a local url with `file://` scheme.
|
||||
|
||||
- Note: It's safe to assume that this method only works on individual files and **won't** copy folders recursively.
|
||||
|
||||
- Parameters:
|
||||
- localFile: a file url to file.
|
||||
- to: destination path of file, including file/directory name.
|
||||
@@ -376,6 +381,8 @@ public protocol FileProviderOperations: FileProviderBasic {
|
||||
Download a file from `path` to designated local file url asynchronously.
|
||||
Method will fail if destination is not a local url with `file://` scheme.
|
||||
|
||||
- Note: It's safe to assume that this method only works on individual files and **won't** copy folders recursively.
|
||||
|
||||
- Parameters:
|
||||
- path: original file or directory path.
|
||||
- toLocalURL: destination local url of file, including file/directory name.
|
||||
@@ -386,7 +393,15 @@ public protocol FileProviderOperations: FileProviderBasic {
|
||||
func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
}
|
||||
|
||||
extension FileProviderOperations {
|
||||
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? {
|
||||
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? {
|
||||
return self.moveItem(path: path, to: to, overwrite: false, completionHandler: completionHandler)
|
||||
@@ -412,8 +427,8 @@ public protocol FileProviderReadWrite: FileProviderBasic {
|
||||
- Parameters:
|
||||
- path: Path of file.
|
||||
- completionHandler: a closure with result of file contents or error.
|
||||
`contents`: contents of file in a `Data` object.
|
||||
`error`: Error returned by system.
|
||||
- `contents`: contents of file in a `Data` object.
|
||||
- `error`: Error returned by system.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
@@ -428,8 +443,8 @@ public protocol FileProviderReadWrite: FileProviderBasic {
|
||||
- offset: First byte index which should be read. **Starts from 0.**
|
||||
- length: Bytes count of data. Pass `-1` to read until the end of file.
|
||||
- completionHandler: a closure with result of file contents or error.
|
||||
`contents`: contents of file in a `Data` object.
|
||||
`error`: Error returned by system.
|
||||
- `contents`: contents of file in a `Data` object.
|
||||
- `error`: Error returned by system.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
@@ -442,12 +457,12 @@ public protocol FileProviderReadWrite: FileProviderBasic {
|
||||
|
||||
- Parameters:
|
||||
- path: Path of target file.
|
||||
- contents: Data to be written into 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`.
|
||||
*/
|
||||
@discardableResult
|
||||
func writeContents(path: String, contents: Data, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
func writeContents(path: String, contents: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Write the contents of the `Data` to a location asynchronously.
|
||||
@@ -455,13 +470,13 @@ public protocol FileProviderReadWrite: FileProviderBasic {
|
||||
|
||||
- Parameters:
|
||||
- path: Path of target file.
|
||||
- contents: Data to be written into file.
|
||||
- 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`.
|
||||
*/
|
||||
@discardableResult
|
||||
func writeContents(path: String, contents: Data, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
func writeContents(path: String, contents: Data?, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Write the contents of the `Data` to a location asynchronously.
|
||||
@@ -469,27 +484,27 @@ public protocol FileProviderReadWrite: FileProviderBasic {
|
||||
|
||||
- Parameters:
|
||||
- path: Path of target file.
|
||||
- contents: Data to be written into file.
|
||||
- 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`.
|
||||
*/
|
||||
@discardableResult
|
||||
func writeContents(path: String, contents: Data, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
func writeContents(path: String, contents: Data?, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Write the contents of the `Data` to a location asynchronously.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of target file.
|
||||
- contents: Data to be written into file.
|
||||
- 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`.
|
||||
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func writeContents(path: String, contents: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
func writeContents(path: String, contents: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
}
|
||||
|
||||
extension FileProviderReadWrite {
|
||||
@@ -499,17 +514,17 @@ extension FileProviderReadWrite {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func writeContents(path: String, contents: Data, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
public func writeContents(path: String, contents: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
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) -> OperationHandle? {
|
||||
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) -> OperationHandle? {
|
||||
return self.writeContents(path: path, contents: contents, atomically: false, overwrite: overwrite, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
@@ -606,11 +621,6 @@ extension FileProviderBasic {
|
||||
return type(of: self).type
|
||||
}
|
||||
|
||||
/// **OBSOLETED** This property never worked as expected and is redundant as only supported by `LocalFileProvider`.
|
||||
/// To simulate `false` value, assign `URL(fileURLWithPath: "/")` to `baseURL`.
|
||||
@available(*, obsoleted: 1.0, message: "Redundant property, now is always true.")
|
||||
var isPathRelative: Bool { return true }
|
||||
|
||||
public func url(of path: String? = nil) -> URL {
|
||||
var rpath: String = path ?? self.currentPath
|
||||
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath
|
||||
@@ -624,26 +634,29 @@ extension FileProviderBasic {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Returns the relative path of url, wothout percent encoding. Even if url is absolute or
|
||||
/// retrieved from another provider, it will try to resolve the url against `baseURL` of
|
||||
/// current provider. It's highly recomended to use this method for displaying purposes.
|
||||
///
|
||||
/// - Parameter url: Absolute url to file or directory.
|
||||
/// - Returns: A `String` contains relative path of url against base url.
|
||||
public func relativePathOf(url: URL) -> String {
|
||||
// check if url derieved from current base url
|
||||
let relativePath = url.relativePath
|
||||
if !relativePath.isEmpty, url.baseURL == self.baseURL {
|
||||
return relativePath.removingPercentEncoding ?? relativePath
|
||||
return (relativePath.removingPercentEncoding ?? relativePath).replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
}
|
||||
|
||||
// resolve url string against baseurl
|
||||
guard let baseURL = self.baseURL?.standardizedFileURL else { return url.absoluteString }
|
||||
let standardPath = url.absoluteString.replacingOccurrences(of: "file:///private/var/", with: "file:///var/", options: .anchored)
|
||||
let standardBase = baseURL.absoluteString.replacingOccurrences(of: "file:///private/var/", with: "file:///var/", options: .anchored)
|
||||
let standardRelativePath = standardPath.replacingOccurrences(of: standardBase, with: "/")
|
||||
return standardRelativePath.removingPercentEncoding ?? standardRelativePath
|
||||
if baseURL?.isFileURL ?? false {
|
||||
guard let baseURL = self.baseURL?.standardizedFileURL else { return url.absoluteString }
|
||||
let standardPath = url.absoluteString.replacingOccurrences(of: "file:///private/var/", with: "file:///var/", options: .anchored)
|
||||
let standardBase = baseURL.absoluteString.replacingOccurrences(of: "file:///private/var/", with: "file:///var/", options: .anchored)
|
||||
let standardRelativePath = standardPath.replacingOccurrences(of: standardBase, with: "/").replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
return standardRelativePath.removingPercentEncoding ?? standardRelativePath
|
||||
} else {
|
||||
guard let baseURL = self.baseURL else { return url.absoluteString }
|
||||
let standardRelativePath = url.absoluteString.replacingOccurrences(of: baseURL.absoluteString, with: "/").replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
if URLComponents(string: standardRelativePath)?.host?.isEmpty ?? true {
|
||||
return standardRelativePath.removingPercentEncoding ?? standardRelativePath
|
||||
} else {
|
||||
return relativePath.replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal func correctPath(_ path: String?) -> String? {
|
||||
@@ -678,7 +691,7 @@ extension FileProviderBasic {
|
||||
}
|
||||
var i = number ?? 2
|
||||
let similiar = contents.map {
|
||||
$0.url?.lastPathComponent ?? $0.name
|
||||
$0.url.lastPathComponent.isEmpty ? $0.name : $0.url.lastPathComponent
|
||||
}.filter {
|
||||
$0.hasPrefix(result)
|
||||
}
|
||||
@@ -732,8 +745,8 @@ public protocol ExtendedFileProvider: FileProviderBasic {
|
||||
- Parameters:
|
||||
- path: path of file.
|
||||
- completionHandler: a closure with result of preview image or error.
|
||||
`image`: `NSImage`/`UIImage` object contains preview.
|
||||
`error`: Error returned by system.
|
||||
- `image`: `NSImage`/`UIImage` object contains preview.
|
||||
- `error`: Error returned by system.
|
||||
*/
|
||||
func thumbnailOfFile(path: String, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void))
|
||||
|
||||
@@ -748,8 +761,8 @@ public protocol ExtendedFileProvider: FileProviderBasic {
|
||||
- path: path of file.
|
||||
- dimension: width and height of result preview image.
|
||||
- completionHandler: a closure with result of preview image or error.
|
||||
`image`: `NSImage`/`UIImage` object contains preview.
|
||||
`error`: Error returned by system.
|
||||
- `image`: `NSImage`/`UIImage` object contains preview.
|
||||
- `error`: Error returned by system.
|
||||
*/
|
||||
func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void))
|
||||
|
||||
@@ -763,9 +776,9 @@ public protocol ExtendedFileProvider: FileProviderBasic {
|
||||
- Parameters:
|
||||
- path: path of file.
|
||||
- completionHandler: a closure with result of preview image or error.
|
||||
`propertiesDictionary`: A `Dictionary` of proprty keys and values.
|
||||
`keys`: An `Array` contains ordering of keys.
|
||||
`error`: Error returned by system.
|
||||
- `propertiesDictionary`: A `Dictionary` of proprty keys and values.
|
||||
- `keys`: An `Array` contains ordering of keys.
|
||||
- `error`: Error returned by system.
|
||||
*/
|
||||
func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void))
|
||||
}
|
||||
@@ -829,19 +842,20 @@ extension ExtendedFileProvider {
|
||||
return resultingImage
|
||||
#else
|
||||
let ppp = Int(UIScreen.main.scale) // fetch device is retina or not
|
||||
guard let context = UIGraphicsGetCurrentContext() else {
|
||||
return nil
|
||||
}
|
||||
size.width *= CGFloat(ppp)
|
||||
size.height *= CGFloat(ppp)
|
||||
UIGraphicsBeginImageContext(size)
|
||||
|
||||
guard let context = UIGraphicsGetCurrentContext() else {
|
||||
return nil
|
||||
}
|
||||
context.saveGState()
|
||||
let transform = pdfPage.getDrawingTransform(CGPDFBox.mediaBox, rect: rect, rotate: 0, preserveAspectRatio: true)
|
||||
context.concatenate(transform)
|
||||
|
||||
context.translateBy(x: 0, y: size.height)
|
||||
context.scaleBy(x: CGFloat(ppp), y: CGFloat(-ppp))
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fill(rect)
|
||||
context.drawPDFPage(pdfPage)
|
||||
|
||||
context.restoreGState()
|
||||
@@ -938,6 +952,8 @@ public enum FileOperationType: CustomStringConvertible {
|
||||
}
|
||||
let dest = json["dest"] as? String
|
||||
switch type {
|
||||
case "Fetch":
|
||||
self = .fetch(path: source)
|
||||
case "Create":
|
||||
self = .create(path: source)
|
||||
case "Modify":
|
||||
@@ -1021,16 +1037,11 @@ public protocol FileOperationDelegate: class {
|
||||
func fileProvider(_ fileProvider: FileProviderOperations, shouldProceedAfterError error: Error, operation: FileOperationType) -> Bool
|
||||
}
|
||||
|
||||
internal class Weak<T: AnyObject> {
|
||||
weak var value : T?
|
||||
init (_ value: T) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
/// For internal use in `FileProvider` framework
|
||||
public protocol FoundationErrorEnum {
|
||||
/// Init from error code
|
||||
init? (rawValue: Int)
|
||||
// Raw error code
|
||||
var rawValue: Int { get }
|
||||
}
|
||||
|
||||
|
||||
@@ -38,10 +38,19 @@ extension URLFileResourceType {
|
||||
}
|
||||
|
||||
internal extension URLResourceKey {
|
||||
static let fileURL = URLResourceKey(rawValue: "NSURLFileURLKey")
|
||||
static let serverDate = URLResourceKey(rawValue: "NSURLServerDateKey")
|
||||
static let entryTag = URLResourceKey(rawValue: "NSURLEntryTagKey")
|
||||
static let mimeType = URLResourceKey(rawValue: "NSURLMIMETypeIdentifierKey")
|
||||
static let fileURLKey = URLResourceKey(rawValue: "NSURLFileURLKey")
|
||||
static let serverDateKey = URLResourceKey(rawValue: "NSURLServerDateKey")
|
||||
static let entryTagKey = URLResourceKey(rawValue: "NSURLEntryTagKey")
|
||||
static let mimeTypeKey = URLResourceKey(rawValue: "NSURLMIMETypeIdentifierKey")
|
||||
|
||||
@available(*, deprecated, renamed: "fileURLKey")
|
||||
static let fileURL = fileURLKey
|
||||
@available(*, deprecated, renamed: "serverDateKey")
|
||||
static let serverDate = serverDateKey
|
||||
@available(*, deprecated, renamed: "entryTagKey")
|
||||
static let entryTag = entryTagKey
|
||||
@available(*, deprecated, renamed: "mimeTypeKey")
|
||||
static let mimeType = mimeTypeKey
|
||||
}
|
||||
|
||||
internal extension URL {
|
||||
@@ -151,6 +160,43 @@ internal extension TimeInterval {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
@@ -21,7 +21,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open internal(set) var credential: URLCredential?
|
||||
open var credential: URLCredential?
|
||||
|
||||
/// Underlying `FileManager` object for listing and metadata fetching.
|
||||
open private(set) var fileManager = FileManager()
|
||||
@@ -70,7 +70,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
return nil
|
||||
}
|
||||
|
||||
var finalBaseURL = baseURL
|
||||
var finalBaseURL = baseURL.absoluteURL
|
||||
|
||||
switch directory {
|
||||
case .documentDirectory:
|
||||
@@ -116,7 +116,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
return nil
|
||||
}
|
||||
self.init(baseURL: baseURL)
|
||||
self.currentPath = aDecoder.decodeObject(of: NSString.self, forKey: "currentPath") as? String ?? ""
|
||||
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
|
||||
self.isCoorinating = aDecoder.decodeBool(forKey: "isCoorinating")
|
||||
}
|
||||
|
||||
@@ -140,12 +140,6 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
return copy
|
||||
}
|
||||
|
||||
/// **OBSOLETED:** No longer is in use and overriding this method has no effect anymore.
|
||||
@available(*, obsoleted: 1.0, message: "Overriding this method has no effect anymore.")
|
||||
open class func defaultBaseURL() -> URL {
|
||||
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
do {
|
||||
@@ -206,15 +200,6 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let fileName = fileName.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
let path = (atPath as NSString).appendingPathComponent(fileName)
|
||||
let opType = FileOperationType.create(path: path)
|
||||
|
||||
return self.doOperation(opType, data: data, atomically: true, 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)
|
||||
@@ -492,9 +477,10 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.modify(path: path)
|
||||
return self.doOperation(opType, data: data, atomically: atomically, completionHandler: completionHandler)
|
||||
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)
|
||||
}
|
||||
|
||||
fileprivate var monitors = [LocalFolderMonitor]()
|
||||
@@ -532,13 +518,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
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 destURL, making it possible
|
||||
to create symbolic links to locations that do not yet exist.
|
||||
Also, if the final path component in url is a symbolic link, that link is not followed.
|
||||
This method does not traverse symbolic links contained in destination path, making it possible
|
||||
to create symbolic links to locations that do not yet exist.
|
||||
Also, if the final path component is a symbolic link, that link is not followed.
|
||||
|
||||
- Parameters:
|
||||
- path: The file path at which to create the new symbolic link. The last component of the path issued as the name of the link.
|
||||
- destPath: The path that contains the item to be pointed to by the link. In other words, this is the destination of the link.
|
||||
- symbolicLink: The file path at which to create the new symbolic link. The last component of the path issued as the name of the link.
|
||||
- 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) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
|
||||
/// Containts path, url and attributes of a local file or resource.
|
||||
public final class LocalFileObject: FileObject {
|
||||
internal override init(url: URL, name: String, path: String) {
|
||||
internal override init(url: URL?, name: String, path: String) {
|
||||
super.init(url: url, name: name, path: path)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ public final class LocalFileObject: FileObject {
|
||||
if #available(iOS 9.0, macOS 10.11, tvOS 9.0, *) {
|
||||
fileURL = URL(fileURLWithPath: rpath, relativeTo: relativeURL)
|
||||
} else {
|
||||
fileURL = URL(string: rpath.isEmpty ? "./" : rpath, relativeTo: relativeURL)
|
||||
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath
|
||||
fileURL = URL(string: rpath, relativeTo: relativeURL) ?? relativeURL
|
||||
}
|
||||
|
||||
if let fileURL = fileURL {
|
||||
@@ -37,7 +38,7 @@ public final class LocalFileObject: FileObject {
|
||||
/// Initiates a `LocalFileObject` with attributes of file in url.
|
||||
public convenience init?(fileWithURL fileURL: URL) {
|
||||
do {
|
||||
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey, .generationIdentifierKey, .documentIdentifierKey])
|
||||
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .totalFileSizeKey, .fileAllocatedSizeKey, .totalFileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey, .generationIdentifierKey, .documentIdentifierKey])
|
||||
let path = fileURL.relativePath.hasPrefix("/") ? fileURL.relativePath : "/" + fileURL.relativePath
|
||||
|
||||
self.init(url: fileURL, name: values.name ?? fileURL.lastPathComponent, path: path)
|
||||
@@ -216,9 +217,11 @@ internal class LocalFileProviderManagerDelegate: NSObject, FileManagerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// Local operation handling is limited. Please don't use as much as possible.
|
||||
/// - 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?) {
|
||||
|
||||
@@ -33,7 +33,11 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
|
||||
}
|
||||
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open let credential: URLCredential?
|
||||
open var credential: URLCredential? {
|
||||
didSet {
|
||||
sessionDelegate?.credential = self.credential
|
||||
}
|
||||
}
|
||||
open private(set) var cache: URLCache?
|
||||
public var useCache: Bool
|
||||
public var validatingCache: Bool
|
||||
@@ -41,16 +45,30 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
|
||||
fileprivate var _session: URLSession?
|
||||
fileprivate var sessionDelegate: SessionDelegate?
|
||||
public var session: URLSession {
|
||||
if _session == nil {
|
||||
self.sessionDelegate = SessionDelegate(fileProvider: self, credential: credential)
|
||||
let queue = OperationQueue()
|
||||
//queue.underlyingQueue = dispatch_queue
|
||||
let config = URLSessionConfiguration.default
|
||||
config.urlCache = cache
|
||||
config.requestCachePolicy = .returnCacheDataElseLoad
|
||||
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: queue)
|
||||
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!)
|
||||
}
|
||||
return _session!
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,7 +86,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
|
||||
- cache: A URLCache to cache downloaded files and contents.
|
||||
*/
|
||||
public init(credential: URLCredential?, serverURL: URL? = nil, drive: String = "root", cache: URLCache? = nil) {
|
||||
let baseURL = serverURL ?? URL(string: "https://api.onedrive.com/")!
|
||||
let baseURL = serverURL?.absoluteURL ?? URL(string: "https://api.onedrive.com/")!
|
||||
self.baseURL = baseURL.path.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
|
||||
self.drive = drive
|
||||
self.currentPath = ""
|
||||
@@ -114,6 +132,10 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let sessionuuid = _session?.sessionDescription {
|
||||
removeSessionHandler(for: sessionuuid)
|
||||
}
|
||||
|
||||
if fileProviderCancelTasksOnInvalidating {
|
||||
_session?.invalidateAndCancel()
|
||||
} else {
|
||||
@@ -225,11 +247,6 @@ extension OneDriveFileProvider: FileProviderOperations {
|
||||
return doOperation(.create(path: path), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let filePath = (path as NSString).appendingPathComponent(fileName)
|
||||
return self.writeContents(path: filePath, contents: data ?? Data(), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
@@ -284,6 +301,14 @@ extension OneDriveFileProvider: FileProviderOperations {
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
@@ -297,23 +322,24 @@ extension OneDriveFileProvider: FileProviderOperations {
|
||||
return nil
|
||||
}
|
||||
var request = URLRequest(url: self.url(of: path, modifier: "content"))
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
let task = session.downloadTask(with: request, completionHandler: { (cacheURL, response, error) in
|
||||
guard let cacheURL = cacheURL, let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: (response as? HTTPURLResponse)?.statusCode ?? -1)
|
||||
let errorData : Data? = nil //Data(contentsOf:cacheURL) // TODO: Figure out how to get error response data for the error description
|
||||
let 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 ?? error)
|
||||
completionHandler?(serverError)
|
||||
return
|
||||
}
|
||||
do {
|
||||
try FileManager.default.moveItem(at: cacheURL, to: destURL)
|
||||
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])
|
||||
@@ -334,30 +360,41 @@ extension OneDriveFileProvider: FileProviderReadWrite {
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
if length > 0 {
|
||||
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
|
||||
request.setValue("bytes=\(offset)-\(offset + Int64(length) - 1)", forHTTPHeaderField: "Range")
|
||||
} else if offset > 0 && length < 0 {
|
||||
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderOneDriveError?
|
||||
if let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: httpResponse.statusCode) {
|
||||
serverError = FileProviderOneDriveError(code: code, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
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
|
||||
}
|
||||
let filedata = serverError ?? error == nil ? data : nil
|
||||
completionHandler(filedata, serverError ?? error)
|
||||
})
|
||||
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? {
|
||||
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, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
|
||||
return upload_simple(path, data: data ?? Data(), overwrite: overwrite, operation: opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
|
||||
@@ -18,11 +18,11 @@ 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
|
||||
if path.hasPrefix("/") {
|
||||
var rpath = path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? path
|
||||
if rpath.hasPrefix("/") {
|
||||
rpath.remove(at: rpath.startIndex)
|
||||
}
|
||||
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: path)!
|
||||
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: rpath)!
|
||||
super.init(url: url, name: name, path: path)
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ public final class OneDriveFileObject: FileObject {
|
||||
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 = resolve(dateString: json["lastModifiedDateTime"] as? String ?? "")
|
||||
self.creationDate = resolve(dateString: json["createdDateTime"] as? String ?? "")
|
||||
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.id = json["id"] as? String
|
||||
self.entryTag = json["eTag"] as? String
|
||||
@@ -61,20 +61,20 @@ public final class OneDriveFileObject: FileObject {
|
||||
/// MIME type of file contents returned by OneDrive server.
|
||||
open internal(set) var contentType: String {
|
||||
get {
|
||||
return allValues[.mimeType] as? String ?? ""
|
||||
return allValues[.mimeTypeKey] as? String ?? ""
|
||||
}
|
||||
set {
|
||||
allValues[.mimeType] = newValue
|
||||
allValues[.mimeTypeKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP E-Tag, can be used to mark changed files.
|
||||
open internal(set) var entryTag: String? {
|
||||
get {
|
||||
return allValues[.entryTag] as? String
|
||||
return allValues[.entryTagKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues[.entryTag] = newValue
|
||||
allValues[.entryTagKey] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,35 +113,8 @@ internal extension OneDriveFileProvider {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func upload_simple(_ targetPath: String, data: Data, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
if data.count > 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.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = data
|
||||
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderOneDriveError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(operation, error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: operation, tasks: [task])
|
||||
}
|
||||
|
||||
func upload_simple(_ targetPath: String, localFile: URL, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let size = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1
|
||||
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)
|
||||
@@ -154,14 +127,24 @@ internal extension OneDriveFileProvider {
|
||||
request.httpMethod = "PUT"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||||
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
|
||||
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: FileProviderOneDriveError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
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(operation, error: responseError ?? error)
|
||||
})
|
||||
self?.delegateNotify(.create(path: targetPath), error: responseError ?? error)
|
||||
}
|
||||
task.taskDescription = operation.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: operation, tasks: [task])
|
||||
@@ -240,14 +223,14 @@ internal extension OneDriveFileProvider {
|
||||
if let location = json["location"] as? [String: Any], let latitude = location["latitude"] as? Double, let longitude = location["longitude"] as? Double {
|
||||
OneDriveFileProvider.decimalFormatter.numberStyle = .decimal
|
||||
OneDriveFileProvider.decimalFormatter.maximumFractionDigits = 5
|
||||
let latStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: latitude))
|
||||
let longStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))
|
||||
let latStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: latitude))!
|
||||
let longStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))!
|
||||
add(key: "Location", value: "\(latStr), \(longStr)")
|
||||
}
|
||||
if let parent = json["image"] as? [String: Any] ?? json["video"] as? [String: Any], let duration = parent["duration"] as? UInt64 {
|
||||
add(key: "Duration", value: (TimeInterval(duration) / 1000).formatshort)
|
||||
}
|
||||
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = resolve(dateString: timeTakenStr) {
|
||||
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = Date(rfcString: timeTakenStr) {
|
||||
OneDriveFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
add(key: "Date taken", value: OneDriveFileProvider.dateFormatter.string(from: timeTaken))
|
||||
}
|
||||
|
||||
+115
-28
@@ -9,41 +9,48 @@
|
||||
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: [Weak<URLSessionTask>]
|
||||
internal var tasks: [URLSessionTask]
|
||||
|
||||
open private(set) var operationType: FileOperationType
|
||||
|
||||
init(operationType: FileOperationType, tasks: [URLSessionTask]) {
|
||||
self.operationType = operationType
|
||||
self.tasks = tasks.map { Weak<URLSessionTask>($0) }
|
||||
self.tasks = tasks
|
||||
}
|
||||
|
||||
internal func add(task: URLSessionTask) {
|
||||
tasks.append(Weak<URLSessionTask>(task))
|
||||
tasks.append(task)
|
||||
}
|
||||
|
||||
private func reape() {
|
||||
self.tasks = tasks.filter { $0.value != nil }
|
||||
internal func reape() {
|
||||
self.tasks = tasks.filter { $0.state != .completed }
|
||||
}
|
||||
|
||||
open var bytesSoFar: Int64 {
|
||||
return tasks.reduce(0) {
|
||||
if let task = $1.value as? URLSessionUploadTask {
|
||||
switch $1 {
|
||||
case let task as URLSessionUploadTask:
|
||||
return $0 + task.countOfBytesSent
|
||||
} else {
|
||||
return $0 + ($1.value?.countOfBytesReceived ?? 0)
|
||||
case let task as FileProviderStreamTask:
|
||||
return $0 + task.countOfBytesSent + task.countOfBytesReceived
|
||||
default:
|
||||
return $0 + $1.countOfBytesReceived
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open var totalBytes: Int64 {
|
||||
return tasks.reduce(0) {
|
||||
if let task = $1.value as? URLSessionUploadTask {
|
||||
switch $1 {
|
||||
case let task as URLSessionUploadTask:
|
||||
return $0 + task.countOfBytesExpectedToSend
|
||||
} else {
|
||||
return $0 + ($1.value?.countOfBytesExpectedToReceive ?? 0)
|
||||
case let task as FileProviderStreamTask:
|
||||
return $0 + task.countOfBytesExpectedToSend + task.countOfBytesExpectedToReceive
|
||||
default:
|
||||
return $0 + $1.countOfBytesExpectedToReceive
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,14 +58,14 @@ open class RemoteOperationHandle: OperationHandle {
|
||||
open func cancel() -> Bool {
|
||||
var canceled = false
|
||||
for taskbox in tasks {
|
||||
taskbox.value?.cancel()
|
||||
taskbox.cancel()
|
||||
canceled = true
|
||||
}
|
||||
return canceled
|
||||
}
|
||||
|
||||
open var inProgress: Bool {
|
||||
return tasks.reduce(false) { $0 || $1.value?.state ?? .canceling == .running }
|
||||
return tasks.reduce(false) { $0 || $1.state == .running }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,27 +90,89 @@ extension FileProviderHTTPError {
|
||||
}
|
||||
}
|
||||
|
||||
class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDelegate {
|
||||
internal var completionHandlersForTasks = [String: [Int: SimpleCompletionHandler]]()
|
||||
internal var downloadCompletionHandlersForTasks = [String: [Int: (URL) -> Void]]()
|
||||
internal var dataCompletionHandlersForTasks = [String: [Int: (Data) -> Void]]()
|
||||
|
||||
internal func initEmptySessionHandler(_ uuid: String) {
|
||||
completionHandlersForTasks[uuid] = [:]
|
||||
downloadCompletionHandlersForTasks[uuid] = [:]
|
||||
dataCompletionHandlersForTasks[uuid] = [:]
|
||||
}
|
||||
|
||||
internal func removeSessionHandler(for uuid: String) {
|
||||
_ = completionHandlersForTasks.removeValue(forKey: uuid)
|
||||
_ = downloadCompletionHandlersForTasks.removeValue(forKey: uuid)
|
||||
_ = dataCompletionHandlersForTasks.removeValue(forKey: uuid)
|
||||
}
|
||||
|
||||
/// All objects set to `FileProviderRemote.session` must be an instance of this class
|
||||
final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDelegate, URLSessionStreamDelegate {
|
||||
|
||||
weak var fileProvider: (FileProviderBasicRemote & FileProviderOperations)?
|
||||
var credential: URLCredential?
|
||||
|
||||
var finishDownloadHandler: ((_ session: Foundation.URLSession, _ downloadTask: URLSessionDownloadTask, _ didFinishDownloadingToURL: URL) -> Void)?
|
||||
var didSendDataHandler: ((_ session: Foundation.URLSession, _ task: URLSessionTask, _ bytesSent: Int64, _ totalBytesSent: Int64, _ totalBytesExpectedToSend: Int64) -> Void)?
|
||||
var didReceivedData: ((_ session: Foundation.URLSession, _ downloadTask: URLSessionDownloadTask, _ bytesWritten: Int64, _ totalBytesWritten: Int64, _ totalBytesExpectedToWrite: Int64) -> Void)?
|
||||
/// Forwardng URLSessionDownloadTaskDelegate call
|
||||
public var finishDownloadHandler: ((_ session: URLSession, _ downloadTask: URLSessionDownloadTask, _ didFinishDownloadingToURL: URL) -> Void)?
|
||||
/// Forwardng URLSessionTaskDelegate call
|
||||
public var didSendDataHandler: ((_ session: URLSession, _ task: URLSessionTask, _ bytesSent: Int64, _ totalBytesSent: Int64, _ totalBytesExpectedToSend: Int64) -> Void)?
|
||||
/// Forwardng URLSessionDownloadTaskDelegate call
|
||||
public var didReceivedData: ((_ session: URLSession, _ downloadTask: URLSessionDownloadTask, _ bytesWritten: Int64, _ totalBytesWritten: Int64, _ totalBytesExpectedToWrite: Int64) -> Void)?
|
||||
/// Forwardng URLSessionStreamTaskDelegate call
|
||||
public var didBecomeStream :((_ session: URLSession, _ taskId: Int, _ didBecome: InputStream, _ outputStream: OutputStream) -> Void)?
|
||||
|
||||
init(fileProvider: FileProviderBasicRemote & FileProviderOperations, credential: URLCredential?) {
|
||||
public init(fileProvider: FileProviderBasicRemote & FileProviderOperations) {
|
||||
self.fileProvider = fileProvider
|
||||
self.credential = credential
|
||||
self.credential = fileProvider.credential
|
||||
}
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
self.finishDownloadHandler?(session, downloadTask, location)
|
||||
return
|
||||
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
if !(error == nil && task is URLSessionDownloadTask) {
|
||||
let completionHandler = completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] ?? nil
|
||||
completionHandler?(error)
|
||||
_ = completionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: task.taskIdentifier)
|
||||
}
|
||||
|
||||
guard let json = task.taskDescription?.deserializeJSON(),
|
||||
let op = FileOperationType(json: json), let fileProvider = fileProvider else {
|
||||
return
|
||||
}
|
||||
|
||||
if !(task is URLSessionDownloadTask), case FileOperationType.fetch = op {
|
||||
return
|
||||
}
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
if task is URLSessionStreamTask {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if error != nil {
|
||||
fileProvider.delegate?.fileproviderFailed(fileProvider, operation: op)
|
||||
} else {
|
||||
fileProvider.delegate?.fileproviderSucceed(fileProvider, operation: op)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
|
||||
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
||||
let completionHandler = dataCompletionHandlersForTasks[session.sessionDescription!]?[dataTask.taskIdentifier] ?? nil
|
||||
completionHandler?(data)
|
||||
_ = dataCompletionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: dataTask.taskIdentifier)
|
||||
}
|
||||
|
||||
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
self.finishDownloadHandler?(session, downloadTask, location)
|
||||
|
||||
let dcompletionHandler = downloadCompletionHandlersForTasks[session.sessionDescription!]?[downloadTask.taskIdentifier]
|
||||
dcompletionHandler?(location)
|
||||
_ = downloadCompletionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: downloadTask.taskIdentifier)
|
||||
_ = completionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: downloadTask.taskIdentifier)
|
||||
}
|
||||
|
||||
public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
|
||||
self.didSendDataHandler?(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
|
||||
|
||||
guard let json = task.taskDescription?.deserializeJSON(),
|
||||
@@ -111,12 +180,23 @@ class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDeleg
|
||||
return
|
||||
}
|
||||
|
||||
switch op {
|
||||
case .create(path: let path):
|
||||
if path.hasSuffix("/") { return }
|
||||
case .modify:
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
let progress = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
|
||||
|
||||
fileProvider.delegate?.fileproviderProgress(fileProvider, operation: op, progress: progress)
|
||||
DispatchQueue.main.async {
|
||||
fileProvider.delegate?.fileproviderProgress(fileProvider, operation: op, progress: progress)
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||
self.didReceivedData?(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
|
||||
|
||||
guard let json = downloadTask.taskDescription?.deserializeJSON(),
|
||||
@@ -124,14 +204,16 @@ class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDeleg
|
||||
return
|
||||
}
|
||||
|
||||
fileProvider.delegate?.fileproviderProgress(fileProvider, operation: op, progress: Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
|
||||
DispatchQueue.main.async {
|
||||
fileProvider.delegate?.fileproviderProgress(fileProvider, operation: op, progress: Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
authenticate(didReceive: challenge, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
authenticate(didReceive: challenge, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@@ -145,6 +227,11 @@ class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDeleg
|
||||
completionHandler(.performDefaultHandling, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 9.0, macOS 10.11, *)
|
||||
public func urlSession(_ session: URLSession, streamTask: URLSessionStreamTask, didBecome inputStream: InputStream, outputStream: OutputStream) {
|
||||
self.didBecomeStream?(session, streamTask.taskIdentifier, inputStream, outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP status codes as an enum.
|
||||
|
||||
+11
-11
@@ -11,11 +11,11 @@ import Foundation
|
||||
// This client implementation is for little-endian platform, namely x86, x64 & arm
|
||||
// For big-endian platforms like PowerPC, there must be a huge overhaul
|
||||
|
||||
protocol SMBProtocolClientDelegate: class {
|
||||
func receivedResponse(client: SMB2ProtocolClient, response: SMBResponse, for: SMBRequest)
|
||||
protocol FileProviderSMBTaskDelegate: class {
|
||||
func receivedResponse(client: FileProviderSMBTask, response: SMBResponse, for: SMBRequest)
|
||||
}
|
||||
|
||||
class SMB2ProtocolClient: FPSStreamTask {
|
||||
class FileProviderSMBTask: FileProviderStreamTask {
|
||||
var timeout: TimeInterval = 30
|
||||
|
||||
private(set) var lastMessageID: UInt64 = 0
|
||||
@@ -31,14 +31,14 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
private(set) var requestStack = [Int: SMBRequest]()
|
||||
private(set) var responseStack = [Int: SMBResponse]()
|
||||
|
||||
weak var delegate: SMBProtocolClientDelegate?
|
||||
weak var delegate: FileProviderSMBTaskDelegate?
|
||||
|
||||
func sendNegotiate(completionHandler: SimpleCompletionHandler) -> UInt64 {
|
||||
let mId = messageId()
|
||||
let smbHeader = SMB2.Header(command: .NEGOTIATE, creditRequestResponse: UInt16(126), messageId: mId, treeId: UInt32(0), sessionId: UInt64(0))
|
||||
let msg = SMB2.NegotiateRequest()
|
||||
let data = createSMB2Message(header: smbHeader, message: msg)
|
||||
self.writeData(data, timeout: 0, completionHandler: { (e) in
|
||||
self.write(data, timeout: timeout, completionHandler: { (e) in
|
||||
completionHandler?(e)
|
||||
})
|
||||
return mId
|
||||
@@ -50,9 +50,9 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
let smbHeader = SMB2.Header(command: SMB2.Command.SESSION_SETUP, creditRequestResponse: credit, messageId: mId, treeId: UInt32(0), sessionId: sessionId)
|
||||
let msg = SMB2.SessionSetupRequest(singing: [])
|
||||
let data = createSMB2Message(header: smbHeader, message: msg)
|
||||
self.writeData(data, timeout: 0, completionHandler: { (e) in
|
||||
self.write(data, timeout: timeout, completionHandler: { (e) in
|
||||
if self.sessionId == 0 {
|
||||
self.readData(OfMinLength: 64, maxLength: 65536, timeout: self.timeout, completionHandler: { (data, eof, e2) in
|
||||
self.readData(ofMinLength: 64, maxLength: 65536, timeout: self.timeout, completionHandler: { (data, eof, e2) in
|
||||
// TODO: set session id
|
||||
completionHandler?(e2 ?? e)
|
||||
})
|
||||
@@ -73,7 +73,7 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
let tcHeader = SMB2.TreeConnectRequest.Header(flags: [])
|
||||
let msg = SMB2.TreeConnectRequest(header: tcHeader, host: host, share: share)
|
||||
let data = createSMB2Message(header: smbHeader, message: msg!)
|
||||
self.writeData(data, timeout: 0, completionHandler: { (e) in
|
||||
self.write(data, timeout: timeout, completionHandler: { (e) in
|
||||
completionHandler?(e)
|
||||
|
||||
})
|
||||
@@ -85,7 +85,7 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
let smbHeader = SMB2.Header(command: .TREE_DISCONNECT, creditRequestResponse: 111, messageId: mId, treeId: treeId, sessionId: sessionId)
|
||||
let msg = SMB2.TreeDisconnect()
|
||||
let data = createSMB2Message(header: smbHeader, message: msg)
|
||||
self.writeData(data, timeout: 0, completionHandler: { (e) in
|
||||
self.write(data, timeout: timeout, completionHandler: { (e) in
|
||||
completionHandler?(e)
|
||||
})
|
||||
return mId
|
||||
@@ -96,7 +96,7 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
let smbHeader = SMB2.Header(command: .LOGOFF, creditRequestResponse: 0, messageId: mId, treeId: 0, sessionId: sessionId)
|
||||
let msg = SMB2.LogOff()
|
||||
let data = createSMB2Message(header: smbHeader, message: msg)
|
||||
self.writeData(data, timeout: 0, completionHandler: { (e) in
|
||||
self.write(data, timeout: timeout, completionHandler: { (e) in
|
||||
completionHandler?(e)
|
||||
})
|
||||
return mId
|
||||
@@ -108,7 +108,7 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
}
|
||||
|
||||
// MARK: create and analyse messages
|
||||
extension SMB2ProtocolClient {
|
||||
extension FileProviderSMBTask {
|
||||
func determineSMBVersion(_ data: Data) -> Float {
|
||||
let smbverChar: Int8 = Int8(bitPattern: data.first ?? 0)
|
||||
let version = 0 - smbverChar
|
||||
|
||||
@@ -15,7 +15,7 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open let credential: URLCredential?
|
||||
open var credential: URLCredential?
|
||||
|
||||
public typealias FileObjectClass = FileObject
|
||||
|
||||
@@ -73,11 +73,6 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
return nil
|
||||
}
|
||||
|
||||
open func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
@@ -113,7 +108,7 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
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) -> OperationHandle? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -32,7 +32,11 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
}
|
||||
|
||||
public weak var delegate: FileProviderDelegate?
|
||||
open let credential: URLCredential?
|
||||
open var credential: URLCredential? {
|
||||
didSet {
|
||||
sessionDelegate?.credential = credential
|
||||
}
|
||||
}
|
||||
open private(set) var cache: URLCache?
|
||||
public var useCache: Bool
|
||||
public var validatingCache: Bool
|
||||
@@ -40,16 +44,30 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
fileprivate var _session: URLSession?
|
||||
fileprivate var sessionDelegate: SessionDelegate?
|
||||
public var session: URLSession {
|
||||
if _session == nil {
|
||||
self.sessionDelegate = SessionDelegate(fileProvider: self, credential: credential)
|
||||
let queue = OperationQueue()
|
||||
//queue.underlyingQueue = dispatch_queue
|
||||
let config = URLSessionConfiguration.default
|
||||
config.urlCache = cache
|
||||
config.requestCachePolicy = .returnCacheDataElseLoad
|
||||
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDownloadDelegate?, delegateQueue: queue)
|
||||
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!)
|
||||
}
|
||||
return _session!
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,7 +82,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
if !["http", "https"].contains(baseURL.uw_scheme.lowercased()) {
|
||||
return nil
|
||||
}
|
||||
self.baseURL = baseURL.path.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
|
||||
self.baseURL = (baseURL.path.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")).absoluteURL
|
||||
self.currentPath = ""
|
||||
self.useCache = false
|
||||
self.validatingCache = true
|
||||
@@ -90,8 +108,8 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
aCoder.encode(self.baseURL, forKey: "baseURL")
|
||||
aCoder.encode(self.credential, forKey: "credential")
|
||||
aCoder.encode(self.currentPath, forKey: "currentPath")
|
||||
aCoder.encode(self.useCache, forKey: "isCoorinating")
|
||||
aCoder.encode(self.validatingCache, forKey: "undoManager")
|
||||
aCoder.encode(self.useCache, forKey: "useCache")
|
||||
aCoder.encode(self.validatingCache, forKey: "validatingCache")
|
||||
}
|
||||
|
||||
public static var supportsSecureCoding: Bool {
|
||||
@@ -109,6 +127,10 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let sessionuuid = _session?.sessionDescription {
|
||||
removeSessionHandler(for: sessionuuid)
|
||||
}
|
||||
|
||||
if fileProviderCancelTasksOnInvalidating {
|
||||
_session?.invalidateAndCancel()
|
||||
} else {
|
||||
@@ -116,14 +138,29 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
}
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) {
|
||||
self.contentsOfDirectory(path: path, including: [], completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler.
|
||||
|
||||
If the directory contains no entries or an error is occured, this method will return the empty array.
|
||||
|
||||
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
|
||||
- Parameter 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 url = self.url(of: path).appendingPathComponent("")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PROPFIND"
|
||||
request.setValue("1", forHTTPHeaderField: "Depth")
|
||||
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:allprop/></D:propfind>".data(using: .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
|
||||
var responseError: FileProviderWebDavError?
|
||||
@@ -145,12 +182,27 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
self.attributesOfItem(path: path, including: [], completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a `FileObject` containing the attributes of the item (file, directory, symlink, etc.) at the path in question via asynchronous completion handler.
|
||||
|
||||
If the directory contains no entries or an error is occured, this method will return the empty `FileObject`.
|
||||
|
||||
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
|
||||
- Parameter including: An array which determines which file properties should be considered to fetch.
|
||||
- Parameter completionHandler: a closure with result of directory entries or error.
|
||||
- `attributes`: A `FileObject` containing the attributes of the item.
|
||||
- `error`: Error returned by system.
|
||||
*/
|
||||
open func attributesOfItem(path: String, including: [URLResourceKey], completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
let url = self.url(of: path)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PROPFIND"
|
||||
request.setValue("1", forHTTPHeaderField: "Depth")
|
||||
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:allprop/></D:propfind>".data(using: .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
|
||||
var responseError: FileProviderWebDavError?
|
||||
@@ -250,7 +302,7 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let url = self.url(of: atPath).appendingPathComponent(folderName, isDirectory: true)
|
||||
let url = self.url(of: atPath).appendingPathComponent(folderName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? folderName, isDirectory: true)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "MKCOL"
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
@@ -266,28 +318,6 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.create(path: (path as NSString).appendingPathComponent(fileName))
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let url = self.url(of: path).appendingPathComponent(fileName)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
let task = session.uploadTask(with: request, from: data, 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)
|
||||
}
|
||||
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)
|
||||
@@ -363,6 +393,14 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
|
||||
@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 {
|
||||
dispatch_queue.async {
|
||||
completionHandler?(self.throwError(localFile.path, code: URLError.fileIsDirectory))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
@@ -373,43 +411,45 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
request.setValue("F", forHTTPHeaderField: "Overwrite")
|
||||
}
|
||||
request.httpMethod = "PUT"
|
||||
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
|
||||
let task = session.uploadTask(with: request, fromFile: localFile)
|
||||
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
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(opType, error: 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: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
|
||||
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)
|
||||
let request = URLRequest(url: url)
|
||||
let task = session.downloadTask(with: request, completionHandler: { (sourceFileURL, 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: nil, url: url)
|
||||
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
|
||||
}
|
||||
if let sourceFileURL = sourceFileURL {
|
||||
do {
|
||||
try FileManager.default.copyItem(at: sourceFileURL, to: toLocalURL)
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
return
|
||||
}
|
||||
do {
|
||||
try FileManager.default.moveItem(at: tempURL, to: destURL)
|
||||
completionHandler?(nil)
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
})
|
||||
}
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
@@ -429,25 +469,40 @@ extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
let url = self.url(of: path)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
if length > 0 {
|
||||
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
|
||||
request.setValue("bytes=\(offset)-\(offset + Int64(length) - 1)", forHTTPHeaderField: "Range")
|
||||
} else if offset > 0 && length < 0 {
|
||||
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
let handle = RemoteOperationHandle(operationType: opType, tasks: [])
|
||||
runDataTask(with: request, operationHandle: handle, 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)
|
||||
|
||||
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
|
||||
}
|
||||
completionHandler(data, responseError ?? error)
|
||||
})
|
||||
do {
|
||||
let data = try Data(contentsOf: tempURL)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(data, nil)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler(nil, e)
|
||||
}
|
||||
}
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return handle
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
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) -> OperationHandle? {
|
||||
let opType = FileOperationType.modify(path: path)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
@@ -459,22 +514,16 @@ extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
if !overwrite {
|
||||
request.setValue("F", forHTTPHeaderField: "Overwrite")
|
||||
}
|
||||
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
|
||||
let task = session.uploadTask(with: request, from: data ?? Data())
|
||||
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] 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: self.url(of: path))
|
||||
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)
|
||||
}
|
||||
defer {
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
}
|
||||
if let error = error {
|
||||
completionHandler?(error)
|
||||
return
|
||||
}
|
||||
if atomically {
|
||||
self.moveItem(path: (path as NSString).appendingPathExtension("tmp")!, to: path, completionHandler: completionHandler)
|
||||
}
|
||||
})
|
||||
completionHandler?(responseError ?? error)
|
||||
self?.delegateNotify(opType, error: responseError ?? error)
|
||||
}
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
@@ -520,12 +569,9 @@ struct DavResponse {
|
||||
|
||||
init? (_ node: AEXMLElement, baseURL: URL?) {
|
||||
|
||||
func removeSlash(_ str: String) -> String {
|
||||
if str.hasPrefix("/") {
|
||||
return str.substring(from: str.index(after: str.startIndex))
|
||||
} else {
|
||||
return str
|
||||
}
|
||||
func standardizePath(_ str: String) -> String {
|
||||
let trimmedStr = str.hasPrefix("/") ? str.substring(from: str.index(after: str.startIndex)) : str
|
||||
return trimmedStr.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? str
|
||||
}
|
||||
|
||||
// find node names with namespace
|
||||
@@ -554,7 +600,7 @@ struct DavResponse {
|
||||
} else {
|
||||
relativePath = hrefAbsolute?.absoluteString.replacingOccurrences(of: baseURL?.absoluteString ?? "", with: "", options: .anchored, range: nil) ?? hrefString
|
||||
}
|
||||
let hrefURL = URL(string: removeSlash(relativePath), relativeTo: baseURL) ?? baseURL
|
||||
let hrefURL = URL(string: standardizePath(relativePath), relativeTo: baseURL) ?? baseURL
|
||||
|
||||
guard let href = hrefURL?.standardized else { return nil }
|
||||
|
||||
@@ -616,13 +662,13 @@ struct DavResponse {
|
||||
public final class WebDavFileObject: FileObject {
|
||||
internal init(_ davResponse: DavResponse) {
|
||||
let href = davResponse.href
|
||||
let name = davResponse.prop["displayname"] ?? (davResponse.hrefString.removingPercentEncoding! as NSString).lastPathComponent
|
||||
let name = davResponse.prop["displayname"] ?? davResponse.href.lastPathComponent
|
||||
let relativePath = href.relativePath
|
||||
let path = relativePath.hasPrefix("/") ? relativePath : ("/" + relativePath)
|
||||
super.init(url: href, name: name, path: path)
|
||||
self.size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown
|
||||
self.creationDate = resolve(dateString: davResponse.prop["creationdate"] ?? "")
|
||||
self.modifiedDate = resolve(dateString: davResponse.prop["getlastmodified"] ?? "")
|
||||
self.creationDate = Date(rfcString: davResponse.prop["creationdate"] ?? "")
|
||||
self.modifiedDate = Date(rfcString: davResponse.prop["getlastmodified"] ?? "")
|
||||
self.contentType = davResponse.prop["getcontenttype"] ?? "octet/stream"
|
||||
self.isHidden = (Int(davResponse.prop["ishidden"] ?? "0") ?? 0) > 0
|
||||
self.type = self.contentType == "httpd/unix-directory" ? .directory : .regular
|
||||
@@ -632,22 +678,54 @@ public final class WebDavFileObject: FileObject {
|
||||
/// MIME type of the file.
|
||||
open internal(set) var contentType: String {
|
||||
get {
|
||||
return allValues[.mimeType] as? String ?? ""
|
||||
return allValues[.mimeTypeKey] as? String ?? ""
|
||||
}
|
||||
set {
|
||||
allValues[.mimeType] = newValue
|
||||
allValues[.mimeTypeKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP E-Tag, can be used to mark changed files.
|
||||
open internal(set) var entryTag: String? {
|
||||
get {
|
||||
return allValues[.entryTag] as? String
|
||||
return allValues[.entryTagKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues[.entryTag] = newValue
|
||||
allValues[.entryTagKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
internal class func resourceKeyToDAVProp(_ key: URLResourceKey) -> String? {
|
||||
switch key {
|
||||
case URLResourceKey.fileSizeKey:
|
||||
return "getcontentlength"
|
||||
case URLResourceKey.creationDateKey:
|
||||
return "creationdate"
|
||||
case URLResourceKey.contentModificationDateKey:
|
||||
return "getlastmodified"
|
||||
case URLResourceKey.fileResourceTypeKey, URLResourceKey.mimeTypeKey:
|
||||
return "getcontenttype"
|
||||
case URLResourceKey.isHiddenKey:
|
||||
return "ishidden"
|
||||
case URLResourceKey.entryTagKey:
|
||||
return "getetag"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
internal class func propString(_ keys: [URLResourceKey]) -> String {
|
||||
var propKeys = ""
|
||||
for item in keys {
|
||||
if let prop = WebDavFileObject.resourceKeyToDAVProp(item) {
|
||||
propKeys += "<D:prop><D:\(prop)/></D:prop>"
|
||||
}
|
||||
}
|
||||
if propKeys.isEmpty {
|
||||
propKeys = "<D:allprop/>"
|
||||
}
|
||||
return propKeys
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned by WebDAV server when trying to access or do operations on a file or folder.
|
||||
|
||||
Reference in New Issue
Block a user