Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d61e51ba1c | |||
| cdff7db32e | |||
| 9533a0e3c9 | |||
| 194673b3b6 | |||
| 194c8a41aa | |||
| c290377433 | |||
| 330a22c45d | |||
| e899804e28 | |||
| 489a9a16d0 | |||
| 93359d5173 | |||
| 12ff3a410d | |||
| a7de09362b | |||
| b1fe37cf2a | |||
| bd59eacee2 | |||
| 42579be371 | |||
| edd4914ca7 | |||
| bd3e1bd74f | |||
| 371c481482 | |||
| dea1d01bf3 | |||
| ae4cd1dff3 | |||
| 72520973e9 | |||
| 63016285af | |||
| 3245c9df03 | |||
| ee1fa89747 | |||
| 4a6b25deac | |||
| 2423cbd0f6 | |||
| 480099ca8a | |||
| 755bf6bf84 | |||
| 525825ff5d | |||
| a35384dc31 |
+66
-18
@@ -1,32 +1,80 @@
|
||||
language: objective-c
|
||||
|
||||
osx_image: xcode8.2
|
||||
|
||||
xcode_project: FileProvider.xcodeproj
|
||||
xcode_project: $PROJECTNAME.xcodeproj
|
||||
env:
|
||||
global:
|
||||
- PROJECTNAME="FileProvider"
|
||||
- FRAMEWORK_NAME="FileProvider.framework"
|
||||
- PROJECT="$PROJECTNAME.xcodeproj"
|
||||
- FRAMEWORK_NAME="$PROJECTNAME.framework"
|
||||
- IOS_FRAMEWORK_SCHEME="$PROJECTNAME iOS"
|
||||
- MACOS_FRAMEWORK_SCHEME="$PROJECTNAME OSX"
|
||||
- TVOS_FRAMEWORK_SCHEME="$PROJECTNAME tvOS"
|
||||
- IOS_SDK=iphonesimulator
|
||||
- MACOS_SDK=macosx
|
||||
- TVOS_SDK=appletvsimulator
|
||||
matrix:
|
||||
- SHCEME="FileProvider OSX" SDK="macosx" ACTION="build"
|
||||
- SHCEME="FileProvider iOS" SDK="iphonesimulator" ACTION="build"
|
||||
- SHCEME="FileProvider tvOS" SDK="appletvsimulator" ACTION="build"
|
||||
- DESTINATION="OS=10.2,name=iPad Air 2" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="YES" CARTHAGEDEPLOY="NO"
|
||||
- DESTINATION="OS=9.0,name=iPhone 6" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="YES"
|
||||
- DESTINATION="OS=8.1,name=iPhone 4S" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO"
|
||||
|
||||
- DESTINATION="OS=10.1,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" SDK="$TVOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO"
|
||||
# - DESTINATION="OS=10.0,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" SDK="$TVOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO"
|
||||
|
||||
- DESTINATION="arch=x86_64" SCHEME="$MACOS_FRAMEWORK_SCHEME" SDK="$MACOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO"
|
||||
before_install:
|
||||
- gem install xcpretty --no-rdoc --no-ri --no-document --quiet
|
||||
- brew update
|
||||
- brew outdated carthage || brew upgrade carthage
|
||||
- gem install cocoapods --no-rdoc --no-ri --no-document --quiet
|
||||
# - gem install xcpretty-travis-formatter
|
||||
|
||||
script:
|
||||
- set pipefail
|
||||
- set -o pipefail
|
||||
- xcodebuild -version
|
||||
- xcodebuild -project $PROJECT.xcodeproj -scheme "$SCHEME" -sdk $SDK $ACTION ONLY_ACTIVE_ARCH=NO | xcpretty
|
||||
# - pod lib lint --quick
|
||||
# after_success:
|
||||
|
||||
# Build Example in Debug if specified
|
||||
- if [ $BUILD_EXAMPLE == "YES" ]; then
|
||||
xcodebuild -project "$WORKSPACE" -scheme "$EXAMPLE_SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty;
|
||||
fi
|
||||
|
||||
# Build Framework in Debug and Run Tests if specified
|
||||
- if [ $RUN_TESTS == "YES" ]; then
|
||||
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty;
|
||||
else
|
||||
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty;
|
||||
fi
|
||||
|
||||
# Build Framework in Release and Run Tests if specified
|
||||
- if [ $RUN_TESTS == "YES" ]; then
|
||||
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty;
|
||||
else
|
||||
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO build | xcpretty;
|
||||
fi
|
||||
|
||||
# Run `pod lib lint` if specified
|
||||
- if [ $POD == "YES" ]; then
|
||||
pod lib lint --quick;
|
||||
fi
|
||||
|
||||
after_success:
|
||||
# Run `pod trunk push` if specified
|
||||
- if [ $POD == "YES" ] && [ -n "$TRAVIS_TAG" ]; then
|
||||
pod trunk push;
|
||||
fi
|
||||
|
||||
# - bash <(curl -s https://codecov.io/bash)
|
||||
before_deploy:
|
||||
- carthage build --no-skip-current
|
||||
- carthage archive $PROJECTNAME
|
||||
- if [ $CARTHAGEDEPLOY == "YES" ] && [ -n "$TRAVIS_TAG" ]; then
|
||||
brew update;
|
||||
brew outdated carthage || brew upgrade carthage;
|
||||
carthage version;
|
||||
carthage build --no-skip-current --verbose;
|
||||
carthage archive $PROJECTNAME;
|
||||
fi
|
||||
|
||||
deploy:
|
||||
file: $PROJECTNAME.framework.zip
|
||||
provider: releases
|
||||
api_key: "$GITHUBTOKEN"
|
||||
file: $FRAMEWORK_NAME.zip
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
repo: amosavian/$PROJECTNAME
|
||||
tags: true
|
||||
condition: "$CARTHAGEDEPLOY = YES"
|
||||
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
|
||||
#
|
||||
|
||||
s.name = "FileProvider"
|
||||
s.version = "0.12.1"
|
||||
s.version = "0.14.2"
|
||||
s.summary = "FileManager replacement for Local and Remote (WebDAV/Dropbox/OneDrive/SMB2) files on iOS and macOS."
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
|
||||
@@ -162,14 +162,11 @@
|
||||
79BD63A71E2CC2940035128C /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/CoreGraphics.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63A91E2CC2BB0035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63AB1E2CC2C20035128C /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/ImageIO.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63AD1E2CC2EB0035128C /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/MediaPlayer.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63AF1E2CC3300035128C /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; };
|
||||
79BD63B11E2CC3350035128C /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; };
|
||||
79BD63B31E2CC33D0035128C /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; };
|
||||
79BD63B51E2CC3860035128C /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
|
||||
79BD63B71E2CC38D0035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
|
||||
79BD63B91E2CC39B0035128C /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/usr/lib/libxml2.tbd; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63BB1E2CC3B90035128C /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/MediaPlayer.framework; sourceTree = DEVELOPER_DIR; };
|
||||
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; };
|
||||
@@ -221,14 +218,11 @@
|
||||
79BD63C11E2CC3D30035128C /* AVFoundation.framework */,
|
||||
79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */,
|
||||
79BD63BD1E2CC3C20035128C /* ImageIO.framework */,
|
||||
79BD63BB1E2CC3B90035128C /* MediaPlayer.framework */,
|
||||
79BD63B91E2CC39B0035128C /* libxml2.tbd */,
|
||||
79BD63B71E2CC38D0035128C /* AVFoundation.framework */,
|
||||
79BD63B51E2CC3860035128C /* CoreFoundation.framework */,
|
||||
79BD63B31E2CC33D0035128C /* MediaPlayer.framework */,
|
||||
79BD63B11E2CC3350035128C /* ImageIO.framework */,
|
||||
79BD63AF1E2CC3300035128C /* libxml2.tbd */,
|
||||
79BD63AD1E2CC2EB0035128C /* MediaPlayer.framework */,
|
||||
79BD63AB1E2CC2C20035128C /* ImageIO.framework */,
|
||||
79BD63A91E2CC2BB0035128C /* AVFoundation.framework */,
|
||||
79BD63A71E2CC2940035128C /* CoreGraphics.framework */,
|
||||
@@ -603,7 +597,7 @@
|
||||
799396601D48B7BF00086753 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.12.1;
|
||||
BUNDLE_VERSION_STRING = 0.14.2;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
@@ -633,7 +627,7 @@
|
||||
799396611D48B7BF00086753 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.12.1;
|
||||
BUNDLE_VERSION_STRING = 0.14.2;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# FileProvider
|
||||

|
||||
|
||||
>This Swift library provide a swifty way to deal with local and remote files and directories in a unified way.
|
||||
|
||||
@@ -24,27 +24,25 @@ This library provides implementaion of WebDav, Dropbox, OneDrive and SMB2 (incom
|
||||
|
||||
All functions are async calls and it wont block your main thread.
|
||||
|
||||
Local and WebDAV providers are fully tested and can be used in production environment.
|
||||
|
||||
## Features
|
||||
|
||||
- [x] **LocalFileProvider** a wrapper around `FileManager` with some additions like searching and reading a portion of file.
|
||||
- [x] **CloudFileProvider** A wrapper around app's ubiquitous container to iCloud Drive in iOS 8+ API.
|
||||
- [x] **WebDAVFileProvider** WebDAV protocol is defacto file transmission standard, replaced FTP.
|
||||
- [x] **DropboxFileProvider** A wrapper around Dropbox REST API.
|
||||
- [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.
|
||||
* For now it has limitation in uploading files up to 100MB.
|
||||
- [ ] **GoogleFileProvider** A wrapper around Goodle Drive REST API.
|
||||
- [ ] **AmazonS3FileProvider** Amazon storage backend. Used by many sites.
|
||||
- [ ] **SMBFileProvider** SMB2/3 introduced in 2006, which is a file and printer sharing protocol originated from Microsoft Windows and now is replacing AFP protocol on macOS.
|
||||
* Data types and some basic functions are implemented but *main interface is not implemented yet!*
|
||||
* SMB1/CIFS is depericated and very tricky to be implemented
|
||||
* Data types and some basic functions are implemented but *main interface is not implemented yet!*.
|
||||
* SMB1/CIFS is deprecated and very tricky to be implemented.
|
||||
- [ ] **FTPFileProvider** while deprecated in 1990s, it's still in use on some Web hosts.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Swift 3**
|
||||
- **Swift 3.0 +**
|
||||
- iOS 8.0 , OSX 10.10
|
||||
- XCode 8.0
|
||||
|
||||
@@ -69,7 +67,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: 8)
|
||||
.Package(url: "https://github.com/amosavian/FileProvider.git", majorVersion: 0, minorVersion: 12)
|
||||
```
|
||||
|
||||
### Manually
|
||||
@@ -95,7 +93,7 @@ Then you can do either:
|
||||
|
||||
* Copy Source folder to your project and Voila!
|
||||
|
||||
* Drop FileProvider.xcodeproj to you Xcode workspace and add the framework to your Embeded Binaries in target.
|
||||
* Drop `FileProvider.xcodeproj` to you Xcode workspace and add the framework to your Embeded Binaries in target.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -120,12 +118,12 @@ Also for using group shared container:
|
||||
|
||||
```swift
|
||||
let documentsProvider = LocalFileProvider(sharedContainerId: "group.yourcompany.appContainer")
|
||||
// Replace your group identifier with string above
|
||||
// Replace your group identifier
|
||||
```
|
||||
|
||||
You can't change the base url later. and all paths are related to this base url by default.
|
||||
|
||||
To initialize an iCloud Container provider use below code, This will automatically manager creating Documents folder in container:
|
||||
To initialize an iCloud Container provider look at [here](https://medium.com/ios-os-x-development/icloud-drive-documents-1a46b5706fe1) to see how to update project settings then use below code, This will automatically manager creating Documents folder in container:
|
||||
|
||||
```swift
|
||||
let documentsProvider = CloudFileProvider(containerId: nil)
|
||||
@@ -141,11 +139,11 @@ let webdavProvider = WebDAVFileProvider(baseURL: URL(string: "http://www.example
|
||||
|
||||
* In case you want to connect non-secure servers for WebDAV (http) in iOS 9+ / macOS 10.11+ you should disable App Transport Security (ATS) according to [this guide.](https://gist.github.com/mlynch/284699d676fe9ed0abfa)
|
||||
|
||||
* For Dropbox & OneDrive, user is clientID and password is Token which both must be retrieved via [OAuth2 API of Dropbox](https://www.dropbox.com/developers/reference/oauth-guide). There are libraries like [p2/OAuth2](https://github.com/p2/OAuth2) or [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) which can facilate the procedure to retrieve token. The latter is easier to use and prefered. Also you can use [auth0/Lock](https://github.com/auth0/Lock.iOS-OSX) which provides graphical user interface.
|
||||
* For Dropbox & OneDrive, user is clientID and password is Token which both must be retrieved via [OAuth2 API of Dropbox](https://www.dropbox.com/developers/reference/oauth-guide). There are libraries like [p2/OAuth2](https://github.com/p2/OAuth2) or [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) which can facilate the procedure to retrieve token. The latter is easier to use and prefered.
|
||||
|
||||
For interaction with UI, set delegate variable of `FileProvider` object
|
||||
|
||||
You can use `absoluteURL()` method if provider to get direct access url (local or remote files) for some file systems which allows to do so (Dropbox doesn't support and returns path simply wrapped in URL)
|
||||
You can use `url(of:)` method if provider to get direct access url (local or remote files) for some file systems which allows to do so (Dropbox doesn't support and returns path simply wrapped in URL)
|
||||
|
||||
### Delegates
|
||||
|
||||
@@ -160,7 +158,7 @@ override func viewDidLoad() {
|
||||
documentsProvider.delegate = self as FileProviderDelegate
|
||||
}
|
||||
|
||||
func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperation) {
|
||||
func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperationType) {
|
||||
switch operation {
|
||||
case .copy(source: let source, destination: let dest):
|
||||
print("\(source) copied to \(dest).")
|
||||
@@ -171,7 +169,7 @@ func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: File
|
||||
}
|
||||
}
|
||||
|
||||
func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperation) {
|
||||
func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperationType) {
|
||||
switch operation {
|
||||
case .copy(source: let source, destination: let dest):
|
||||
print("copy of \(source) failed.")
|
||||
@@ -182,7 +180,7 @@ func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileO
|
||||
}
|
||||
}
|
||||
|
||||
func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperation, progress: Float) {
|
||||
func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperationType, progress: Float) {
|
||||
switch operation {
|
||||
case .copy(source: let source, destination: let dest):
|
||||
print("Copy\(source) to \(dest): \(progress * 100) completed.")
|
||||
@@ -264,7 +262,13 @@ You can then pass "" (empty string) to `contentsOfDirectory` method to list file
|
||||
Creating new directory:
|
||||
|
||||
```swift
|
||||
documentsProvider.create(folder: "new folder", at: "/", completionHandler: nil)
|
||||
documentsProvider.create(folder: "new folder", at: "/", completionHandler: { error in
|
||||
if let error = error {
|
||||
// Error handling here
|
||||
} else {
|
||||
// The operation succeed
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Creating new file from data:
|
||||
@@ -329,20 +333,62 @@ let data = "What's up Newyork!".data(encoding: .utf8)
|
||||
documentsProvider.writeContents(path: "old.txt", content: data, atomically: true, completionHandler: nil)
|
||||
```
|
||||
|
||||
### 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:
|
||||
|
||||
```swift
|
||||
// To setup a new UndoManager:
|
||||
documentsProvider.setupUndoManager()
|
||||
// or if you have an UndoManager object already:
|
||||
documentsProvider.undoManager = self.undoManager
|
||||
|
||||
// e.g.: To undo last operation manually:
|
||||
documentsProvider.undoManager?.undo()
|
||||
```
|
||||
|
||||
You can also bind `UndoManager` object with view controller to use shake gesture and builtin undo support in iOS/macOS, add these code to your ViewController class like this sample code:
|
||||
|
||||
```swift
|
||||
class ViewController: UIViewController
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override var undoManager: UndoManager? {
|
||||
return (provider as? FileProvideUndoable)?.undoManager
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
// Your code here
|
||||
UIApplication.shared.applicationSupportsShakeToEdit = true
|
||||
self.becomeFirstResponder()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
// Your code here
|
||||
UIApplication.shared.applicationSupportsShakeToEdit = false
|
||||
self.resignFirstResponder()
|
||||
}
|
||||
// The rest of your implementation
|
||||
}
|
||||
```
|
||||
|
||||
### Operation Handle
|
||||
|
||||
Creating/Copying/Deleting functions return a `OperationHandle` for remote operations. It provides operation type, progress and a `.cancel()` method which allows you to cancel operation in midst.
|
||||
|
||||
It's not supported by native `(NS)FileManager` so `LocalFileProvider`, but this functionality will be added to future `PosixFileProvider` class.
|
||||
|
||||
### Monitoring FIle Changes
|
||||
### Monitoring File Changes
|
||||
|
||||
You can monitor updates in some file system (Local and SMB2), there is three methods in supporting provider you can use to register a handler, to unregister and to check whether it's being monitored or not. It's useful to find out when new files added or removed from directory and update user interface. The handler will be dispatched to main threads to avoid UI bugs with a 0.25 sec delay.
|
||||
|
||||
```swift
|
||||
// to register a new notification handler
|
||||
documentsProvider.registerNotifcation(path: provider.currentPath)
|
||||
{
|
||||
documentsProvider.registerNotifcation(path: provider.currentPath) {
|
||||
// calling functions to update UI
|
||||
}
|
||||
|
||||
@@ -365,7 +411,7 @@ To check either file thumbnail is supported or not and fetch thumbnail, use (and
|
||||
let path = "/newImage.jpg"
|
||||
let thumbSize = CGSize(width: 64, height: 64)
|
||||
if documentsProvider.thumbnailOfFileSupported(path: path {
|
||||
documentsProvider..thumbnailOfFile(path: file.path, dimension: thumbSize, completionHandler: { (image, error) in
|
||||
documentsProvider.thumbnailOfFile(path: file.path, dimension: thumbSize, completionHandler: { (image, error) in
|
||||
DispatchQueue.main.async {
|
||||
self.previewImage.image = image
|
||||
}
|
||||
@@ -377,7 +423,7 @@ if documentsProvider.thumbnailOfFileSupported(path: path {
|
||||
|
||||
##### Meta-informations
|
||||
|
||||
To get meta-information like image/video taken date, dimension, etc., use (and modify) these example code:
|
||||
To get meta-information like image/video taken date, location, dimension, etc., use (and modify) these example code:
|
||||
|
||||
```swift
|
||||
if documentsProvider..propertiesOfFile(path: file.path, completionHandler: { (propertiesDictionary, keys, error) in
|
||||
@@ -393,6 +439,12 @@ 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:
|
||||
|
||||
- [ ] Implement Test-case (XCTest)
|
||||
- [ ] Add Sample project for iOS
|
||||
- [ ] Add Sample project for macOS
|
||||
|
||||
## Projects in use
|
||||
|
||||
* [EDM - Browse and Receive Files](https://itunes.apple.com/us/app/edm-browse-and-receive-files/id948397575?ls=1&mt=8)
|
||||
@@ -402,7 +454,9 @@ If you used this library in your project, you can open an issue to inform us.
|
||||
|
||||
## Meta
|
||||
|
||||
Amir-Abbas Mousavian – [@amosavian](https://twitter.com/amosavian)
|
||||
Amir-Abbas Mousavian – [@amosavian](https://twitter.com/amosavian)
|
||||
|
||||
Thanks to [Hootan Moradi](https://github.com/hoootan) for designing logo.
|
||||
|
||||
Distributed under the MIT license. See `LICENSE` for more information.
|
||||
|
||||
|
||||
+421
-105
@@ -8,15 +8,25 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
Allows accessing to iCloud Drive stored files. Determine scope when initializing, to either access
|
||||
to public documents folder or files stored as data.
|
||||
|
||||
To setup a functional iCloud container, please
|
||||
[read this page](https://medium.com/ios-os-x-development/icloud-drive-documents-1a46b5706fe1).
|
||||
*/
|
||||
open class CloudFileProvider: LocalFileProvider {
|
||||
/// An string to identify type of provider.
|
||||
open override class var type: String { return "iCloudDrive" }
|
||||
|
||||
/// Actually is readonly, value is true
|
||||
/// Forces file operations to use `NSFileCoordinating`,
|
||||
/// Actually this is readonly, and value is always true.
|
||||
override open var isCoorinating: Bool {
|
||||
get {
|
||||
return true
|
||||
}
|
||||
set {
|
||||
assert(true, "CloudFileProvider.isCoorinating can't be set")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -24,6 +34,12 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
/// The fully-qualified container identifier for an iCloud container directory.
|
||||
open fileprivate(set) var containerId: String?
|
||||
|
||||
/// Scope of container, indicates user can manipulate data/files or not.
|
||||
open fileprivate(set) var scope: UbiquitousScope
|
||||
|
||||
/// Set this property to ignore initiations asserting to be on secondary thread
|
||||
static open var asserting: Bool = true
|
||||
|
||||
/**
|
||||
Initializes the provider for the iCloud container associated with the specified identifier and
|
||||
establishes access to that container.
|
||||
@@ -31,19 +47,27 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
- Important: Do not call this method from your app’s main thread. Because this method might take a nontrivial amount of time to set up iCloud and return the requested URL, you should always call it from a secondary thread.
|
||||
|
||||
- Parameter containerId: The fully-qualified container identifier for an iCloud container directory. The string you specify must not contain wildcards and must be of the form `<TEAMID>.<CONTAINER>`, where `<TEAMID>` is your development team ID and `<CONTAINER>` is the bundle identifier of the container you want to access.\
|
||||
The container identifiers for your app must be declared in the `com.apple.developer.ubiquity-container-identifiers` array of the `.entitlements` property list file in your Xcode project.\
|
||||
If you specify nil for this parameter, this method uses the first container listed in the `com.apple.developer.ubiquity-container-identifiers` entitlement array.
|
||||
The container identifiers for your app must be declared in the `com.apple.developer.ubiquity-container-identifiers` array of the `.entitlements` property list file in your Xcode project.\
|
||||
If you specify nil for this parameter, this method uses the first container listed in the `com.apple.developer.ubiquity-container-identifiers` entitlement array.
|
||||
- Parameter scope: Use `.documents` (default) to put documents that the user is allowed to access inside a Documents subdirectory. Otherwise use `.data` to store user-related data files that your app needs to share but that are not files you want the user to manipulate directly.
|
||||
*/
|
||||
public init? (containerId: String?) {
|
||||
assert(!Thread.isMainThread, "LocalFileProvider.init(containerId:) is not recommended to be executed on Main Thread.")
|
||||
guard FileManager.default.ubiquityIdentityToken == nil else {
|
||||
public init? (containerId: String?, scope: UbiquitousScope = .documents) {
|
||||
assert(!CloudFileProvider.asserting || !Thread.isMainThread, "LocalFileProvider.init(containerId:) is not recommended to be executed on Main Thread.")
|
||||
guard FileManager.default.ubiquityIdentityToken != nil else {
|
||||
return nil
|
||||
}
|
||||
guard let ubiquityURL = FileManager.default.url(forUbiquityContainerIdentifier: containerId) else {
|
||||
return nil
|
||||
}
|
||||
self.containerId = containerId
|
||||
let baseURL = ubiquityURL.standardized.appendingPathComponent("Documents/")
|
||||
self.scope = scope
|
||||
let baseURL: URL
|
||||
if scope == .documents {
|
||||
baseURL = ubiquityURL.appendingPathComponent("Documents/")
|
||||
} else {
|
||||
baseURL = ubiquityURL
|
||||
}
|
||||
|
||||
super.init(baseURL: baseURL)
|
||||
self.isCoorinating = true
|
||||
|
||||
@@ -57,12 +81,25 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
try? fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler.
|
||||
|
||||
If the directory contains no entries or an error is occured, this method will return the empty array.
|
||||
|
||||
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
|
||||
- Parameter completionHandler: a closure with result of directory entries or error.
|
||||
`contents`: An array of `FileObject` identifying the the directory entries.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
// FIXME: create runloop for dispatch_queue, start query on it
|
||||
dispatch_queue.async {
|
||||
let pathURL = self.url(of: path)
|
||||
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "%K BEGINSWITH %@", NSMetadataItemPathKey, pathURL.path)
|
||||
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
|
||||
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
|
||||
defer {
|
||||
@@ -91,23 +128,45 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(contents, nil)
|
||||
query.stop()
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(contents, nil)
|
||||
}
|
||||
|
||||
})
|
||||
query.start()
|
||||
DispatchQueue.main.async {
|
||||
if !query.start() {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Please don't rely this function to get iCloud drive total and remaining capacity
|
||||
/// - Important: iCloud Storage size and free space is unavailable, it returns local space
|
||||
open override func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) {
|
||||
super.storageProperties(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 completionHandler: a closure with result of directory entries or error.
|
||||
`attributes`: A `FileObject` containing the attributes of the item.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
open override func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
let pathURL = self.url(of: path)
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemPathKey, pathURL.path)
|
||||
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
|
||||
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
|
||||
defer {
|
||||
@@ -119,51 +178,262 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
|
||||
guard let result = (query.results as? [NSMetadataItem])?.first, let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
let error = self.throwError(path, code: CocoaError.fileNoSuchFile)
|
||||
completionHandler(nil, error)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let file = self.mapFileObject(attributes: attribs) {
|
||||
completionHandler(file, nil)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(file, nil)
|
||||
}
|
||||
} else {
|
||||
let noFileError = self.throwError(path, code: CocoaError.fileNoSuchFile)
|
||||
completionHandler(nil, noFileError)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, noFileError)
|
||||
}
|
||||
}
|
||||
})
|
||||
query.start()
|
||||
DispatchQueue.main.async {
|
||||
if !query.start() {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Search files inside directory using query asynchronously.
|
||||
|
||||
- Note: For now only it's limited to file names. `query` parameter may take `NSPredicate` format in near future.
|
||||
|
||||
- Parameters:
|
||||
- path: location of directory to start search
|
||||
- recursive: Searching subdirectories of path
|
||||
- query: Simple string of file name to be search (for now).
|
||||
- foundItemHandler: Closure which is called when a file is found
|
||||
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
|
||||
*/
|
||||
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
|
||||
let mapDict: [String: String] = ["url": NSMetadataItemURLKey, "name": NSMetadataItemFSNameKey, "path": NSMetadataItemPathKey, "filesize": NSMetadataItemFSSizeKey, "modifiedDate": NSMetadataItemFSContentChangeDateKey, "creationDate": NSMetadataItemFSCreationDateKey, "contentType": NSMetadataItemContentTypeKey]
|
||||
|
||||
func updateQueryKeys(_ queryComponent: NSPredicate) -> NSPredicate {
|
||||
if let cQuery = queryComponent as? NSCompoundPredicate {
|
||||
let newSub = cQuery.subpredicates.map { updateQueryKeys($0 as! NSPredicate) }
|
||||
switch cQuery.compoundPredicateType {
|
||||
case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub)
|
||||
case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub.first!)
|
||||
case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub)
|
||||
}
|
||||
} else if let cQuery = queryComponent as? NSComparisonPredicate {
|
||||
var newLeft = cQuery.leftExpression
|
||||
var newRight = cQuery.rightExpression
|
||||
if newLeft.expressionType == .keyPath, let newKey = mapDict[newLeft.keyPath] {
|
||||
newLeft = NSExpression(forKeyPath: newKey)
|
||||
}
|
||||
if newRight.expressionType == .keyPath, let newKey = mapDict[newRight.keyPath] {
|
||||
newRight = NSExpression(forKeyPath: newKey)
|
||||
}
|
||||
if newLeft.expressionType == .keyPath, newLeft.keyPath == "type" {
|
||||
newRight = NSExpression(forConstantValue: newRight.constantValue as? String == "directory" ? "public.directory": "public.data")
|
||||
}
|
||||
if newRight.expressionType == .keyPath, newRight.keyPath == "type" {
|
||||
newLeft = NSExpression(forConstantValue: newLeft.constantValue as? String == "directory" ? "public.directory": "public.data")
|
||||
}
|
||||
return NSComparisonPredicate(leftExpression: newLeft, rightExpression: newRight, modifier: cQuery.comparisonPredicateModifier, type: cQuery.predicateOperatorType, options: cQuery.options)
|
||||
} else {
|
||||
return queryComponent
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_queue.async {
|
||||
let pathURL = self.url(of: path)
|
||||
let mdquery = NSMetadataQuery()
|
||||
mdquery.predicate = NSPredicate(format: "(%K BEGINSWITH %@) && (\(updateQueryKeys(query).predicateFormat))", NSMetadataItemPathKey, pathURL.path)
|
||||
mdquery.searchScopes = [self.scope.rawValue]
|
||||
|
||||
var lastReportedCount = 0
|
||||
|
||||
if let foundItemHandler = foundItemHandler {
|
||||
var updateObserver: NSObjectProtocol?
|
||||
|
||||
// FIXME: Remove this section as it won't work as expected on iCloud
|
||||
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryGatheringProgress, object: mdquery, queue: nil, using: { (notification) in
|
||||
|
||||
mdquery.disableUpdates()
|
||||
|
||||
guard mdquery.resultCount > lastReportedCount else { return }
|
||||
|
||||
for index in lastReportedCount..<mdquery.resultCount {
|
||||
guard let attribs = (mdquery.result(at: index) as? NSMetadataItem)?.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let file = self.mapFileObject(attributes: attribs) {
|
||||
foundItemHandler(file)
|
||||
}
|
||||
}
|
||||
lastReportedCount = mdquery.resultCount
|
||||
|
||||
mdquery.enableUpdates()
|
||||
})
|
||||
}
|
||||
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: mdquery, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
mdquery.stop()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
}
|
||||
|
||||
guard let results = mdquery.results as? [NSMetadataItem] else {
|
||||
return
|
||||
}
|
||||
|
||||
mdquery.disableUpdates()
|
||||
|
||||
var contents = [FileObject]()
|
||||
for result in results {
|
||||
guard let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let file = self.mapFileObject(attributes: attribs) {
|
||||
contents.append(file)
|
||||
}
|
||||
}
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(contents, nil)
|
||||
}
|
||||
})
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if !mdquery.start() {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open override func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
dispatch_queue.async {
|
||||
completionHandler(self.fileManager.ubiquityIdentityToken != nil)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a new directory at the specified path asynchronously.
|
||||
This will create any necessary intermediate directories.
|
||||
|
||||
- Parameters:
|
||||
- folder: Directory name.
|
||||
- at: Parent path of new directory.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.create(folder: folderName, at: atPath, completionHandler: completionHandler) else { return nil }
|
||||
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.
|
||||
Either a new name or the old one.
|
||||
|
||||
- Parameters:
|
||||
- path: original file or directory path.
|
||||
- to: destination path of file or directory, including file/directory name.
|
||||
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.moveItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Copies a file or directory from `path` to designated path asynchronously.
|
||||
When want copy a file, destination path should also consists of file name.
|
||||
Either a new name or the old one.
|
||||
|
||||
- Parameters:
|
||||
- path: original file or directory path.
|
||||
- to: destination path of file or directory, including file/directory name.
|
||||
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.copyItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Removes the file or directory at the specified path.
|
||||
|
||||
- Important: Due to a bug (race condition?) in Apple API, it takes about 3-5 seconds to update containing folder
|
||||
list and triggering notification registered for directory while completion handler will run almost immediately.
|
||||
It's your responsibility to workaourd this bug/feature and mark file as deleted in your software.
|
||||
|
||||
- Parameters:
|
||||
- path: file or directory path.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
|
||||
|
||||
*/
|
||||
@discardableResult
|
||||
open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.removeItem(path: path, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Uploads a file from local file url to designated path asynchronously.
|
||||
Method will fail if source is not a local url with `file://` scheme.
|
||||
|
||||
- Parameters:
|
||||
- localFile: a file url to file.
|
||||
- to: destination path of file, including file/directory name.
|
||||
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
// TODO: Make use of overwrite parameter
|
||||
@@ -198,6 +468,16 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
return CloudOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Download a file from `path` to designated local file url asynchronously.
|
||||
Method will fail if destination is not a local url with `file://` scheme.
|
||||
|
||||
- Parameters:
|
||||
- path: original file or directory path.
|
||||
- toLocalURL: destination local url of file, including file/directory name.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
|
||||
@@ -216,104 +496,83 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Retreives a `Data` object with the contents of the file asynchronously vis contents argument of completion handler.
|
||||
If path specifies a directory, or if some other error occurs, data will be nil.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of file.
|
||||
- completionHandler: a closure with result of file contents or error.
|
||||
`contents`: contents of file in a `Data` object.
|
||||
`error`: Error returned by system.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
guard let r = super.contents(path: path, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler.
|
||||
If path specifies a directory, or if some other error occurs, data will be nil.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of file.
|
||||
- offset: First byte index which should be read. **Starts from 0.**
|
||||
- length: Bytes count of data. Pass `-1` to read until the end of file.
|
||||
- completionHandler: a closure with result of file contents or error.
|
||||
`contents`: contents of file in a `Data` object.
|
||||
`error`: Error returned by system.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
guard let r = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Write the contents of the `Data` to a location asynchronously.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of target file.
|
||||
- contents: Data to be written into file.
|
||||
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
|
||||
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
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)
|
||||
}
|
||||
|
||||
open override func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
let pathURL = self.url(of: path)
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "(%K BEGINSWITH %@) && (%K LIKE %@)", NSMetadataItemPathKey, pathURL.path, NSMetadataItemFSNameKey, query)
|
||||
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
|
||||
|
||||
var lastReportedCount = 0
|
||||
|
||||
if let foundItemHandler = foundItemHandler {
|
||||
var updateObserver: NSObjectProtocol?
|
||||
|
||||
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryGatheringProgress, object: query, queue: nil, using: { (notification) in
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
guard query.resultCount > lastReportedCount else { return }
|
||||
|
||||
for index in lastReportedCount..<query.resultCount {
|
||||
guard let attribs = (query.result(at: index) as? NSMetadataItem)?.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let file = self.mapFileObject(attributes: attribs) {
|
||||
foundItemHandler(file)
|
||||
}
|
||||
}
|
||||
lastReportedCount = query.resultCount
|
||||
|
||||
query.enableUpdates()
|
||||
})
|
||||
}
|
||||
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
query.stop()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
}
|
||||
|
||||
guard let results = query.results as? [NSMetadataItem] else {
|
||||
return
|
||||
}
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
var contents = [FileObject]()
|
||||
for result in results {
|
||||
guard let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let file = self.mapFileObject(attributes: attribs) {
|
||||
contents.append(file)
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(contents, nil)
|
||||
})
|
||||
|
||||
query.start()
|
||||
}
|
||||
}
|
||||
fileprivate var monitors = [String: (NSMetadataQuery, NSObjectProtocol)]()
|
||||
|
||||
fileprivate var monitors = [URL: (NSMetadataQuery, NSObjectProtocol)]()
|
||||
//
|
||||
/**
|
||||
Starts monitoring a path and its subpaths, including files and folders, for any change,
|
||||
including copy, move/rename, content changes, etc.
|
||||
To avoid thread congestion, `evetHandler` will be triggered with 0.2 seconds interval,
|
||||
and has a 0.25 second delay, to ensure it's called after updates.
|
||||
|
||||
- Note: this functionality is available only in `LocalFileProvider` and `CloudFileProvider`.
|
||||
- Note: `eventHandler` is not called on main thread, for updating UI. dispatch routine to main thread.
|
||||
- Important: `eventHandler` may be called if file is changed in recursive subpaths of registered path.
|
||||
This may cause negative impact on performance if a root path is being monitored.
|
||||
|
||||
- Parameters:
|
||||
- path: path of directory.
|
||||
- eventHandler: Closure executed after change, on a secondary thread.
|
||||
*/
|
||||
open override func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
|
||||
self.unregisterNotifcation(path: path)
|
||||
let pathURL = self.url(of: path)
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "(%K BEGINSWITH %@)", NSMetadataItemPathKey, pathURL.path)
|
||||
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
|
||||
query.valueListAttributes = []
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
|
||||
let updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: nil, using: { (notification) in
|
||||
|
||||
@@ -324,24 +583,32 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
query.enableUpdates()
|
||||
})
|
||||
|
||||
query.start()
|
||||
|
||||
monitors[pathURL] = (query, updateObserver)
|
||||
DispatchQueue.main.async {
|
||||
if query.start() {
|
||||
self.monitors[path] = (query, updateObserver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops monitoring the path.
|
||||
///
|
||||
/// - Parameter path: path of directory.
|
||||
open override func unregisterNotifcation(path: String) {
|
||||
let key = url(of: path)
|
||||
guard let (query, observer) = monitors[key] else {
|
||||
guard let (query, observer) = monitors[path] else {
|
||||
return
|
||||
}
|
||||
query.disableUpdates()
|
||||
query.stop()
|
||||
monitors.removeValue(forKey: key)
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
monitors.removeValue(forKey: path)
|
||||
}
|
||||
|
||||
/// Investigate either the path is registered for change notification or not.
|
||||
///
|
||||
/// - Parameter path: path of directory.
|
||||
/// - Returns: Directory is being monitored or not.
|
||||
open override func isRegisteredForNotification(path: String) -> Bool {
|
||||
return monitors[url(of: path)] != nil
|
||||
return monitors[path] != nil
|
||||
}
|
||||
|
||||
open override func copy(with zone: NSZone? = nil) -> Any {
|
||||
@@ -349,12 +616,11 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
copy?.currentPath = self.currentPath
|
||||
copy?.delegate = self.delegate
|
||||
copy?.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy?.isPathRelative = self.isPathRelative
|
||||
return copy as Any
|
||||
}
|
||||
|
||||
fileprivate func mapFileObject(attributes attribs: [String: Any]) -> FileObject? {
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, let name = attribs[NSMetadataItemFSNameKey] as? String else {
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardizedFileURL, let name = attribs[NSMetadataItemFSNameKey] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -387,20 +653,68 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a pulic url with expiration date, can be shared with other people.
|
||||
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
/**
|
||||
Genrates a public url to a file to be shared with other users and can be downloaded without authentication.
|
||||
|
||||
- Important: URL will be available for a limitied time, determined in `expiration` argument.
|
||||
|
||||
- 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.
|
||||
*/
|
||||
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
operation_queue.addOperation {
|
||||
do {
|
||||
var expiration: NSDate?
|
||||
let url = try self.opFileManager.url(forPublishingUbiquitousItemAt: self.url(of: path), expiration: &expiration)
|
||||
completionHandler(url, nil, expiration as Date?, nil)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(url, nil, expiration as Date?, nil)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler(nil, nil, nil, e)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, nil, nil, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum UbiquitousScope: RawRepresentable {
|
||||
/// Search all files not in the Documents directories of the app’s iCloud container directories.
|
||||
/// Use this scope to store user-related data files that your app needs to share
|
||||
/// but that are not files you want the user to manipulate directly.
|
||||
case data
|
||||
/// Search all files in the Documents directories of the app’s iCloud container directories.
|
||||
/// Put documents that the user is allowed to access inside a Documents subdirectory.
|
||||
case documents
|
||||
|
||||
public typealias RawValue = String
|
||||
|
||||
public init? (rawValue: String) {
|
||||
switch rawValue {
|
||||
case NSMetadataQueryUbiquitousDataScope:
|
||||
self = .data
|
||||
case NSMetadataQueryUbiquitousDocumentsScope:
|
||||
self = .documents
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .data:
|
||||
return NSMetadataQueryUbiquitousDataScope
|
||||
case .documents:
|
||||
return NSMetadataQueryUbiquitousDocumentsScope
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class CloudOperationHandle: OperationHandle {
|
||||
public let baseURL: URL?
|
||||
public let operationType: FileOperationType
|
||||
@@ -458,11 +772,12 @@ open class CloudOperationHandle: OperationHandle {
|
||||
fileprivate static func getMetadataItem(url: URL) -> NSMetadataItem? {
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path)
|
||||
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
|
||||
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]
|
||||
|
||||
var item: NSMetadataItem?
|
||||
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
@@ -479,8 +794,9 @@ open class CloudOperationHandle: OperationHandle {
|
||||
|
||||
})
|
||||
|
||||
group.enter()
|
||||
query.start()
|
||||
DispatchQueue.main.async {
|
||||
query.start()
|
||||
}
|
||||
_ = group.wait(timeout: DispatchTime.now() + 30)
|
||||
return item
|
||||
}
|
||||
|
||||
@@ -10,9 +10,14 @@
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
|
||||
/**
|
||||
Allows accessing to Dropbox stored files. This provider doesn't cache or save files internally, however you can
|
||||
set `useCache` and `cache` properties to use Foundation `NSURLCache` system.
|
||||
|
||||
- Note: Uploading files and data are limited to 150MB, for now.
|
||||
*/
|
||||
open class DropboxFileProvider: FileProviderBasicRemote {
|
||||
open class var type: String { return "DropBox" }
|
||||
open let isPathRelative: Bool
|
||||
open let baseURL: URL?
|
||||
open var currentPath: String
|
||||
|
||||
@@ -47,6 +52,16 @@ open class DropboxFileProvider: FileProviderBasicRemote {
|
||||
return _session!
|
||||
}
|
||||
|
||||
fileprivate var _longpollSession: URLSession?
|
||||
internal var longpollSession: URLSession {
|
||||
if _longpollSession == nil {
|
||||
let config = URLSessionConfiguration.default
|
||||
config.timeoutIntervalForRequest = 600
|
||||
_longpollSession = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
|
||||
}
|
||||
return _longpollSession!
|
||||
}
|
||||
|
||||
/**
|
||||
Initializer for Dropbox provider with given client ID and Token.
|
||||
These parameters must be retrieved via [OAuth2 API of Dropbox](https://www.dropbox.com/developers/reference/oauth-guide).
|
||||
@@ -55,11 +70,10 @@ open class DropboxFileProvider: FileProviderBasicRemote {
|
||||
The latter is easier to use and prefered. Also you can use [auth0/Lock](https://github.com/auth0/Lock.iOS-OSX) which provides graphical user interface.
|
||||
|
||||
- Parameter credential: a `URLCredential` object with Client ID set as `user` and Token set as `password`.
|
||||
- Parameter cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used.
|
||||
- Parameter cache: A URLCache to cache downloaded files and contents.
|
||||
*/
|
||||
public init(credential: URLCredential?, cache: URLCache? = nil) {
|
||||
self.baseURL = nil
|
||||
self.isPathRelative = true
|
||||
self.currentPath = ""
|
||||
self.useCache = false
|
||||
self.validatingCache = true
|
||||
@@ -129,31 +143,62 @@ open class DropboxFileProvider: FileProviderBasicRemote {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
var foundFiles = [DropboxFileObject]()
|
||||
if let queryStr = query.findValue(forKey: "name", operator: .beginsWith) as? String {
|
||||
// Dropbox only support searching for file names begin with query in non-enterprise accounts.
|
||||
// We will use it if there is a `name BEGINSWITH[c] "query"` in predicate, then filter to form final result.
|
||||
search(path, query: queryStr, foundItem: { (file) in
|
||||
if query.evaluate(with: file.mapPredicate()) {
|
||||
foundFiles.append(file)
|
||||
foundItemHandler?(file)
|
||||
}
|
||||
}, completionHandler: { (error) in
|
||||
completionHandler(foundFiles, error)
|
||||
})
|
||||
} else {
|
||||
// Dropbox doesn't support searching attributes natively. The workaround is to fallback to listing all files
|
||||
// and filter it locally. It may have a network burden in case there is many files in Dropbox, so please use it concisely.
|
||||
list(path, recursive: true, progressHandler: { (files, _, error) in
|
||||
for file in files where query.evaluate(with: file.mapPredicate()) {
|
||||
foundItemHandler?(file)
|
||||
}
|
||||
}, completionHandler: { (files, _, error) in
|
||||
let predicatedFiles = files.filter { query.evaluate(with: $0.mapPredicate()) }
|
||||
completionHandler(predicatedFiles, error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
self.storageProperties { total, _ in
|
||||
completionHandler(total > 0)
|
||||
}
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate: FileOperationDelegate?
|
||||
}
|
||||
|
||||
extension DropboxFileProvider: FileProviderOperations {
|
||||
|
||||
|
||||
public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
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)
|
||||
}
|
||||
|
||||
public func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
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)
|
||||
}
|
||||
|
||||
public func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.remove(path: path), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@@ -201,7 +246,7 @@ extension DropboxFileProvider: FileProviderOperations {
|
||||
return RemoteOperationHandle(operationType: operation, tasks: [task])
|
||||
}
|
||||
|
||||
public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
@@ -209,7 +254,7 @@ extension DropboxFileProvider: FileProviderOperations {
|
||||
return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
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
|
||||
@@ -243,8 +288,11 @@ extension DropboxFileProvider: FileProviderOperations {
|
||||
}
|
||||
|
||||
extension DropboxFileProvider: FileProviderReadWrite {
|
||||
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
if length == 0 {
|
||||
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -282,16 +330,7 @@ extension DropboxFileProvider: FileProviderReadWrite {
|
||||
return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
var foundFiles = [DropboxFileObject]()
|
||||
search(path, query: query, foundItem: { (file) in
|
||||
foundFiles.append(file)
|
||||
foundItemHandler?(file)
|
||||
}, completionHandler: { (error) in
|
||||
completionHandler(foundFiles, error)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
|
||||
/* There is two ways to monitor folders changing in Dropbox. Either using webooks
|
||||
* which means you have to implement a server to translate it to push notifications
|
||||
@@ -304,13 +343,13 @@ extension DropboxFileProvider: FileProviderReadWrite {
|
||||
fileprivate func unregisterNotifcation(path: String) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
// TODO: Implement /copy_reference, /get_account & /get_current_account
|
||||
*/
|
||||
// TODO: Implement /get_account & /get_current_account
|
||||
}
|
||||
|
||||
extension DropboxFileProvider {
|
||||
/// *DEPRECATED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead.
|
||||
@available(*, deprecated, message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
|
||||
@available(*, deprecated, 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)
|
||||
@@ -318,7 +357,7 @@ extension DropboxFileProvider {
|
||||
}
|
||||
|
||||
/// *DEPRECATED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead.
|
||||
@available(*, deprecated, message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
|
||||
@available(*, deprecated, 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)
|
||||
@@ -332,7 +371,7 @@ extension DropboxFileProvider {
|
||||
|
||||
- Parameters:
|
||||
- to: path of file, including file/directory name.
|
||||
- completionHandler: a block with result of directory entries or error.
|
||||
- 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.
|
||||
@@ -375,7 +414,7 @@ extension DropboxFileProvider {
|
||||
- Parameters:
|
||||
- remoteURL: a valid remote url to file.
|
||||
- to: Destination path of file, including file/directory name.
|
||||
- completionHandler: a block with result of directory entries or error.
|
||||
- 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.
|
||||
@@ -440,7 +479,7 @@ extension DropboxFileProvider {
|
||||
}
|
||||
|
||||
extension DropboxFileProvider: ExtendedFileProvider {
|
||||
public func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
open func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
switch (path as NSString).pathExtension.lowercased() {
|
||||
case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff":
|
||||
return true
|
||||
@@ -455,7 +494,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public func propertiesOfFileSupported(path: String) -> Bool {
|
||||
open func propertiesOfFileSupported(path: String) -> Bool {
|
||||
let fileExt = (path as NSString).pathExtension.lowercased()
|
||||
switch fileExt {
|
||||
case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff":
|
||||
@@ -470,7 +509,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
}
|
||||
|
||||
/// Default value for dimension is 64x64, according to Dropbox documentation
|
||||
public func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
|
||||
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
|
||||
let url: URL
|
||||
switch (path as NSString).pathExtension.lowercased() {
|
||||
case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff":
|
||||
@@ -513,7 +552,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
|
||||
open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
|
||||
let url = URL(string: "files/get_metadata", relativeTo: apiURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
+24
-17
@@ -8,16 +8,14 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct FileProviderDropboxError: Error, CustomStringConvertible {
|
||||
/// Error returned by Dropbox server when trying to access or do operations on a file or folder.
|
||||
public struct FileProviderDropboxError: FileProviderHTTPError {
|
||||
public let code: FileProviderHTTPErrorCode
|
||||
public let path: String
|
||||
public let errorDescription: String?
|
||||
|
||||
public var description: String {
|
||||
return code.description
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
@@ -41,37 +39,43 @@ public final class DropboxFileObject: FileObject {
|
||||
self.rev = json["rev"] as? String
|
||||
}
|
||||
|
||||
/// The time contents of file has been modified on server, returns nil if not set
|
||||
open internal(set) var serverTime: Date? {
|
||||
get {
|
||||
return allValues["NSURLServerDateKey"] as? Date
|
||||
return allValues[.serverDate] as? Date
|
||||
}
|
||||
set {
|
||||
allValues["NSURLServerDateKey"] = newValue
|
||||
allValues[.serverDate] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The document identifier is a value assigned by the Dropbox to a file.
|
||||
/// This value is used to identify the document regardless of where it is moved on a volume.
|
||||
/// The identifier persists across system restarts.
|
||||
open internal(set) var id: String? {
|
||||
get {
|
||||
return allValues["NSURLDocumentIdentifyKey"] as? String
|
||||
return allValues[.documentIdentifierKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues["NSURLDocumentIdentifyKey"] = newValue
|
||||
allValues[.documentIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The revision of file, which changes when a file contents are modified.
|
||||
/// Changes to attributes or other file metadata do not change the identifier.
|
||||
open internal(set) var rev: String? {
|
||||
get {
|
||||
return allValues[URLResourceKey.generationIdentifierKey.rawValue] as? String
|
||||
return allValues[.generationIdentifierKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.generationIdentifierKey.rawValue] = newValue
|
||||
allValues[.generationIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
// codebeat:disable[ARITY]
|
||||
internal extension DropboxFileProvider {
|
||||
func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
|
||||
func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, session: URLSession? = nil, progressHandler: ((_ contents: [FileObject], _ nextCursor: String?, _ error: Error?) -> Void)? = nil, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
let url: URL
|
||||
if let cursor = cursor {
|
||||
@@ -87,15 +91,16 @@ internal extension DropboxFileProvider {
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
let task = (session ?? self.session).dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderDropboxError?
|
||||
var files = prevContents
|
||||
var files = [DropboxFileObject]()
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderDropboxError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8) {
|
||||
let json = jsonToDictionary(jsonStr)
|
||||
if let entries = json?["entries"] as? [AnyObject] , entries.count > 0 {
|
||||
files.reserveCapacity(entries.count)
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
|
||||
files.append(file)
|
||||
@@ -104,12 +109,14 @@ internal extension DropboxFileProvider {
|
||||
let ncursor = json?["cursor"] as? String
|
||||
let hasmore = (json?["has_more"] as? NSNumber)?.boolValue ?? false
|
||||
if hasmore {
|
||||
self.list(path, cursor: ncursor, prevContents: files, completionHandler: completionHandler)
|
||||
progressHandler?(files, ncursor, responseError ?? error)
|
||||
self.list(path, cursor: ncursor, prevContents: prevContents + files, completionHandler: completionHandler)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
completionHandler(files, nil, responseError ?? error)
|
||||
progressHandler?(files, nil, responseError ?? error)
|
||||
completionHandler(prevContents + files, nil, responseError ?? error)
|
||||
})
|
||||
task.taskDescription = FileOperationType.fetch(path: path).json
|
||||
task.resume()
|
||||
|
||||
@@ -10,14 +10,8 @@ import Foundation
|
||||
import ImageIO
|
||||
import CoreGraphics
|
||||
import AVFoundation
|
||||
#if os(iOS) || os(tvOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import Cocoa
|
||||
#endif
|
||||
|
||||
extension LocalFileProvider: ExtendedFileProvider {
|
||||
|
||||
public func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
switch (path as NSString).pathExtension.lowercased() {
|
||||
case LocalFileInformationGenerator.imageThumbnailExtensions:
|
||||
@@ -123,30 +117,83 @@ extension LocalFileProvider: ExtendedFileProvider {
|
||||
|
||||
completionHandler(dic, keys, nil)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds supported file types and thumbnail/properties generator for specefied type of file
|
||||
public struct LocalFileInformationGenerator {
|
||||
/// Image extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]`
|
||||
static public var imageThumbnailExtensions: [String] = ["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]
|
||||
static public var audioThumbnailExtensions: [String] = ["mp3", "aac", "m4a"]
|
||||
static public var videoThumbnailExtensions: [String] = ["mov", "mp4", "m4v", "mpg", "mpeg"]
|
||||
static public var pdfThumbnailExtensions: [String] = ["pdf"]
|
||||
static public var officeThumbnailExtensions: [String] = []
|
||||
static public var customThumbnailExtensions: [String] = []
|
||||
|
||||
/// Audio and music extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["mp3", "aac", "m4a"]`
|
||||
static public var audioThumbnailExtensions: [String] = ["mp3", "aac", "m4a"]
|
||||
|
||||
/// Video extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["mov", "mp4", "m4v", "mpg", "mpeg"]`
|
||||
static public var videoThumbnailExtensions: [String] = ["mov", "mp4", "m4v", "mpg", "mpeg"]
|
||||
|
||||
/// Portable document file extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["pdf"]`
|
||||
static public var pdfThumbnailExtensions: [String] = ["pdf"]
|
||||
|
||||
/// Office document extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var officeThumbnailExtensions: [String] = []
|
||||
|
||||
/// Custom document extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var customThumbnailExtensions: [String] = []
|
||||
|
||||
|
||||
/// Image extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff"]`
|
||||
static public var imagePropertiesExtensions: [String] = ["jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff"]
|
||||
|
||||
/// Audio and music extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["mp3", "aac", "m4a", "caf"]`
|
||||
static public var audioPropertiesExtensions: [String] = ["mp3", "aac", "m4a", "caf"]
|
||||
|
||||
/// Video extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["mp4", "mpg", "3gp", "mov", "avi"]`
|
||||
static public var videoPropertiesExtensions: [String] = ["mp4", "mpg", "3gp", "mov", "avi"]
|
||||
|
||||
/// Portable document file extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["pdf"]`
|
||||
static public var pdfPropertiesExtensions: [String] = ["pdf"]
|
||||
|
||||
/// Archive extensions (like zip) supportes for properties.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var archivePropertiesExtensions: [String] = []
|
||||
|
||||
/// Office document extensions supportes for properties.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var officePropertiesExtensions: [String] = []
|
||||
|
||||
/// Custom document extensions supportes for properties.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var customPropertiesExtensions: [String] = []
|
||||
|
||||
/// Thumbnail generator closure for image files.
|
||||
static public var imageThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
return ImageClass(contentsOfFile: fileURL.path)
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for audio and music files.
|
||||
static public var audioThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
let playerItem = AVPlayerItem(url: fileURL)
|
||||
let metadataList = playerItem.asset.commonMetadata
|
||||
@@ -160,6 +207,7 @@ public struct LocalFileInformationGenerator {
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for video files.
|
||||
static public var videoThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
let asset = AVAsset(url: fileURL)
|
||||
let assetImgGenerate = AVAssetImageGenerator(asset: asset)
|
||||
@@ -167,7 +215,7 @@ public struct LocalFileInformationGenerator {
|
||||
let time = CMTimeMake(asset.duration.value / 3, asset.duration.timescale)
|
||||
if let cgImage = try? assetImgGenerate.copyCGImage(at: time, actualTime: nil) {
|
||||
#if os(macOS)
|
||||
return ImageClass(cgImage: cgImage, size: NSSize.zero)
|
||||
return ImageClass(cgImage: cgImage, size: .zero)
|
||||
#else
|
||||
return ImageClass(cgImage: cgImage)
|
||||
#endif
|
||||
@@ -175,20 +223,36 @@ public struct LocalFileInformationGenerator {
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for portable document files files.
|
||||
static public var pdfThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
guard let data = try? Data(contentsOf: fileURL) else { return nil }
|
||||
return LocalFileProvider.convertToImage(pdfData: data)
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for office document files.
|
||||
/// - Note: No default implementation is avaiable
|
||||
static public var officeThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for custom type of files.
|
||||
/// - Note: No default implementation is avaiable
|
||||
static public var customThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Properties generator closure for image files.
|
||||
static public var imageProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value, !((value as? String)?.isEmpty ?? false) {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func simplify(_ top:Int64, _ bottom:Int64) -> (newTop:Int, newBottom:Int) {
|
||||
var x = top
|
||||
var y = bottom
|
||||
@@ -203,79 +267,55 @@ public struct LocalFileInformationGenerator {
|
||||
return(Int(newTopVal), Int(newBottomVal))
|
||||
}
|
||||
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
guard let cgDataRef = CGImageSourceCreateWithURL(fileURL as CFURL, nil), let cfImageDict = CGImageSourceCopyPropertiesAtIndex(cgDataRef, 0, nil) else {
|
||||
return (dic, keys)
|
||||
}
|
||||
let imageDict = cfImageDict as NSDictionary
|
||||
let tiffDict = imageDict[kCGImagePropertyTIFFDictionary as String] as? [String : AnyObject] ?? [:]
|
||||
let exifDict = imageDict[kCGImagePropertyExifDictionary as String] as? [String : AnyObject] ?? [:]
|
||||
if let pixelWidth: AnyObject = imageDict.object(forKey: kCGImagePropertyPixelWidth) as? NSNumber, let pixelHeight: AnyObject = imageDict.object(forKey: kCGImagePropertyPixelHeight) as? NSNumber {
|
||||
keys.append("Dimensions")
|
||||
dic["Dimensions"] = "\(pixelWidth)x\(pixelHeight)"
|
||||
let tiffDict = imageDict[kCGImagePropertyTIFFDictionary as String] as? NSDictionary ?? [:]
|
||||
let exifDict = imageDict[kCGImagePropertyExifDictionary as String] as? NSDictionary ?? [:]
|
||||
if let pixelWidth = imageDict.object(forKey: kCGImagePropertyPixelWidth) as? NSNumber, let pixelHeight = imageDict.object(forKey: kCGImagePropertyPixelHeight) as? NSNumber {
|
||||
add(key: "Dimensions", value: "\(pixelWidth)x\(pixelHeight)")
|
||||
}
|
||||
if let dpi = imageDict[kCGImagePropertyDPIWidth as String] {
|
||||
keys.append("DPI")
|
||||
dic["DPI"] = dpi
|
||||
|
||||
add(key: "DPI", value: imageDict[kCGImagePropertyDPIWidth as String])
|
||||
add(key: "Device make", value: tiffDict[kCGImagePropertyTIFFMake as String])
|
||||
add(key: "Device model", value: tiffDict[kCGImagePropertyTIFFModel as String])
|
||||
add(key: "Lens model", value: exifDict[kCGImagePropertyExifLensModel as String])
|
||||
add(key: "Artist", value: tiffDict[kCGImagePropertyTIFFArtist as String] as? String)
|
||||
add(key: "Copyright", value: tiffDict[kCGImagePropertyTIFFCopyright as String] as? String)
|
||||
add(key: "Date taken", value: tiffDict[kCGImagePropertyTIFFDateTime as String] as? String)
|
||||
|
||||
if let latitude = tiffDict[kCGImagePropertyGPSLatitude as String] as? NSNumber, let longitude = tiffDict[kCGImagePropertyGPSLongitude as String] as? NSNumber {
|
||||
add(key: "Location", value: "\(latitude), \(longitude)")
|
||||
}
|
||||
if let devicemake = tiffDict[kCGImagePropertyTIFFMake as String] {
|
||||
keys.append("Device make")
|
||||
dic["Device make"] = devicemake
|
||||
}
|
||||
if let devicemodel = tiffDict[kCGImagePropertyTIFFModel as String] {
|
||||
keys.append("Device model")
|
||||
dic["Device model"] = devicemodel
|
||||
}
|
||||
if let lensmodel = exifDict[kCGImagePropertyExifLensModel as String] {
|
||||
keys.append("Lens model")
|
||||
dic["Lens model"] = lensmodel
|
||||
}
|
||||
if let artist = tiffDict[kCGImagePropertyTIFFArtist as String] as? String , !artist.isEmpty {
|
||||
keys.append("Artist")
|
||||
dic["Artist"] = artist
|
||||
}
|
||||
if let cr = tiffDict[kCGImagePropertyTIFFCopyright as String] as? String , !cr.isEmpty {
|
||||
keys.append("Copyright")
|
||||
dic["Copyright"] = cr
|
||||
}
|
||||
if let date = tiffDict[kCGImagePropertyTIFFDateTime as String] as? String , !date.isEmpty {
|
||||
keys.append("Date taken")
|
||||
dic["Date taken"] = date
|
||||
}
|
||||
if let latitude = tiffDict[kCGImagePropertyGPSLatitude as String]?.doubleValue, let longitude = tiffDict[kCGImagePropertyGPSLongitude as String]?.doubleValue {
|
||||
keys.append("Location")
|
||||
dic["Location"] = "\(latitude), \(longitude)"
|
||||
}
|
||||
if let colorspace = imageDict[kCGImagePropertyColorModel as String] {
|
||||
keys.append("Color space")
|
||||
dic["Color space"] = colorspace
|
||||
}
|
||||
if let focallen = exifDict[kCGImagePropertyExifFocalLength as String] {
|
||||
keys.append("Focal length")
|
||||
dic["Focal length"] = focallen
|
||||
}
|
||||
if let fnum = exifDict[kCGImagePropertyExifFNumber as String] {
|
||||
keys.append("F number")
|
||||
dic["F number"] = fnum
|
||||
}
|
||||
if let expprog = exifDict[kCGImagePropertyExifExposureProgram as String] {
|
||||
keys.append("Exposure program")
|
||||
dic["Exposure program"] = expprog
|
||||
}
|
||||
if let exp = exifDict[kCGImagePropertyExifExposureTime as String]?.doubleValue {
|
||||
let expfrac = simplify(Int64(exp * 10_000_000_000_000), 10_000_000_000_000)
|
||||
keys.append("Exposure time")
|
||||
dic["Exposure time"] = "\(expfrac.newTop)/\(expfrac.newBottom)"
|
||||
}
|
||||
if let iso = exifDict[kCGImagePropertyExifISOSpeedRatings as String] as? NSArray , iso.count > 0 {
|
||||
keys.append("ISO speed")
|
||||
dic["ISO speed"] = iso[0]
|
||||
add(key: "Altitude", value: tiffDict[kCGImagePropertyGPSAltitude as String] as? NSNumber)
|
||||
add(key: "Area", value: tiffDict[kCGImagePropertyGPSAreaInformation as String] as? NSNumber)
|
||||
|
||||
add(key: "Color space", value: imageDict[kCGImagePropertyColorModel as String])
|
||||
add(key: "Focal length", value: exifDict[kCGImagePropertyExifFocalLength as String])
|
||||
add(key: "F number", value: exifDict[kCGImagePropertyExifFNumber as String])
|
||||
add(key: "Exposure program", value: exifDict[kCGImagePropertyExifExposureProgram as String])
|
||||
|
||||
if let exp = exifDict[kCGImagePropertyExifExposureTime as String] as? NSNumber {
|
||||
let expfrac = simplify(Int64(exp.doubleValue * 10_000_000_000_000), 10_000_000_000_000)
|
||||
add(key: "Exposure time", value: "\(expfrac.newTop)/\(expfrac.newBottom)")
|
||||
}
|
||||
add(key: "ISO speed", value: (exifDict[kCGImagePropertyExifISOSpeedRatings as String] as? NSArray)?.first)
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
/// Properties generator closure for audio and music files.
|
||||
static var audioProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func makeDescription(_ key: String?) -> String? {
|
||||
guard let key = key else {
|
||||
return nil
|
||||
@@ -287,8 +327,6 @@ public struct LocalFileInformationGenerator {
|
||||
return newKey.capitalized
|
||||
}
|
||||
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
if FileManager.default.fileExists(atPath: fileURL.path) {
|
||||
let playerItem = AVPlayerItem(url: fileURL)
|
||||
let metadataList = playerItem.asset.commonMetadata
|
||||
@@ -301,20 +339,25 @@ public struct LocalFileInformationGenerator {
|
||||
}
|
||||
}
|
||||
if let ap = try? AVAudioPlayer(contentsOf: fileURL) {
|
||||
keys.append("Duration")
|
||||
dic["Duration"] = LocalFileProvider.formatshort(interval: ap.duration)
|
||||
if let bitRate = ap.settings[AVSampleRateKey] as? Int {
|
||||
keys.append("Bitrate")
|
||||
dic["Bitrate"] = bitRate
|
||||
}
|
||||
add(key: "Duration", value: LocalFileProvider.formatshort(interval: ap.duration))
|
||||
add(key: "Bitrate", value: ap.settings[AVSampleRateKey] as? Int)
|
||||
}
|
||||
}
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
/// Properties generator closure for video files.
|
||||
static public var videoProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if let audioprops = LocalFileInformationGenerator.audioProperties?(fileURL) {
|
||||
dic = audioprops.prop
|
||||
keys = audioprops.keys
|
||||
@@ -325,21 +368,18 @@ public struct LocalFileInformationGenerator {
|
||||
}
|
||||
let asset = AVURLAsset(url: fileURL, options: nil)
|
||||
let videoTracks = asset.tracks(withMediaType: AVMediaTypeVideo)
|
||||
if videoTracks.count > 0 {
|
||||
if let videoTrack = videoTracks.first {
|
||||
var bitrate: Float = 0
|
||||
let width = Int(videoTracks[0].naturalSize.width)
|
||||
let height = Int(videoTracks[0].naturalSize.height)
|
||||
keys.append("Dimensions")
|
||||
dic["Dimensions"] = "\(width)x\(height)"
|
||||
let width = Int(videoTrack.naturalSize.width)
|
||||
let height = Int(videoTrack.naturalSize.height)
|
||||
add(key: "Dimensions", value: "\(width)x\(height)")
|
||||
var duration: Int64 = 0
|
||||
for track in videoTracks {
|
||||
duration += track.timeRange.duration.timescale > 0 ? track.timeRange.duration.value / Int64(track.timeRange.duration.timescale) : 0
|
||||
bitrate += track.estimatedDataRate
|
||||
}
|
||||
keys.append("Duration")
|
||||
dic["Duration"] = LocalFileProvider.formatshort(interval: TimeInterval(duration))
|
||||
keys.append("Video Bitrate")
|
||||
dic["Video Bitrate"] = "\(Int(ceil(bitrate / 1000))) kbps"
|
||||
add(key: "Duration", value: LocalFileProvider.formatshort(interval: TimeInterval(duration)))
|
||||
add(key: "Video Bitrate", value: "\(Int(ceil(bitrate / 1000))) kbps")
|
||||
}
|
||||
let audioTracks = asset.tracks(withMediaType: AVMediaTypeAudio)
|
||||
// dic["Audio channels"] = audioTracks.count
|
||||
@@ -347,12 +387,22 @@ public struct LocalFileInformationGenerator {
|
||||
for track in audioTracks {
|
||||
bitrate += track.estimatedDataRate
|
||||
}
|
||||
keys.append("Audio Bitrate")
|
||||
dic["Audio Bitrate"] = "\(Int(ceil(bitrate / 1000))) kbps"
|
||||
add(key: "Audio Bitrate", value: "\(Int(ceil(bitrate / 1000))) kbps")
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
/// Properties generator closure for protable documents files.
|
||||
static public var pdfProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value, !((value as? String)?.isEmpty ?? false) {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func getKey(_ key: String, from dict: CGPDFDictionaryRef) -> String? {
|
||||
var cfValue: CGPDFStringRef? = nil
|
||||
if (CGPDFDictionaryGetString(dict, key, &cfValue)), let value = CGPDFStringCopyTextString(cfValue!) {
|
||||
@@ -361,7 +411,8 @@ public struct LocalFileInformationGenerator {
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertDate(_ date: String) -> Date? {
|
||||
func convertDate(_ date: String?) -> Date? {
|
||||
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))
|
||||
@@ -378,64 +429,42 @@ public struct LocalFileInformationGenerator {
|
||||
return nil
|
||||
}
|
||||
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
if let data = try? Data(contentsOf: fileURL), let provider = CGDataProvider(data: data as CFData), let reference = CGPDFDocument(provider), let dict = reference.info {
|
||||
if let title = getKey("Title", from: dict), !title.isEmpty {
|
||||
keys.append("Title")
|
||||
dic["Title"] = title
|
||||
}
|
||||
if let author = getKey("Author", from: dict), !author.isEmpty {
|
||||
keys.append("Author")
|
||||
dic["Author"] = author
|
||||
}
|
||||
if let subject = getKey("Subject", from: dict), !subject.isEmpty {
|
||||
keys.append("Subject")
|
||||
dic["Subject"] = subject
|
||||
}
|
||||
add(key: "Title", value: getKey("Title", from: dict))
|
||||
add(key: "Author", value: getKey("Author", from: dict))
|
||||
add(key: "Subject", value: getKey("Subject", from: dict))
|
||||
var majorVersion: Int32 = 0
|
||||
var minorVersion: Int32 = 0
|
||||
reference.getVersion(majorVersion: &majorVersion, minorVersion: &minorVersion)
|
||||
if majorVersion > 0 {
|
||||
keys.append("Version")
|
||||
dic["Version"] = String(majorVersion) + "." + String(minorVersion)
|
||||
}
|
||||
if reference.numberOfPages > 0 {
|
||||
keys.append("Pages")
|
||||
dic["Pages"] = reference.numberOfPages
|
||||
add(key: "Version", value: String(majorVersion) + "." + String(minorVersion))
|
||||
}
|
||||
add(key: "Pages", value: reference.numberOfPages)
|
||||
|
||||
if reference.numberOfPages > 0, let pageRef = reference.page(at: 1) {
|
||||
let size = pageRef.getBoxRect(CGPDFBox.mediaBox).size
|
||||
keys.append("Resolution")
|
||||
dic["Resolution"] = "\(Int(size.width))x\(Int(size.height))"
|
||||
add(key: "Resolution", value: "\(Int(size.width))x\(Int(size.height))")
|
||||
}
|
||||
if let creator = getKey("Creator", from: dict), !creator.isEmpty {
|
||||
keys.append("Content creator")
|
||||
dic["Content creator"] = creator
|
||||
}
|
||||
if let creationDateString = getKey("CreationDate", from: dict), let creationDate = convertDate(creationDateString) {
|
||||
keys.append("Creation date")
|
||||
dic["Creation date"] = creationDate
|
||||
}
|
||||
if let modifiedDateString = getKey("ModDate", from: dict), let modDate = convertDate(modifiedDateString) {
|
||||
keys.append("Modified date")
|
||||
dic["Modified date"] = modDate
|
||||
}
|
||||
keys.append("Security")
|
||||
dic["Security"] = reference.isEncrypted ? "Present" : "None"
|
||||
keys.append("Allows printing")
|
||||
dic["Allows printing"] = reference.allowsPrinting ? "Yes" : "No"
|
||||
keys.append("Allows copying")
|
||||
dic["Allows copying"] = reference.allowsCopying ? "Yes" : "No"
|
||||
add(key: "Content creator", value: getKey("Creator", from: dict))
|
||||
add(key: "Creation date", value: convertDate(getKey("CreationDate", from: dict)))
|
||||
add(key: "Modified date", value: convertDate(getKey("ModDate", from: dict)))
|
||||
add(key: "Security", value: reference.isEncrypted ? "Present" : "None")
|
||||
add(key: "Allows printing", value: reference.allowsPrinting ? "Yes" : "No")
|
||||
add(key: "Allows copying", value: reference.allowsCopying ? "Yes" : "No")
|
||||
}
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
/// Properties generator closure for video files.
|
||||
/// - Note: No default implementation is avaiable
|
||||
static public var archiveProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
|
||||
|
||||
/// Properties generator closure for office doument files.
|
||||
/// - Note: No default implementation is avaiable
|
||||
static public var officeProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
|
||||
|
||||
/// Properties generator closure for custom type of files.
|
||||
/// - Note: No default implementation is avaiable
|
||||
static public var customProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@ 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.
|
||||
@objc
|
||||
open class FPSStreamTask: URLSessionTask, StreamDelegate {
|
||||
internal class FPSStreamTask: URLSessionTask, StreamDelegate {
|
||||
fileprivate var inputStream: InputStream?
|
||||
fileprivate var outputStream: OutputStream?
|
||||
|
||||
@@ -449,23 +448,23 @@ open class FPSStreamTask: URLSessionTask, StreamDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
extension URLSession {
|
||||
internal extension URLSession {
|
||||
/* Creates a bidirectional stream task to a given host and port.
|
||||
*/
|
||||
public func fpstreamTaskWithHostName(_ hostname: String, port: Int) -> FPSStreamTask {
|
||||
func fpstreamTaskWithHostName(_ hostname: String, port: Int) -> FPSStreamTask {
|
||||
return FPSStreamTask(session: self, host: hostname, port: port)
|
||||
}
|
||||
|
||||
/* Creates a bidirectional stream task with an NSNetService to identify the endpoint.
|
||||
* The NSNetService will be resolved before any IO completes.
|
||||
*/
|
||||
public func fpstreamTaskWithNetService(_ service: NetService) -> FPSStreamTask {
|
||||
func fpstreamTaskWithNetService(_ service: NetService) -> FPSStreamTask {
|
||||
return fpstreamTaskWithNetService(service)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public protocol FPSStreamDelegate : URLSessionTaskDelegate {
|
||||
internal protocol FPSStreamDelegate : URLSessionTaskDelegate {
|
||||
|
||||
|
||||
/* Indiciates that the read side of a connection has been closed. Any
|
||||
|
||||
+97
-35
@@ -8,24 +8,24 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Containts path and attributes of a file or resource.
|
||||
open class FileObject {
|
||||
/// Containts path, url and attributes of a file or resource.
|
||||
open class FileObject: Equatable {
|
||||
/// A `Dictionary` contains file information, using `URLResourceKey` keys.
|
||||
open internal(set) var allValues: [String: Any]
|
||||
open internal(set) var allValues: [URLResourceKey: Any]
|
||||
|
||||
internal init(allValues: [String: Any]) {
|
||||
internal init(allValues: [URLResourceKey: Any]) {
|
||||
self.allValues = allValues
|
||||
}
|
||||
|
||||
internal init(url: URL, name: String, path: String) {
|
||||
self.allValues = [String: Any]()
|
||||
self.allValues = [URLResourceKey: Any]()
|
||||
self.url = url
|
||||
self.name = name
|
||||
self.path = path
|
||||
}
|
||||
|
||||
/// url to access the resource, not supported by Dropbox provider
|
||||
@available(*, deprecated, message: "Use url.absoluteURL instead.")
|
||||
@available(*, deprecated, renamed: "url", message: "Use url.absoluteURL instead.")
|
||||
open var absoluteURL: URL? {
|
||||
return url?.absoluteURL
|
||||
}
|
||||
@@ -34,97 +34,91 @@ open class FileObject {
|
||||
/// not supported by Dropbox provider.
|
||||
open internal(set) var url: URL? {
|
||||
get {
|
||||
return allValues["NSURLFileURLKey"] as? URL
|
||||
return allValues[.fileURL] as? URL
|
||||
}
|
||||
set {
|
||||
allValues["NSURLFileURLKey"] = newValue
|
||||
allValues[.fileURL] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Name of the file, usually equals with the last path component
|
||||
open internal(set) var name: String {
|
||||
get {
|
||||
return allValues[URLResourceKey.nameKey.rawValue] as! String
|
||||
return allValues[.nameKey] as! String
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.nameKey.rawValue] = newValue
|
||||
allValues[.nameKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Relative path of file object
|
||||
open internal(set) var path: String {
|
||||
get {
|
||||
return allValues[URLResourceKey.pathKey.rawValue] as! String
|
||||
return allValues[.pathKey] as! String
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.pathKey.rawValue] = newValue
|
||||
allValues[.pathKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Size of file on disk, return -1 for directories.
|
||||
open internal(set) var size: Int64 {
|
||||
get {
|
||||
return allValues[URLResourceKey.fileSizeKey.rawValue] as? Int64 ?? -1
|
||||
return allValues[.fileSizeKey] as? Int64 ?? -1
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.fileSizeKey.rawValue] = newValue
|
||||
allValues[.fileSizeKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The time contents of file has been created, returns nil if not set
|
||||
open internal(set) var creationDate: Date? {
|
||||
get {
|
||||
return allValues[URLResourceKey.creationDateKey.rawValue] as? Date
|
||||
return allValues[.creationDateKey] as? Date
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.creationDateKey.rawValue] = newValue
|
||||
allValues[.creationDateKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The time contents of file has been modified, returns nil if not set
|
||||
open internal(set) var modifiedDate: Date? {
|
||||
get {
|
||||
return allValues[URLResourceKey.contentModificationDateKey.rawValue] as? Date
|
||||
return allValues[.contentModificationDateKey] as? Date
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.contentModificationDateKey.rawValue] = newValue
|
||||
allValues[.contentModificationDateKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// return resource type of file, usually directory, regular or symLink
|
||||
open internal(set) var type: URLFileResourceType? {
|
||||
get {
|
||||
return allValues[URLResourceKey.fileResourceTypeKey.rawValue] as? URLFileResourceType
|
||||
return allValues[.fileResourceTypeKey] as? URLFileResourceType
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.fileResourceTypeKey.rawValue] = newValue
|
||||
allValues[.fileResourceTypeKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// **DEPRECATED:** Use `type` property instead.
|
||||
@available(*, deprecated, message: "Use type property instead.")
|
||||
open var fileType: URLFileResourceType? {
|
||||
return self.type
|
||||
}
|
||||
|
||||
/// File is hidden either because begining with dot or filesystem flags
|
||||
/// Setting this value on a file begining with dot has no effect
|
||||
open internal(set) var isHidden: Bool {
|
||||
get {
|
||||
return allValues[URLResourceKey.isHiddenKey.rawValue] as? Bool ?? false
|
||||
return allValues[.isHiddenKey] as? Bool ?? false
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.isHiddenKey.rawValue] = newValue
|
||||
allValues[.isHiddenKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// File can not be written
|
||||
open internal(set) var isReadOnly: Bool {
|
||||
get {
|
||||
return !(allValues[URLResourceKey.isWritableKey.rawValue] as? Bool ?? true)
|
||||
return !(allValues[.isWritableKey] as? Bool ?? true)
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.isWritableKey.rawValue] = !newValue
|
||||
allValues[.isWritableKey] = !newValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +136,66 @@ open class FileObject {
|
||||
open var isSymLink: Bool {
|
||||
return self.type == .symbolicLink
|
||||
}
|
||||
|
||||
/// Check `FileObject` equality
|
||||
public static func ==(lhs: FileObject, rhs: FileObject) -> Bool {
|
||||
if rhs === lhs {
|
||||
return true
|
||||
}
|
||||
if type(of: lhs) != type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
if let rurl = rhs.url, let lurl = lhs.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 typeDict: [URLFileResourceType: String] = [.directory: "directory", .regular: "regular", .symbolicLink: "symbolicLink", .unknown: "unknown"]
|
||||
var result = [String: Any]()
|
||||
for (key, value) in allValues {
|
||||
if let convertkey = mapDict[key] {
|
||||
result[convertkey] = value
|
||||
}
|
||||
}
|
||||
result["eTag"] = result["entryTag"]
|
||||
result["isReadOnly"] = self.isReadOnly
|
||||
result["isDirectory"] = self.isDirectory
|
||||
result["isRegularFile"] = self.isRegularFile
|
||||
result["isSymLink"] = self.isSymLink
|
||||
result["type"] = typeDict[self.type ?? .unknown] ?? "unknown"
|
||||
return result
|
||||
}
|
||||
|
||||
static public func convertPredicate(fromSpotlight query: NSPredicate) -> NSPredicate {
|
||||
let mapDict: [String: URLResourceKey] = [NSMetadataItemURLKey: .fileURL, NSMetadataItemFSNameKey: .nameKey, NSMetadataItemPathKey: .pathKey,
|
||||
NSMetadataItemFSSizeKey: .fileSizeKey, NSMetadataItemFSCreationDateKey: .creationDateKey,
|
||||
NSMetadataItemFSContentChangeDateKey: .contentModificationDateKey, "kMDItemFSInvisible": .isHiddenKey, "kMDItemFSIsWriteable": .isWritableKey, "kMDItemKind": .mimeType]
|
||||
|
||||
if let cQuery = query as? NSCompoundPredicate {
|
||||
let newSub = cQuery.subpredicates.map { convertPredicate(fromSpotlight: $0 as! NSPredicate) }
|
||||
switch cQuery.compoundPredicateType {
|
||||
case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub)
|
||||
case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub[0])
|
||||
case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub)
|
||||
}
|
||||
} else if let cQuery = query as? NSComparisonPredicate {
|
||||
var newLeft = cQuery.leftExpression
|
||||
var newRight = cQuery.rightExpression
|
||||
if newLeft.expressionType == .keyPath, let newKey = mapDict[newLeft.keyPath] {
|
||||
newLeft = NSExpression(forKeyPath: newKey.rawValue)
|
||||
}
|
||||
if newRight.expressionType == .keyPath, let newKey = mapDict[newRight.keyPath] {
|
||||
newRight = NSExpression(forKeyPath: newKey.rawValue)
|
||||
}
|
||||
return NSComparisonPredicate(leftExpression: newLeft, rightExpression: newRight, modifier: cQuery.comparisonPredicateModifier, type: cQuery.predicateOperatorType, options: cQuery.options)
|
||||
} else {
|
||||
return query
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal func resolve(dateString: String) -> Date? {
|
||||
@@ -231,7 +285,7 @@ public struct FileObjectSorting {
|
||||
self.isDirectoriesFirst = isDirectoriesFirst
|
||||
}
|
||||
|
||||
/// Sorts array of `FileObject`s by criterias set in properties
|
||||
/// Sorts array of `FileObject`s by criterias set in attributes.
|
||||
public func sort(_ files: [FileObject]) -> [FileObject] {
|
||||
return files.sorted {
|
||||
if isDirectoriesFirst {
|
||||
@@ -269,19 +323,20 @@ public struct FileObjectSorting {
|
||||
}
|
||||
|
||||
extension Array where Element: FileObject {
|
||||
/// Returns a sorted array of `FileObject`s by criterias set in properties.
|
||||
public func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) -> [Element] {
|
||||
/// Returns a sorted array of `FileObject`s by criterias set in attributes.
|
||||
public func sort(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) -> [Element] {
|
||||
let sorting = FileObjectSorting(type: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
|
||||
return sorting.sort(self) as! [Element]
|
||||
}
|
||||
|
||||
/// Sorts array of `FileObject`s by criterias set in properties
|
||||
/// Sorts array of `FileObject`s by criterias set in attributes.
|
||||
public mutating func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) {
|
||||
self = self.sorted(by: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
|
||||
self = self.sort(by: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
|
||||
}
|
||||
}
|
||||
|
||||
extension URLFileResourceType {
|
||||
/// Returns corresponding `URLFileResourceType` of a `FileAttributeType` value
|
||||
public init(fileTypeValue: FileAttributeType) {
|
||||
switch fileTypeValue {
|
||||
case FileAttributeType.typeCharacterSpecial: self = .characterSpecial
|
||||
@@ -296,6 +351,13 @@ 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")
|
||||
}
|
||||
|
||||
internal extension URL {
|
||||
var uw_scheme: String {
|
||||
return self.scheme ?? ""
|
||||
|
||||
+222
-61
@@ -15,8 +15,10 @@ import Cocoa
|
||||
public typealias ImageClass = NSImage
|
||||
#endif
|
||||
|
||||
/// Completion handler type with an error argument
|
||||
public typealias SimpleCompletionHandler = ((_ error: Error?) -> Void)?
|
||||
|
||||
/// This protocol defines FileProvider neccesary functions and properties to connect and get contents list
|
||||
public protocol FileProviderBasic: class {
|
||||
/// An string to identify type of provider.
|
||||
static var type: String { get }
|
||||
@@ -24,9 +26,6 @@ public protocol FileProviderBasic: class {
|
||||
/// An string to identify type of provider.
|
||||
var type: String { get }
|
||||
|
||||
/// The paths in arguments should resolved against base url.
|
||||
var isPathRelative: Bool { get }
|
||||
|
||||
/// The url of which paths should resolve against.
|
||||
var baseURL: URL? { get }
|
||||
|
||||
@@ -66,7 +65,7 @@ public protocol FileProviderBasic: class {
|
||||
If the directory contains no entries or an error is occured, this method will return the empty array.
|
||||
|
||||
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
|
||||
- Parameter completionHandler: a block with result of directory entries or error.
|
||||
- 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.
|
||||
*/
|
||||
@@ -78,16 +77,53 @@ public protocol FileProviderBasic: class {
|
||||
If the directory contains no entries or an error is occured, this method will return the empty `FileObject`.
|
||||
|
||||
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
|
||||
- Parameter completionHandler: a block with result of directory entries or error.
|
||||
- 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.
|
||||
*/
|
||||
func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void))
|
||||
|
||||
|
||||
/// Returns total and used space in provider container asynchronously.
|
||||
/// Returns total and used capacity in provider container asynchronously.
|
||||
func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void))
|
||||
|
||||
/**
|
||||
Search files inside directory using query asynchronously.
|
||||
|
||||
- Note: Query string is limited to file name, to search based on other file attributes, use NSPredicate version.
|
||||
|
||||
- Parameters:
|
||||
- path: location of directory to start search
|
||||
- recursive: Searching subdirectories of path
|
||||
- query: Simple string that file name begins with to be search, case-insensitive.
|
||||
- foundItemHandler: Closure which is called when a file is found
|
||||
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
|
||||
*/
|
||||
func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void))
|
||||
|
||||
/**
|
||||
Search files inside directory using query asynchronously.
|
||||
|
||||
Sample predicates:
|
||||
```
|
||||
NSPredicate(format: "(name CONTAINS[c] 'hello') && (filesize >= 10000)")
|
||||
NSPredicate(format: "(modifiedDate >= %@)", Date())
|
||||
NSPredicate(format: "(path BEGINSWITH %@)", "folder/child folder")
|
||||
```
|
||||
|
||||
- Note: Don't pass Spotlight predicates to this method directly, use `FileProvider.convertSpotlightPredicateTo()` method to get usable predicate.
|
||||
|
||||
- Important: A file name criteria should be provided for Dropbox.
|
||||
|
||||
- Parameters:
|
||||
- path: location of directory to start search
|
||||
- recursive: Searching subdirectories of path
|
||||
- query: An `NSPredicate` object with keys like `FileObject` members, except `size` which becomes `filesize`.
|
||||
- foundItemHandler: Closure which is called when a file is found
|
||||
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
|
||||
*/
|
||||
func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void))
|
||||
|
||||
/**
|
||||
Returns an independent url to access the file. Some providers like `Dropbox` due to their nature.
|
||||
don't return an absolute url to be used to access file directly.
|
||||
@@ -95,9 +131,23 @@ public protocol FileProviderBasic: class {
|
||||
- Returns: An url, can be used to access to file directly.
|
||||
*/
|
||||
func url(of path: String?) -> URL
|
||||
|
||||
/// Checks the connection to server or permission on local
|
||||
func isReachable(completionHandler: @escaping(_ success: Bool) -> Void)
|
||||
}
|
||||
|
||||
public extension FileProviderBasic {
|
||||
extension FileProviderBasic {
|
||||
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
let predicate = NSPredicate(format: "name BEHINSWITH[c] %@", query)
|
||||
self.searchFiles(path: path, recursive: recursive, query: predicate, foundItemHandler: foundItemHandler, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/// Converts Spotlight search predicate to `FileProvider.searchFiles()` method usable predicate.
|
||||
@available(*, obsoleted: 1.0, renamed: "FileObject.convertProdicate(fromSpotlight:)", message: "Use FileObject.convertProdicate(fromSpotlight:) instead.")
|
||||
public func convertSpotlightPredicateTo(_ query: NSPredicate) -> NSPredicate {
|
||||
return FileObject.convertPredicate(fromSpotlight: query)
|
||||
}
|
||||
|
||||
/// The maximum number of queued operations that can execute at the same time.
|
||||
///
|
||||
/// The default value of this property is `OperationQueue.defaultMaxConcurrentOperationCount`.
|
||||
@@ -111,16 +161,35 @@ public extension FileProviderBasic {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checking equality of two file provider, regardless of current path queues and delegates.
|
||||
public func ==(lhs: FileProviderBasic, rhs: FileProviderBasic) -> Bool {
|
||||
if lhs === rhs { return true }
|
||||
if type(of: lhs) != type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
return lhs.type == rhs.type && lhs.baseURL == rhs.baseURL && lhs.credential == rhs.credential
|
||||
}
|
||||
|
||||
/// Cancels all active underlying tasks
|
||||
public var fileProviderCancelTasksOnInvalidating = true
|
||||
|
||||
/// Extending `FileProviderBasic` for web-based file providers
|
||||
public protocol FileProviderBasicRemote: FileProviderBasic {
|
||||
/// Underlying URLSession instance used for HTTP/S requests
|
||||
var session: URLSession { get }
|
||||
|
||||
/// A `URLCache` to cache downloaded files and contents.
|
||||
///
|
||||
/// If set to nil, `URLCache.shared` object will be used.
|
||||
/// - Note: It has no effect unless setting `useCache` property to `true`.
|
||||
/**
|
||||
A `URLCache` to cache downloaded files and contents.
|
||||
|
||||
- Note: It has no effect unless setting `useCache` property to `true`.
|
||||
|
||||
- Warning: FileProvider doesn't manage/free `URLCache` object in a memory pressure scenario. It's upon you to clear
|
||||
cache memory when receiving `didReceiveMemoryWarning` or via observing `.UIApplicationDidReceiveMemoryWarning` notification.
|
||||
To clear memory usage use this code:
|
||||
```
|
||||
provider.cache?.removeAllCachedResponses()
|
||||
```
|
||||
*/
|
||||
var cache: URLCache? { get }
|
||||
|
||||
/// Determine to use `cache` property to cache downloaded file objects. Doesn't have effect on query type methods.
|
||||
@@ -178,6 +247,7 @@ internal extension FileProviderBasicRemote {
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines methods for common file operaions including create, copy/move and delete
|
||||
public protocol FileProviderOperations: FileProviderBasic {
|
||||
/// Delgate for managing operations involving the copying, moving, linking, or removal of files and directories. When you use an FileManager object to initiate a copy, move, link, or remove operation, the file provider asks its delegate whether the operation should begin at all and whether it should proceed when an error occurs.
|
||||
var fileOperationDelegate : FileOperationDelegate? { get set }
|
||||
@@ -190,6 +260,7 @@ public protocol FileProviderOperations: FileProviderBasic {
|
||||
- folder: Directory name.
|
||||
- at: Parent path of new directory.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func create(folder: String, at: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
@@ -206,7 +277,6 @@ public protocol FileProviderOperations: FileProviderBasic {
|
||||
- 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?
|
||||
|
||||
/**
|
||||
@@ -337,6 +407,7 @@ extension FileProviderOperations {
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines method for fetching and modifying file contents
|
||||
public protocol FileProviderReadWrite: FileProviderBasic {
|
||||
/**
|
||||
Retreives a `Data` object with the contents of the file asynchronously vis contents argument of completion handler.
|
||||
@@ -344,7 +415,7 @@ public protocol FileProviderReadWrite: FileProviderBasic {
|
||||
|
||||
- Parameters:
|
||||
- path: Path of file.
|
||||
- completionHandler: a block with result of file contents or error.
|
||||
- completionHandler: a closure with result of file contents or error.
|
||||
`contents`: contents of file in a `Data` object.
|
||||
`error`: Error returned by system.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
@@ -360,7 +431,7 @@ public protocol FileProviderReadWrite: FileProviderBasic {
|
||||
- path: Path of file.
|
||||
- offset: First byte index which should be read. **Starts from 0.**
|
||||
- length: Bytes count of data. Pass `-1` to read until the end of file.
|
||||
- completionHandler: a block with result of file contents or error.
|
||||
- completionHandler: a closure with result of file contents or error.
|
||||
`contents`: contents of file in a `Data` object.
|
||||
`error`: Error returned by system.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
@@ -423,20 +494,6 @@ public protocol FileProviderReadWrite: FileProviderBasic {
|
||||
*/
|
||||
@discardableResult
|
||||
func writeContents(path: String, contents: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Search files inside directory using query asynchronously.
|
||||
|
||||
- Note: For now only it's limited to file names. `query` parameter may take `NSPredicate` format in near future.
|
||||
|
||||
- Parameters:
|
||||
- path: location of directory to start search
|
||||
- recursive: Searching subdirectories of path
|
||||
- query: Simple string of file name to be search (for now).
|
||||
- foundItemHandler: Block which is called when a file is found
|
||||
- completionHandler: Block which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
|
||||
*/
|
||||
func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void))
|
||||
}
|
||||
|
||||
extension FileProviderReadWrite {
|
||||
@@ -461,6 +518,7 @@ extension FileProviderReadWrite {
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows a file provider to notify changes occured
|
||||
public protocol FileProviderMonitor: FileProviderBasic {
|
||||
|
||||
/**
|
||||
@@ -476,7 +534,7 @@ public protocol FileProviderMonitor: FileProviderBasic {
|
||||
|
||||
- Parameters:
|
||||
- path: path of directory.
|
||||
- eventHandler: Block executed after change, on a secondary thread.
|
||||
- eventHandler: Closure executed after change, on a secondary thread.
|
||||
*/
|
||||
func registerNotifcation(path: String, eventHandler: @escaping (() -> Void))
|
||||
|
||||
@@ -492,40 +550,76 @@ public protocol FileProviderMonitor: FileProviderBasic {
|
||||
func isRegisteredForNotification(path: String) -> Bool
|
||||
}
|
||||
|
||||
/// Allows undo file operations done by provider
|
||||
public protocol FileProvideUndoable: FileProviderOperations {
|
||||
/// To initialize undo manager either call `setupUndoManager()` or set it manually.
|
||||
///
|
||||
/// - Note: Only some operations (moving/renaming, copying and creating) are supported for undoing.
|
||||
/// - Note: recording operations will occur after setting this object.
|
||||
var undoManager: UndoManager? { get set }
|
||||
|
||||
/// UndoManager supports undoing this file operation
|
||||
func canUndo(handle: OperationHandle) -> Bool
|
||||
/// UndoManager supports undoing this operation
|
||||
func canUndo(operation: FileOperationType) -> Bool
|
||||
}
|
||||
|
||||
public extension FileProvideUndoable {
|
||||
public func canUndo(operation: FileOperationType) -> Bool {
|
||||
return undoOperation(for: operation) != nil
|
||||
}
|
||||
|
||||
public func canUndo(handle: OperationHandle) -> Bool {
|
||||
return canUndo(operation: handle.operationType)
|
||||
}
|
||||
|
||||
internal func undoOperation(for operation: FileOperationType) -> FileOperationType? {
|
||||
switch operation {
|
||||
case .create(path: let path):
|
||||
return .remove(path: path)
|
||||
case .modify(path: _):
|
||||
return nil
|
||||
case .copy(source: _, destination: let dest):
|
||||
return .remove(path: dest)
|
||||
case .move(source: let source, destination: let dest):
|
||||
return .move(source: dest, destination: source)
|
||||
case .link(link: let link, target: _):
|
||||
return .remove(path: link)
|
||||
case .remove(path: _):
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Initiates `self.undoManager` if equals with `nil`, and set `levelsOfUndo` to 10.
|
||||
public func setupUndoManager() {
|
||||
guard self.undoManager == nil else { return }
|
||||
self.undoManager = UndoManager()
|
||||
self.undoManager?.levelsOfUndo = 10
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines protocol for provider allows all common operations.
|
||||
public protocol FileProvider: FileProviderBasic, FileProviderOperations, FileProviderReadWrite, NSCopying {
|
||||
}
|
||||
|
||||
internal let pathTrimSet = CharacterSet(charactersIn: " /")
|
||||
extension FileProviderBasic {
|
||||
public var type: String {
|
||||
return Self.type
|
||||
return type(of: self).type
|
||||
}
|
||||
|
||||
/// path without heading and trailing slash
|
||||
public var bareCurrentPath: String {
|
||||
return currentPath.trimmingCharacters(in: pathTrimSet)
|
||||
}
|
||||
|
||||
func escaped(path: String) -> String {
|
||||
return path.trimmingCharacters(in: pathTrimSet).addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
|
||||
}
|
||||
|
||||
/// **DEPRECATED:** Use `url(of:).absoluteURL` instead.
|
||||
@available(*, deprecated, message: "Use url(of:).absoluteURL instead.")
|
||||
public func absoluteURL(_ path: String? = nil) -> URL {
|
||||
return url(of: path).absoluteURL
|
||||
}
|
||||
/// **DEPRECATED** This property never worked as expected and is redundant as only supported by `LocalFileProvider`.
|
||||
/// To simulate `false` value, assign `URL(fileURLWithPath: "/")` to `baseURL`.
|
||||
@available(*, deprecated, message: "Redundant property, now is always true.")
|
||||
var isPathRelative: Bool { return true }
|
||||
|
||||
public func url(of path: String? = nil) -> URL {
|
||||
var rpath: String
|
||||
if let path = path {
|
||||
rpath = path
|
||||
} else {
|
||||
rpath = self.currentPath
|
||||
}
|
||||
var rpath: String = path ?? self.currentPath
|
||||
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath
|
||||
if let baseURL = baseURL {
|
||||
if isPathRelative && rpath.hasPrefix("/") {
|
||||
if rpath.hasPrefix("/") {
|
||||
rpath.remove(at: rpath.startIndex)
|
||||
}
|
||||
return URL(string: rpath, relativeTo: baseURL) ?? baseURL
|
||||
@@ -534,13 +628,24 @@ 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 {
|
||||
if url.baseURL == self.baseURL {
|
||||
// check if url derieved from current base url
|
||||
if !url.relativePath.isEmpty, url.baseURL == self.baseURL {
|
||||
return url.relativePath.removingPercentEncoding!
|
||||
}
|
||||
|
||||
// resolve url string against baseurl
|
||||
guard let baseURL = self.baseURL?.standardizedFileURL else { return url.absoluteString }
|
||||
return url.standardizedFileURL.absoluteString.replacingOccurrences(of: baseURL.absoluteString, with: "/").removingPercentEncoding!
|
||||
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)
|
||||
return standardPath.replacingOccurrences(of: standardBase, with: "/").removingPercentEncoding!
|
||||
}
|
||||
|
||||
internal func correctPath(_ path: String?) -> String? {
|
||||
@@ -585,7 +690,7 @@ extension FileProviderBasic {
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
_ = group.wait(timeout: DispatchTime.distantFuture)
|
||||
_ = group.wait(timeout: DispatchTime.now() + 5)
|
||||
let finalFile = result + (!fileExt.isEmpty ? "." + fileExt : "")
|
||||
return (dirPath as NSString).appendingPathComponent(finalFile)
|
||||
}
|
||||
@@ -607,6 +712,7 @@ extension FileProviderBasic {
|
||||
}
|
||||
}
|
||||
|
||||
/// Define methods to get preview and thumbnail for files or folders
|
||||
public protocol ExtendedFileProvider: FileProviderBasic {
|
||||
/// Returuns true if thumbnail preview is supported by provider and file type accordingly.
|
||||
///
|
||||
@@ -627,7 +733,7 @@ public protocol ExtendedFileProvider: FileProviderBasic {
|
||||
|
||||
- Parameters:
|
||||
- path: path of file.
|
||||
- completionHandler: a block with result of preview image or error.
|
||||
- completionHandler: a closure with result of preview image or error.
|
||||
`image`: `NSImage`/`UIImage` object contains preview.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
@@ -643,7 +749,7 @@ public protocol ExtendedFileProvider: FileProviderBasic {
|
||||
- Parameters:
|
||||
- path: path of file.
|
||||
- dimension: width and height of result preview image.
|
||||
- completionHandler: a block with result of preview image or error.
|
||||
- completionHandler: a closure with result of preview image or error.
|
||||
`image`: `NSImage`/`UIImage` object contains preview.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
@@ -658,7 +764,7 @@ public protocol ExtendedFileProvider: FileProviderBasic {
|
||||
|
||||
- Parameters:
|
||||
- path: path of file.
|
||||
- completionHandler: a block with result of preview image or error.
|
||||
- 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.
|
||||
@@ -774,14 +880,14 @@ extension ExtendedFileProvider {
|
||||
let newSize = CGSize(width: width, height: height)
|
||||
|
||||
#if os(macOS)
|
||||
var imageRect = NSRect(origin: CGPoint.zero, size: image.size)
|
||||
var imageRect = NSRect(origin: .zero, size: image.size)
|
||||
let imageRef = image.cgImage(forProposedRect: &imageRect, context: nil, hints: nil)
|
||||
|
||||
// Create NSImage from the CGImage using the new size
|
||||
return NSImage(cgImage: imageRef!, size: newSize)
|
||||
#else
|
||||
UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0)
|
||||
image.draw(in: CGRect(origin: CGPoint.zero, size: newSize))
|
||||
image.draw(in: CGRect(origin: .zero, size: newSize))
|
||||
let newImage = UIGraphicsGetImageFromCurrentImageContext() ?? image
|
||||
UIGraphicsEndImageContext()
|
||||
return newImage
|
||||
@@ -818,7 +924,7 @@ public enum FileOperationType: CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
/// present participle of action, like 'Copying`.
|
||||
/// present participle of action, like `Copying`.
|
||||
public var actionDescription: String {
|
||||
return description.trimmingCharacters(in: CharacterSet(charactersIn: "e")) + "ing"
|
||||
}
|
||||
@@ -842,6 +948,32 @@ public enum FileOperationType: CustomStringConvertible {
|
||||
return mirror.children.dropFirst().first?.value as? String
|
||||
}
|
||||
|
||||
init? (json: [String: AnyObject]) {
|
||||
guard let type = json["type"] as? String, let source = json["source"] as? String else {
|
||||
return nil
|
||||
}
|
||||
let dest = json["dest"] as? String
|
||||
switch type {
|
||||
case "Create":
|
||||
self = .create(path: source)
|
||||
case "Modify":
|
||||
self = .modify(path: source)
|
||||
case "Remove":
|
||||
self = .remove(path: source)
|
||||
case "Copy":
|
||||
guard let dest = dest else { return nil }
|
||||
self = .copy(source: source, destination: dest)
|
||||
case "Move":
|
||||
guard let dest = dest else { return nil }
|
||||
self = .move(source: source, destination: dest)
|
||||
case "Link":
|
||||
guard let dest = dest else { return nil }
|
||||
self = .link(link: source, target: dest)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
internal var json: String? {
|
||||
var dictionary: [String: AnyObject] = ["type": self.description as NSString]
|
||||
dictionary["source"] = source as NSString?
|
||||
@@ -850,7 +982,7 @@ public enum FileOperationType: CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Allows to get progress or cancel an in-progress operation, useful for remote providers
|
||||
public protocol OperationHandle {
|
||||
/// Operation supposed to be done on files. Contains file paths as associated value.
|
||||
var operationType: FileOperationType { get }
|
||||
@@ -879,6 +1011,9 @@ public extension OperationHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Delegate methods for reporting provider's operation result and progress, when it's ready to update
|
||||
/// user interface.
|
||||
/// All methods are called in main thread to avoids UI bugs.
|
||||
public protocol FileProviderDelegate: class {
|
||||
/// fileproviderSucceed(_:operation:) gives delegate a notification when an operation finished with success.
|
||||
/// This method is called in main thread to avoids UI bugs.
|
||||
@@ -892,6 +1027,7 @@ public protocol FileProviderDelegate: class {
|
||||
func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperationType, progress: Float)
|
||||
}
|
||||
|
||||
/// The `FileOperationDelegate` protocol defines methods for managing operations involving the copying, moving, linking, or removal of files and directories. When you use an `FileProvider` object to initiate a copy, move, link, or remove operation, the file provider asks its delegate whether the operation should begin at all and whether it should proceed when an error occurs.
|
||||
public protocol FileOperationDelegate: class {
|
||||
|
||||
/// fileProvider(_:shouldOperate:) gives the delegate an opportunity to filter the file operation. Returning true from this method will allow the copy to happen. Returning false from this method causes the item in question to be skipped. If the item skipped was a directory, no children of that directory will be subject of the operation, nor will the delegate be notified of those children.
|
||||
@@ -908,10 +1044,35 @@ internal class Weak<T: AnyObject> {
|
||||
}
|
||||
}
|
||||
|
||||
/// For internal use in `FileProvider` framework
|
||||
public protocol FoundationErrorEnum {
|
||||
init? (rawValue: Int)
|
||||
var rawValue: Int { get }
|
||||
}
|
||||
|
||||
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! }
|
||||
return val.first?.value
|
||||
}
|
||||
|
||||
func findAllValues(forKey key: String?) -> [(value: Any, operator: NSComparisonPredicate.Operator)] {
|
||||
if let cQuery = self as? NSCompoundPredicate {
|
||||
let find = cQuery.subpredicates.flatMap { ($0 as! NSPredicate).findAllValues(forKey: key) }
|
||||
return find
|
||||
} else if let cQuery = self as? NSComparisonPredicate {
|
||||
if cQuery.leftExpression.expressionType == .keyPath, key == nil || cQuery.leftExpression.keyPath == key!, let const = cQuery.rightExpression.constantValue {
|
||||
return [(value: const, operator: cQuery.predicateOperatorType)]
|
||||
}
|
||||
if cQuery.rightExpression.expressionType == .keyPath, key == nil || cQuery.rightExpression.keyPath == key!, let const = cQuery.leftExpression.constantValue {
|
||||
return [(value: const, operator: cQuery.predicateOperatorType)]
|
||||
}
|
||||
return []
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension URLError.Code: FoundationErrorEnum {}
|
||||
extension CocoaError.Code: FoundationErrorEnum {}
|
||||
|
||||
+231
-291
@@ -8,20 +8,29 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
/**
|
||||
This provider class allows interacting with local files placed in user disk. It also allows an
|
||||
easy way to use `NSFileCoordintaing` to coordinate read and write when neccessary.
|
||||
|
||||
it uses `FileManager` foundation class with some additions like searching and reading a portion of file.
|
||||
*/
|
||||
open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndoable {
|
||||
open class var type: String { return "Local" }
|
||||
open var isPathRelative: Bool
|
||||
open fileprivate(set) var baseURL: URL?
|
||||
open var currentPath: String
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open internal(set) var credential: URLCredential?
|
||||
|
||||
|
||||
/// Underlying `FileManager` object for listing and metadata fetching.
|
||||
open private(set) var fileManager = FileManager()
|
||||
/// Underlying `FileManager` object for operationa like copying, moving, etc.
|
||||
open private(set) var opFileManager = FileManager()
|
||||
fileprivate var fileProviderManagerDelegate: LocalFileProviderManagerDelegate? = nil
|
||||
|
||||
open var undoManager: UndoManager? = nil
|
||||
|
||||
/**
|
||||
Forces file operations to use `NSFileCoordinating`, should be set `true` if:
|
||||
- Files are on ubiquity (iCloud) container.
|
||||
@@ -33,15 +42,21 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
*/
|
||||
open var isCoorinating: Bool
|
||||
|
||||
/// **OBSOLETED**: Use FileProvider.init(for:in:) instead.
|
||||
@available(*, obsoleted: 1.0, renamed: "init(for:in:)", message: "Use FileProvider.init(for:in:) instead.")
|
||||
public convenience init (directory: FileManager.SearchPathDirectory = .documentDirectory, domainMask: FileManager.SearchPathDomainMask = .userDomainMask) {
|
||||
self.init(baseURL: FileManager.default.urls(for: directory, in: domainMask).first!)
|
||||
}
|
||||
|
||||
/**
|
||||
Initializes provider for the specified common directory in the requested domains.
|
||||
default values are `directory: .documentDirectory, domainMask: .userDomainMask`.
|
||||
|
||||
- Parameters:
|
||||
- directory: The search path directory. The supported values are described in `FileManager.SearchPathDirectory`.
|
||||
- domainMask: The file system domain to search. The value for this parameter is one or more of the constants described in `FileManager.SearchPathDomainMask`.
|
||||
*/
|
||||
public convenience init (directory: FileManager.SearchPathDirectory = .documentDirectory, domainMask: FileManager.SearchPathDomainMask = .userDomainMask) {
|
||||
- for: The search path directory. The supported values are described in `FileManager.SearchPathDirectory`.
|
||||
- in: Base locations for directory to search. The value for this parameter is one or more of the constants described in `FileManager.SearchPathDomainMask`.
|
||||
*/
|
||||
public convenience init (for directory: FileManager.SearchPathDirectory = .documentDirectory, in domainMask: FileManager.SearchPathDomainMask = .userDomainMask) {
|
||||
self.init(baseURL: FileManager.default.urls(for: directory, in: domainMask).first!)
|
||||
}
|
||||
|
||||
@@ -53,7 +68,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
default values are `directory: .documentDirectory`.
|
||||
|
||||
- Parameters:
|
||||
- sharedContainerId: Same with `App Group` identifier defined in project settings.
|
||||
- sharedContainerId: Same with `App Group` identifier defined in project settings.
|
||||
- directory: The search path directory. The supported values are described in `FileManager.SearchPathDirectory`.
|
||||
*/
|
||||
public convenience init? (sharedContainerId: String, directory: FileManager.SearchPathDirectory = .documentDirectory) {
|
||||
@@ -71,12 +86,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
case .cachesDirectory:
|
||||
finalBaseURL = baseURL.appendingPathComponent("Library/Caches")
|
||||
case .applicationSupportDirectory:
|
||||
finalBaseURL = baseURL.appendingPathComponent("Library/Application%20support")
|
||||
finalBaseURL = baseURL.appendingPathComponent("Library/Application support")
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
self.init(baseURL: finalBaseURL)
|
||||
self.isCoorinating = true
|
||||
|
||||
try? fileManager.createDirectory(at: finalBaseURL, withIntermediateDirectories: true)
|
||||
}
|
||||
@@ -89,7 +105,6 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
fatalError("Cannot initialize a Local provider from remote URL.")
|
||||
}
|
||||
self.baseURL = baseURL
|
||||
self.isPathRelative = true
|
||||
self.currentPath = ""
|
||||
self.credential = nil
|
||||
self.isCoorinating = false
|
||||
@@ -112,7 +127,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
do {
|
||||
let contents = try self.fileManager.contentsOfDirectory(at: self.url(of: path), includingPropertiesForKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .isHiddenKey, .volumeIsReadOnlyKey], options: .skipsSubdirectoryDescendants)
|
||||
let contents = try self.fileManager.contentsOfDirectory(at: self.url(of: path), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants)
|
||||
let filesAttributes = contents.flatMap({ (fileURL) -> LocalFileObject? in
|
||||
let path = self.relativePathOf(url: fileURL)
|
||||
return LocalFileObject(fileWithPath: path, relativeTo: self.baseURL)
|
||||
@@ -124,202 +139,190 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) {
|
||||
let dict = (try? FileManager.default.attributesOfFileSystem(forPath: baseURL?.path ?? "/"))
|
||||
let totalSize = (dict?[.systemSize] as? NSNumber)?.int64Value ?? -1;
|
||||
let freeSize = (dict?[.systemFreeSize] as? NSNumber)?.int64Value ?? 0;
|
||||
completionHandler(totalSize, totalSize - freeSize)
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
completionHandler(LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), nil)
|
||||
}
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) {
|
||||
let values = try? baseURL?.resourceValues(forKeys: [.volumeTotalCapacityKey, .volumeAvailableCapacityKey])
|
||||
let totalSize = Int64(values??.volumeTotalCapacity ?? -1)
|
||||
let freeSize = Int64(values??.volumeAvailableCapacity ?? 0)
|
||||
completionHandler(totalSize, totalSize - freeSize)
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
let iterator = self.fileManager.enumerator(at: self.url(of: path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in
|
||||
completionHandler([], e)
|
||||
return true
|
||||
}
|
||||
var result = [LocalFileObject]()
|
||||
while let fileURL = iterator?.nextObject() as? URL {
|
||||
let path = self.relativePathOf(url: fileURL)
|
||||
if let fileObject = LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), query.evaluate(with: fileObject.mapPredicate()) {
|
||||
result.append(fileObject)
|
||||
foundItemHandler?(fileObject)
|
||||
}
|
||||
}
|
||||
completionHandler(result, nil)
|
||||
}
|
||||
}
|
||||
|
||||
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
dispatch_queue.async {
|
||||
completionHandler(self.fileManager.isReadableFile(atPath: self.baseURL!.path))
|
||||
}
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate : FileOperationDelegate?
|
||||
|
||||
@discardableResult
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
|
||||
let url = self.url(of: atPath).appendingPathComponent(folderName)
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
do {
|
||||
try self.opFileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: [:])
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.writingIntent(with: url, options: .forReplacing)
|
||||
self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(fileName))
|
||||
let url = self.url(of: atPath).appendingPathComponent(fileName, isDirectory: false)
|
||||
let fileName = fileName.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
let path = (atPath as NSString).appendingPathComponent(fileName)
|
||||
let opType = FileOperationType.create(path: path)
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
let success = self.opFileManager.createFile(atPath: url.path, contents: data, attributes: nil)
|
||||
if success {
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} else {
|
||||
completionHandler?(self.throwError(atPath, code: URLError.cannotCreateFile as FoundationErrorEnum))
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.writingIntent(with:url, options: .forReplacing)
|
||||
self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
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)
|
||||
let sourceUrl = self.url(of: path)
|
||||
let destUrl = self.url(of: toPath)
|
||||
|
||||
let sourceIntent = NSFileAccessIntent.writingIntent(with: sourceUrl, options: .forDeleting)
|
||||
let destIntent = NSFileAccessIntent.writingIntent(with: destUrl, options: .forReplacing)
|
||||
|
||||
let operationHandler: (URL, URL) -> Void = { sourceUrl, destUrl in
|
||||
if !overwrite && self.fileManager.fileExists(atPath: destUrl.path) {
|
||||
completionHandler?(self.throwError(toPath, code: URLError.cannotMoveFile as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
do {
|
||||
try self.opFileManager.moveItem(at: sourceUrl, to: destUrl)
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
|
||||
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
|
||||
completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum))
|
||||
return nil
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
coordinated(intents: [sourceIntent, destIntent], completionHandler: operationHandler) { (error) in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(sourceUrl, destUrl)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toPath)
|
||||
let sourceUrl = self.url(of: path)
|
||||
let destUrl = self.url(of: toPath)
|
||||
|
||||
let sourceIntent = NSFileAccessIntent.readingIntent(with: sourceUrl, options: .withoutChanges)
|
||||
let destIntent = NSFileAccessIntent.writingIntent(with: destUrl, options: .forDeleting)
|
||||
|
||||
let operationHandler: (URL, URL) -> Void = { sourceUrl, destUrl in
|
||||
if !overwrite && self.fileManager.fileExists(atPath: destUrl.path) {
|
||||
completionHandler?(self.throwError(toPath, code: URLError.cannotMoveFile as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
do {
|
||||
try self.opFileManager.copyItem(at: sourceUrl, to: destUrl)
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
coordinated(intents: [sourceIntent, destIntent], moving: true, completionHandler: operationHandler) { (error) in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(sourceUrl, destUrl)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.remove(path: path)
|
||||
let url = self.url(of: path)
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
return self.doOperation(opType, forUploading: true, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
dynamic func doSimpleOperation(_ box: UndoBox) {
|
||||
guard let _ = self.undoManager else { return }
|
||||
_ = self.doOperation(box.undoOperation) { (_) in
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
func urlofpath(path: String) -> URL {
|
||||
if path.hasPrefix("file://") {
|
||||
let removedSchemePath = path.replacingOccurrences(of: "file://", with: "", options: .anchored)
|
||||
let pDecodedPath = removedSchemePath.removingPercentEncoding ?? removedSchemePath
|
||||
return URL(fileURLWithPath: pDecodedPath)
|
||||
} else {
|
||||
return self.url(of: path)
|
||||
}
|
||||
}
|
||||
|
||||
guard let sourcePath = opType.source else { return nil }
|
||||
let destPath = opType.destination
|
||||
let source: URL = urlofpath(path: sourcePath)
|
||||
|
||||
let dest: URL?
|
||||
if let destPath = destPath {
|
||||
dest = urlofpath(path: destPath)
|
||||
} else {
|
||||
dest = nil
|
||||
}
|
||||
|
||||
if let undoManager = self.undoManager, let undoOp = self.undoOperation(for: opType) {
|
||||
let undoBox = UndoBox(provider: self, operation: opType, undoOperation: undoOp)
|
||||
undoManager.beginUndoGrouping()
|
||||
undoManager.registerUndo(withTarget: self, selector: #selector(LocalFileProvider.doSimpleOperation(_:)), object: undoBox)
|
||||
undoManager.setActionName(opType.actionDescription)
|
||||
undoManager.endUndoGrouping()
|
||||
}
|
||||
|
||||
var successfulSecurityScopedResourceAccess = false
|
||||
|
||||
let operationHandler: (URL, URL?) -> Void = { source, dest in
|
||||
do {
|
||||
let successfulSecurityScopedResourceAccess = url.startAccessingSecurityScopedResource()
|
||||
try self.opFileManager.removeItem(at: url)
|
||||
if successfulSecurityScopedResourceAccess {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
switch opType {
|
||||
case .create:
|
||||
if sourcePath.hasSuffix("/") {
|
||||
try self.opFileManager.createDirectory(at: source, withIntermediateDirectories: true, attributes: [:])
|
||||
} else {
|
||||
try data?.write(to: source, options: Data.WritingOptions.atomic)
|
||||
}
|
||||
case .modify:
|
||||
try data?.write(to: source, options: atomically ? [.atomic] : [])
|
||||
case .copy:
|
||||
guard let dest = dest else { return }
|
||||
try self.opFileManager.copyItem(at: source, to: dest)
|
||||
case .move:
|
||||
guard let dest = dest else { return }
|
||||
try self.opFileManager.moveItem(at: source, to: dest)
|
||||
case.remove:
|
||||
try self.opFileManager.removeItem(at: source)
|
||||
default:
|
||||
return
|
||||
}
|
||||
if successfulSecurityScopedResourceAccess {
|
||||
source.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(nil)
|
||||
}
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
if successfulSecurityScopedResourceAccess {
|
||||
source.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(e)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
@@ -327,63 +330,41 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.writingIntent(with:url, options: .forReplacing)
|
||||
self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler?(error)
|
||||
successfulSecurityScopedResourceAccess = source.startAccessingSecurityScopedResource()
|
||||
var intents = [NSFileAccessIntent]()
|
||||
switch opType {
|
||||
case .create, .modify:
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forReplacing))
|
||||
case .copy:
|
||||
guard let dest = dest else { return nil }
|
||||
intents.append(NSFileAccessIntent.readingIntent(with: source, options: forUploading ? .forUploading : .withoutChanges))
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: dest, options: .forReplacing))
|
||||
case .move:
|
||||
guard let dest = dest else { return nil }
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forMoving))
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: dest, options: .forReplacing))
|
||||
case .remove:
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forDeleting))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
self.coordinated(intents: intents, completionHandler: operationHandler, errorHandler: { error in
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(url)
|
||||
operationHandler(source, dest)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
// TODO: Make use of overwrite parameter
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
operation_queue.addOperation {
|
||||
do {
|
||||
try self.opFileManager.copyItem(at: localFile, to: self.url(of: toPath))
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
}
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
|
||||
operation_queue.addOperation {
|
||||
do {
|
||||
try self.opFileManager.copyItem(at: self.url(of: path), to: toLocalURL)
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
}
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
@@ -392,16 +373,22 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
completionHandler(data, nil)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(data, nil)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler(nil, e)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges)
|
||||
coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler(nil, error)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
@@ -417,7 +404,10 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
|
||||
@discardableResult
|
||||
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
if length == 0 {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -429,12 +419,10 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
let url = self.url(of: path)
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
guard self.fileManager.fileExists(atPath: url.path) && !url.fileIsDirectory else {
|
||||
completionHandler(nil, self.throwError(path, code: URLError.fileDoesNotExist as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
guard let handle = FileHandle(forReadingAtPath: url.path) else {
|
||||
completionHandler(nil, self.throwError(path, code: URLError.cannotOpenFile as FoundationErrorEnum))
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileNoSuchFile as FoundationErrorEnum))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -442,19 +430,26 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
handle.closeFile()
|
||||
}
|
||||
|
||||
let size = LocalFileObject(fileWithURL: url)?.size ?? -1
|
||||
guard size > offset else {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
|
||||
}
|
||||
return
|
||||
}
|
||||
handle.seek(toFileOffset: UInt64(offset))
|
||||
guard Int64(handle.offsetInFile) == offset else {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadUnknown as FoundationErrorEnum))
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let data = handle.readData(ofLength: length)
|
||||
guard length > 0 && data.count == length else {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
|
||||
completionHandler(data, nil)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(data, nil)
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
@@ -477,65 +472,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
@discardableResult
|
||||
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.modify(path: path)
|
||||
let url = self.url(of: path)
|
||||
var options: Data.WritingOptions = []
|
||||
if atomically {
|
||||
options.insert(.atomic)
|
||||
}
|
||||
if overwrite {
|
||||
options.insert(.withoutOverwriting)
|
||||
}
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
do {
|
||||
try data.write(to: url, options: atomically ? [.atomic] : [])
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async{
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.writingIntent(with: url, options: .forReplacing)
|
||||
coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
let iterator = self.fileManager.enumerator(at: self.url(of: path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in
|
||||
completionHandler([], e)
|
||||
return true
|
||||
}
|
||||
var result = [LocalFileObject]()
|
||||
while let fileURL = iterator?.nextObject() as? URL {
|
||||
if fileURL.lastPathComponent.lowercased().contains(query.lowercased()) {
|
||||
let path = self.relativePathOf(url: fileURL)
|
||||
if let fileObject = LocalFileObject(fileWithPath: path, relativeTo: self.baseURL) {
|
||||
result.append(fileObject)
|
||||
foundItemHandler?(fileObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
completionHandler(result, nil)
|
||||
}
|
||||
return self.doOperation(opType, data: data, atomically: atomically, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
fileprivate var monitors = [LocalFolderMonitor]()
|
||||
@@ -566,15 +503,16 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
}
|
||||
|
||||
open func isRegisteredForNotification(path: String) -> Bool {
|
||||
return monitors.map( { self.relativePathOf(url: $0.url) } ).contains(path)
|
||||
return monitors.map( { self.relativePathOf(url: $0.url) } ).contains(path.trimmingCharacters(in: CharacterSet(charactersIn: "/")))
|
||||
}
|
||||
|
||||
open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = LocalFileProvider(baseURL: self.baseURL!)
|
||||
copy.currentPath = self.currentPath
|
||||
copy.undoManager = self.undoManager
|
||||
copy.isCoorinating = self.isCoorinating
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy.isPathRelative = self.isPathRelative
|
||||
return copy
|
||||
}
|
||||
}
|
||||
@@ -634,23 +572,25 @@ internal extension LocalFileProvider {
|
||||
errorHandler?(error)
|
||||
return
|
||||
}
|
||||
completionHandler(intents[0].url)
|
||||
completionHandler(intents.first!.url)
|
||||
}
|
||||
}
|
||||
|
||||
func coordinated(intents: [NSFileAccessIntent], moving: Bool = false, completionHandler: @escaping (_ sourceUrl: URL, _ destURL: URL) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
|
||||
func coordinated(intents: [NSFileAccessIntent], moving: Bool = false, completionHandler: @escaping (_ sourceUrl: URL, _ destURL: URL?) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
|
||||
let coordinator = NSFileCoordinator(filePresenter: nil)
|
||||
coordinator.coordinate(with: intents, queue: operation_queue) { (error) in
|
||||
if let error = error {
|
||||
errorHandler?(error)
|
||||
return
|
||||
}
|
||||
if moving {
|
||||
coordinator.item(at: intents[0].url, willMoveTo: intents[1].url)
|
||||
guard let newSource: URL = intents.first?.url else { return }
|
||||
let newDest: URL? = intents.dropFirst().first?.url
|
||||
if moving, let newDest = newDest {
|
||||
coordinator.item(at: newSource, willMoveTo: newDest)
|
||||
}
|
||||
completionHandler(intents[0].url, intents[1].url)
|
||||
if moving {
|
||||
coordinator.item(at: intents[0].url, didMoveTo: intents[1].url)
|
||||
completionHandler(newSource, newDest)
|
||||
if moving, let newDest = newDest {
|
||||
coordinator.item(at: newSource, didMoveTo: newDest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+51
-21
@@ -8,15 +8,17 @@
|
||||
|
||||
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) {
|
||||
super.init(url: url, name: name, path: path)
|
||||
}
|
||||
|
||||
/// Initiates a `LocalFileObject` with attributes of file in path.
|
||||
public convenience init? (fileWithPath path: String, relativeTo relativeURL: URL?) {
|
||||
var fileURL: URL?
|
||||
var rpath = path.replacingOccurrences(of: relativeURL?.absoluteString ?? "", with: "", options: .anchored)
|
||||
if path.hasPrefix("/") {
|
||||
var rpath = path.replacingOccurrences(of: relativeURL?.path ?? "", with: "", options: .anchored)
|
||||
if relativeURL != nil && rpath.hasPrefix("/") {
|
||||
rpath.remove(at: rpath.startIndex)
|
||||
}
|
||||
if rpath.isEmpty {
|
||||
@@ -35,39 +37,54 @@ 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])
|
||||
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .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)
|
||||
for (key, value) in values.allValues {
|
||||
self.allValues[key.rawValue] = value
|
||||
self.allValues[key] = value
|
||||
}
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The total size allocated on disk for the file
|
||||
open internal(set) var allocatedSize: Int64 {
|
||||
get {
|
||||
return allValues[URLResourceKey.fileAllocatedSizeKey.rawValue] as? Int64 ?? 0
|
||||
return allValues[.fileAllocatedSizeKey] as? Int64 ?? 0
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.fileAllocatedSizeKey.rawValue] = Int(exactly: newValue) ?? Int.max
|
||||
allValues[.fileAllocatedSizeKey] = Int(exactly: newValue) ?? Int.max
|
||||
}
|
||||
}
|
||||
|
||||
/// The document identifier is a value assigned by the kernel/system to a file or directory.
|
||||
/// This value is used to identify the document regardless of where it is moved on a volume.
|
||||
/// The identifier persists across system restarts.
|
||||
open internal(set) var id: Int? {
|
||||
get {
|
||||
return allValues[.documentIdentifierKey] as? Int
|
||||
}
|
||||
set {
|
||||
allValues[.documentIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The revision of file, which changes when a file contents are modified.
|
||||
/// Changes to attributes or other file metadata do not change the identifier.
|
||||
open var rev: String? {
|
||||
get {
|
||||
let data = allValues[URLResourceKey.generationIdentifierKey.rawValue] as? Data
|
||||
let data = allValues[.generationIdentifierKey] as? Data
|
||||
return data?.map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class LocalFolderMonitor {
|
||||
internal final class LocalFolderMonitor {
|
||||
fileprivate let source: DispatchSourceFileSystemObject
|
||||
fileprivate let descriptor: CInt
|
||||
fileprivate let qq: DispatchQueue = DispatchQueue.global(qos: .default)
|
||||
@@ -84,13 +101,16 @@ internal class LocalFolderMonitor {
|
||||
// We have a 0.2 second delay to ensure we wont call handler 1000s times when there is
|
||||
// a huge file operation. This ensures app will work smoothly while this 250 milisec won't
|
||||
// affect user experince much
|
||||
let main_handler: ()->Void = {
|
||||
let main_handler: ()->Void = { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
if Date().timeIntervalSinceReferenceDate < self.monitoredTime + 0.2 {
|
||||
return
|
||||
}
|
||||
self.monitoredTime = Date().timeIntervalSinceReferenceDate
|
||||
self.source.suspend()
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.25, execute: {
|
||||
handler()
|
||||
self.source.resume()
|
||||
})
|
||||
}
|
||||
source.setEventHandler(handler: main_handler)
|
||||
@@ -199,6 +219,7 @@ internal class LocalFileProviderManagerDelegate: NSObject, FileManagerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// Local operation handling is limited. Please don't use as much as possible.
|
||||
open class LocalOperationHandle: OperationHandle {
|
||||
public let baseURL: URL
|
||||
public let operationType: FileOperationType
|
||||
@@ -278,18 +299,15 @@ open class LocalOperationHandle: OperationHandle {
|
||||
let fp = FileManager()
|
||||
let filesList = fp.enumerator(at: pathURL, includingPropertiesForKeys: keys, options: enumOpt, errorHandler: nil)
|
||||
while let fileURL = filesList?.nextObject() as? URL {
|
||||
do {
|
||||
let values = try fileURL.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey])
|
||||
let isdir = values.isDirectory ?? false
|
||||
let size = Int64(values.fileSize ?? 0)
|
||||
if isdir {
|
||||
folders += 1
|
||||
} else {
|
||||
files += 1
|
||||
}
|
||||
totalsize += size
|
||||
} catch _ {
|
||||
guard let values = try? fileURL.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey]) else { continue }
|
||||
let isdir = values.isDirectory ?? false
|
||||
let size = Int64(values.fileSize ?? 0)
|
||||
if isdir {
|
||||
folders += 1
|
||||
} else {
|
||||
files += 1
|
||||
}
|
||||
totalsize += size
|
||||
}
|
||||
|
||||
return (folders, files, totalsize)
|
||||
@@ -297,6 +315,18 @@ open class LocalOperationHandle: OperationHandle {
|
||||
}
|
||||
}
|
||||
|
||||
class UndoBox: NSObject {
|
||||
weak var provider: FileProvideUndoable?
|
||||
let operation: FileOperationType
|
||||
let undoOperation: FileOperationType
|
||||
|
||||
init(provider: FileProvideUndoable, operation: FileOperationType, undoOperation: FileOperationType) {
|
||||
self.provider = provider
|
||||
self.operation = operation
|
||||
self.undoOperation = undoOperation
|
||||
}
|
||||
}
|
||||
|
||||
internal extension URL {
|
||||
var fileIsDirectory: Bool {
|
||||
return (try? self.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
|
||||
|
||||
@@ -10,18 +10,19 @@
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
|
||||
/**
|
||||
Allows accessing to OneDrive stored files, either hosted on Microsoft servers or business coprporate one.
|
||||
This provider doesn't cache or save files internally, however you can set `useCache` and `cache` properties
|
||||
to use Foundation `NSURLCache` system.
|
||||
|
||||
- Note: Uploading files and data are limited to 100MB, for now.
|
||||
*/
|
||||
open class OneDriveFileProvider: FileProviderBasicRemote {
|
||||
open class var type: String { return "OneDrive" }
|
||||
open let isPathRelative: Bool
|
||||
open let baseURL: URL?
|
||||
/// OneDrive server url, equals with unwrapped `baseURL`
|
||||
open var serverURL: URL { return baseURL! }
|
||||
/// Drive name for user, default is `root`. Changing its value will effect on new operations.
|
||||
open var drive: String
|
||||
/// Generated storage url from server url and drive name
|
||||
open var driveURL: URL {
|
||||
return URL(string: "/drive/\(drive):/", relativeTo: baseURL)!
|
||||
}
|
||||
open var currentPath: String
|
||||
|
||||
open var dispatch_queue: DispatchQueue
|
||||
@@ -64,12 +65,12 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
|
||||
- serverURL: server url, Set it if you are trying to connect OneDrive Business server, otherwise leave it
|
||||
`nil` to connect to OneDrive Personal uses.
|
||||
- drive: drive name for user on server, default value is `root`.
|
||||
- cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used.
|
||||
- cache: A URLCache to cache downloaded files and contents.
|
||||
*/
|
||||
public init(credential: URLCredential?, serverURL: URL? = nil, drive: String = "root", cache: URLCache? = nil) {
|
||||
self.baseURL = (serverURL ?? URL(string: "https://api.onedrive.com/")!).appendingPathComponent("")
|
||||
let baseURL = serverURL ?? URL(string: "https://api.onedrive.com/")!
|
||||
self.baseURL = baseURL.path.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
|
||||
self.drive = drive
|
||||
self.isPathRelative = true
|
||||
self.currentPath = ""
|
||||
self.useCache = false
|
||||
self.validatingCache = true
|
||||
@@ -95,8 +96,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
let url = URL(string: escaped(path: path), relativeTo: driveURL)!
|
||||
var request = URLRequest(url: url)
|
||||
var request = URLRequest(url: url(of: path))
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
@@ -115,8 +115,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
|
||||
let url = URL(string: "/drive/root", relativeTo: baseURL)!
|
||||
var request = URLRequest(url: url)
|
||||
var request = URLRequest(url: url())
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
@@ -131,31 +130,83 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
var foundFiles = [OneDriveFileObject]()
|
||||
var queryStr: String?
|
||||
queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first
|
||||
guard let finalQueryStr = queryStr else { return }
|
||||
search(path, query: finalQueryStr, foundItem: { (file) in
|
||||
if query.evaluate(with: file.mapPredicate()) {
|
||||
foundFiles.append(file)
|
||||
foundItemHandler?(file)
|
||||
}
|
||||
}, completionHandler: { (error) in
|
||||
completionHandler(foundFiles, error)
|
||||
})
|
||||
}
|
||||
|
||||
open func url(of path: String? = nil, modifier: String? = nil) -> URL {
|
||||
var rpath: String
|
||||
if let path = path {
|
||||
rpath = path
|
||||
} else {
|
||||
rpath = self.currentPath
|
||||
}
|
||||
|
||||
if rpath.hasPrefix("/") {
|
||||
rpath.remove(at: rpath.startIndex)
|
||||
}
|
||||
if rpath.isEmpty {
|
||||
if let modifier = modifier {
|
||||
return baseURL!.appendingPathComponent("drive/\(drive)/\(modifier)")
|
||||
}
|
||||
return baseURL!.appendingPathComponent("drive/\(drive)")
|
||||
}
|
||||
let driveURL = baseURL!.appendingPathComponent("drive/\(drive):/")
|
||||
rpath = (rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath)
|
||||
rpath = rpath.trimmingCharacters(in: pathTrimSet)
|
||||
if let modifier = modifier {
|
||||
rpath = rpath + ":/" + modifier
|
||||
}
|
||||
return URL(string: rpath, relativeTo: driveURL) ?? driveURL
|
||||
}
|
||||
|
||||
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
var request = URLRequest(url: url())
|
||||
request.httpMethod = "HEAD"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
let status = (response as? HTTPURLResponse)?.statusCode ?? 400
|
||||
completionHandler(status == 200)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate: FileOperationDelegate?
|
||||
}
|
||||
|
||||
extension OneDriveFileProvider: FileProviderOperations {
|
||||
|
||||
|
||||
public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
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)
|
||||
}
|
||||
|
||||
public func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
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)
|
||||
}
|
||||
|
||||
public func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.remove(path: path), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@@ -165,7 +216,7 @@ extension OneDriveFileProvider: FileProviderOperations {
|
||||
}
|
||||
guard let sourcePath = operation.source else { return nil }
|
||||
let destPath = operation.destination
|
||||
var request = URLRequest(url: URL(string: sourcePath, relativeTo: driveURL)!)
|
||||
var request = URLRequest(url: url(of: sourcePath))
|
||||
switch operation {
|
||||
case .create:
|
||||
request.httpMethod = "CREATE"
|
||||
@@ -200,7 +251,7 @@ extension OneDriveFileProvider: FileProviderOperations {
|
||||
return RemoteOperationHandle(operationType: operation, tasks: [task])
|
||||
}
|
||||
|
||||
public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
@@ -208,13 +259,12 @@ extension OneDriveFileProvider: FileProviderOperations {
|
||||
return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let url = URL(string: escaped(path: path) + ":/content", relativeTo: driveURL)!
|
||||
var request = URLRequest(url: url)
|
||||
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
|
||||
@@ -239,14 +289,16 @@ extension OneDriveFileProvider: FileProviderOperations {
|
||||
}
|
||||
|
||||
extension OneDriveFileProvider: FileProviderReadWrite {
|
||||
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
if length == 0 {
|
||||
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
let url = URL(string: escaped(path: path) + ":/content", relativeTo: driveURL)!
|
||||
var request = URLRequest(url: url)
|
||||
var request = URLRequest(url: self.url(of: path, modifier: "content"))
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
if length > 0 {
|
||||
@@ -267,7 +319,7 @@ extension OneDriveFileProvider: FileProviderReadWrite {
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
|
||||
public 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
|
||||
@@ -276,16 +328,6 @@ extension OneDriveFileProvider: FileProviderReadWrite {
|
||||
return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
var foundFiles = [OneDriveFileObject]()
|
||||
search(path, query: query, foundItem: { (file) in
|
||||
foundFiles.append(file)
|
||||
foundItemHandler?(file)
|
||||
}, completionHandler: { (error) in
|
||||
completionHandler(foundFiles, error)
|
||||
})
|
||||
}
|
||||
|
||||
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
|
||||
/* There is two ways to monitor folders changing in OneDrive. Either using webooks
|
||||
* which means you have to implement a server to translate it to push notifications
|
||||
@@ -304,15 +346,14 @@ extension OneDriveFileProvider: FileProviderReadWrite {
|
||||
|
||||
- Parameters:
|
||||
- to: path of file, including file/directory name.
|
||||
- completionHandler: a block with result of directory entries or error.
|
||||
- completionHandler: a closure with result of directory entries or error.
|
||||
`link`: a url returned by OneDrive to share.
|
||||
`attribute`: `nil` for OneDrive.
|
||||
`expiration`: `nil` for OneDrive, as it doesn't expires.
|
||||
`error`: Error returned by OneDrive.
|
||||
*/
|
||||
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: OneDriveFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
let url = URL(string: escaped(path: path) + ":/action.createLink", relativeTo: driveURL)!
|
||||
var request = URLRequest(url: url)
|
||||
var request = URLRequest(url: self.url(of: path, modifier: "action.createLink"))
|
||||
request.httpMethod = "POST"
|
||||
let requestDictionary: [String: AnyObject] = ["type": "view" as NSString]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
@@ -337,11 +378,11 @@ extension OneDriveFileProvider: FileProviderReadWrite {
|
||||
|
||||
|
||||
extension OneDriveFileProvider: ExtendedFileProvider {
|
||||
public func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
open func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public func propertiesOfFileSupported(path: String) -> Bool {
|
||||
open func propertiesOfFileSupported(path: String) -> Bool {
|
||||
let fileExt = (path as NSString).pathExtension.lowercased()
|
||||
switch fileExt {
|
||||
case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff":
|
||||
@@ -355,12 +396,12 @@ extension OneDriveFileProvider: ExtendedFileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
|
||||
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
|
||||
let url: URL
|
||||
if let dimension = dimension {
|
||||
url = URL(string: escaped(path: path) + ":/thumbnails/0/=c\(dimension.width)x\(dimension.height)/content", relativeTo: driveURL)!
|
||||
url = self.url(of: path, modifier: "thumbnails/0/=c\(dimension.width)x\(dimension.height)/content")
|
||||
} else {
|
||||
url = URL(string: escaped(path: path) + ":/thumbnails/0/small/content", relativeTo: driveURL)!
|
||||
url = self.url(of: path, modifier: "thumbnails/0/small/content")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
@@ -380,9 +421,8 @@ extension OneDriveFileProvider: ExtendedFileProvider {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
|
||||
let url = URL(string: escaped(path: path), relativeTo: driveURL)!
|
||||
var request = URLRequest(url: url)
|
||||
open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
|
||||
var request = URLRequest(url: url(of: path))
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
|
||||
@@ -8,16 +8,14 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct FileProviderOneDriveError: Error, CustomStringConvertible {
|
||||
/// Error returned by OneDrive server when trying to access or do operations on a file or folder.
|
||||
public struct FileProviderOneDriveError: FileProviderHTTPError {
|
||||
public let code: FileProviderHTTPErrorCode
|
||||
public let path: String
|
||||
public let errorDescription: String?
|
||||
|
||||
public var description: String {
|
||||
return code.description
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -36,7 +34,9 @@ public final class OneDriveFileObject: FileObject {
|
||||
internal convenience init? (baseURL: URL?, drive: String, json: [String: AnyObject]) {
|
||||
guard let name = json["name"] as? String else { return nil }
|
||||
guard let path = (json["parentReference"] as? NSDictionary)?["path"] as? String else { return nil }
|
||||
let lPath = path.replacingOccurrences(of: "/drive/\(drive):", with: "/", options: .anchored, range: nil)
|
||||
var lPath = path.replacingOccurrences(of: "/drive/\(drive)", with: "/", options: .anchored, range: nil)
|
||||
lPath = lPath.replacingOccurrences(of: "/:", with: "", options: .anchored)
|
||||
lPath = lPath.replacingOccurrences(of: "//", with: "", options: .anchored)
|
||||
self.init(baseURL: baseURL, name: name, path: lPath)
|
||||
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
|
||||
self.modifiedDate = resolve(dateString: json["lastModifiedDateTime"] as? String ?? "")
|
||||
@@ -46,43 +46,43 @@ public final class OneDriveFileObject: FileObject {
|
||||
self.entryTag = json["eTag"] as? String
|
||||
}
|
||||
|
||||
/// The document identifier is a value assigned by the OneDrive to a file.
|
||||
/// This value is used to identify the document regardless of where it is moved on a volume.
|
||||
/// The identifier persists across system restarts.
|
||||
open internal(set) var id: String? {
|
||||
get {
|
||||
return allValues["NSURLDocumentIdentifyKey"] as? String
|
||||
return allValues[.documentIdentifierKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues["NSURLDocumentIdentifyKey"] = newValue
|
||||
allValues[.documentIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// MIME type of file contents returned by OneDrive server.
|
||||
open internal(set) var contentType: String {
|
||||
get {
|
||||
return allValues["NSURLContentTypeKey"] as? String ?? ""
|
||||
return allValues[.mimeType] as? String ?? ""
|
||||
}
|
||||
set {
|
||||
allValues["NSURLContentTypeKey"] = newValue
|
||||
allValues[.mimeType] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP E-Tag, can be used to mark changed files.
|
||||
open internal(set) var entryTag: String? {
|
||||
get {
|
||||
return allValues["NSURLEntryTagKey"] as? String
|
||||
return allValues[.entryTag] as? String
|
||||
}
|
||||
set {
|
||||
allValues["NSURLEntryTagKey"] = newValue
|
||||
allValues[.entryTag] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
internal extension OneDriveFileProvider {
|
||||
func list(_ path: String, cursor: String? = nil, prevContents: [OneDriveFileObject] = [], completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
|
||||
let url: URL
|
||||
if let cursor = cursor {
|
||||
url = URL(string: cursor)!
|
||||
} else {
|
||||
url = URL(string: escaped(path: path), relativeTo: driveURL)!
|
||||
}
|
||||
func list(_ path: String, cursor: URL? = nil, prevContents: [OneDriveFileObject] = [], completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
|
||||
let url = cursor ?? self.url(of: path, modifier: "children")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
@@ -100,7 +100,7 @@ internal extension OneDriveFileProvider {
|
||||
files.append(file)
|
||||
}
|
||||
}
|
||||
let ncursor = json?["@odata.nextLink"] as? String
|
||||
let ncursor: URL? = (json?["@odata.nextLink"] as? String).flatMap { URL(string: $0) }
|
||||
let hasmore = ncursor != nil
|
||||
if hasmore {
|
||||
self.list(path, cursor: ncursor, prevContents: files, completionHandler: completionHandler)
|
||||
@@ -122,7 +122,7 @@ internal extension OneDriveFileProvider {
|
||||
return nil
|
||||
}
|
||||
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
|
||||
let url = URL(string: escaped(path: targetPath) + ":/content" + queryStr, relativeTo: driveURL)!
|
||||
let url = self.url(of: targetPath, modifier: "content\(queryStr)")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
@@ -134,7 +134,7 @@ internal extension OneDriveFileProvider {
|
||||
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
|
||||
self.delegateNotify(operation, error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
task.resume()
|
||||
@@ -150,7 +150,7 @@ internal extension OneDriveFileProvider {
|
||||
return nil
|
||||
}
|
||||
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
|
||||
let url = URL(string: escaped(path: targetPath) + ":/content" + queryStr, relativeTo: driveURL)!
|
||||
let url = self.url(of: targetPath, modifier: "content\(queryStr)")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
@@ -161,22 +161,17 @@ internal extension OneDriveFileProvider {
|
||||
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
|
||||
self.delegateNotify(operation, error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: operation, tasks: [task])
|
||||
}
|
||||
|
||||
func search(_ startPath: String = "", query: String, next: String? = nil, foundItem:@escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
|
||||
func search(_ startPath: String = "", query: String, next: URL? = nil, foundItem:@escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
|
||||
let url: URL
|
||||
if let next = next {
|
||||
url = URL(string: next)!
|
||||
} else if self.escaped(path: startPath) == "" {
|
||||
url = URL(string: "/drive/\(drive)/view.search?q=\(query)", relativeTo: baseURL)!
|
||||
} else {
|
||||
url = URL(string: "\(escaped(path: startPath))/view.search?q=\(query)", relativeTo: driveURL)!
|
||||
}
|
||||
let q = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
|
||||
url = next ?? self.url(of: startPath, modifier: "view.search?q=\(q)")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
@@ -194,9 +189,8 @@ internal extension OneDriveFileProvider {
|
||||
foundItem(file)
|
||||
}
|
||||
}
|
||||
let next = json?["@odata.nextLink"] as? String
|
||||
let hasmore = next != nil
|
||||
if hasmore, let next = next {
|
||||
let next: URL? = (json?["@odata.nextLink"] as? String).flatMap { URL(string: $0) }
|
||||
if let next = next {
|
||||
self.search(startPath, query: query, next: next, foundItem: foundItem, completionHandler: completionHandler)
|
||||
} else {
|
||||
completionHandler(responseError ?? error)
|
||||
@@ -235,72 +229,51 @@ internal extension OneDriveFileProvider {
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if let parent = json["image"] as? [String: Any] ?? json["video"] as? [String: Any], let height = parent["height"] as? UInt64, let width = parent["width"] as? UInt64 {
|
||||
keys.append("Dimensions")
|
||||
dic["Dimensions"] = "\(width)x\(height)"
|
||||
add(key: "Dimensions", value: "\(width)x\(height)")
|
||||
}
|
||||
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
|
||||
keys.append("Location")
|
||||
let latStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: latitude))
|
||||
let longStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))
|
||||
dic["Location"] = "\(latStr), \(longStr)"
|
||||
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 {
|
||||
keys.append("Duration")
|
||||
dic["Duration"] = OneDriveFileProvider.formatshort(interval: TimeInterval(duration) / 1000)
|
||||
add(key: "Duration", value: OneDriveFileProvider.formatshort(interval: TimeInterval(duration) / 1000))
|
||||
}
|
||||
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = resolve(dateString: timeTakenStr) {
|
||||
keys.append("Date taken")
|
||||
OneDriveFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
dic["Date taken"] = OneDriveFileProvider.dateFormatter.string(from: timeTaken)
|
||||
add(key: "Date taken", value: OneDriveFileProvider.dateFormatter.string(from: timeTaken))
|
||||
}
|
||||
|
||||
if let photo = json["photo"] as? [String: Any] {
|
||||
if let devicemake = photo["cameraMake"] as? String {
|
||||
keys.append("Device make")
|
||||
dic["Device make"] = devicemake
|
||||
}
|
||||
if let devicemodel = photo["cameraModel"] as? String {
|
||||
keys.append("Device model")
|
||||
dic["Device model"] = devicemodel
|
||||
}
|
||||
if let focallen = photo["focalLength"] as? Double {
|
||||
keys.append("Focal length")
|
||||
dic["Focal length"] = focallen
|
||||
}
|
||||
if let fnum = photo["fNumber"] as? Double {
|
||||
keys.append("F number")
|
||||
dic["F number"] = fnum
|
||||
}
|
||||
add(key: "Device make", value: photo["cameraMake"] as? String)
|
||||
add(key: "Device model", value: photo["cameraModel"] as? String)
|
||||
add(key: "focalLength", value: photo["focalLength"] as? Double)
|
||||
add(key: "fNumber", value: photo["fNumber"] as? Double)
|
||||
if let expNom = photo["exposureNumerator"] as? Double, let expDen = photo["exposureDenominator"] as? Double {
|
||||
keys.append("Exposure time")
|
||||
dic["Exposure time"] = "\(Int(expNom))/\(Int(expDen))"
|
||||
add(key: "Exposure time", value: "\(Int(expNom))/\(Int(expDen))")
|
||||
}
|
||||
if let iso = photo["iso"] as? Int64 {
|
||||
keys.append("ISO speed")
|
||||
dic["ISO speed"] = iso
|
||||
}
|
||||
|
||||
add(key: "ISO speed", value: photo["iso"] as? Int64)
|
||||
}
|
||||
|
||||
if let audio = json["audio"] as? [String: Any] {
|
||||
for (key, value) in audio {
|
||||
if key == "bitrate" || key == "isVariableBitrate" { continue }
|
||||
let casedKey = spaceCamelCase(key)
|
||||
keys.append(casedKey)
|
||||
dic[casedKey] = value
|
||||
add(key: casedKey, value: value)
|
||||
}
|
||||
}
|
||||
|
||||
if let video = json["video"] as? [String: Any] {
|
||||
if let bitRate = video["bitrate"] as? Int {
|
||||
keys.append("Bitrate")
|
||||
dic["Bitrate"] = bitRate
|
||||
}
|
||||
}
|
||||
add(key: "Bitrate", value: (json["video"] as? NSDictionary)?["bitrate"] as? Int)
|
||||
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
+104
-36
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Allows to get progress or cancel an in-progress operation, for remote, `URLSession` based providers.
|
||||
open class RemoteOperationHandle: OperationHandle {
|
||||
|
||||
internal var tasks: [Weak<URLSessionTask>]
|
||||
@@ -42,7 +43,7 @@ open class RemoteOperationHandle: OperationHandle {
|
||||
if let task = $1.value as? URLSessionUploadTask {
|
||||
return $0 + task.countOfBytesExpectedToSend
|
||||
} else {
|
||||
return $0 + ($1.value?.countOfBytesExpectedToSend ?? 0)
|
||||
return $0 + ($1.value?.countOfBytesExpectedToReceive ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +62,23 @@ open class RemoteOperationHandle: OperationHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// A protocol defines properties for errors returned by HTTP/S based providers.
|
||||
/// Including Dropbox, OneDrive and WebDAV.
|
||||
public protocol FileProviderHTTPError: Error, CustomStringConvertible {
|
||||
/// HTTP status code returned for error by server.
|
||||
var code: FileProviderHTTPErrorCode { get }
|
||||
/// Path of file/folder casued that error
|
||||
var path: String { get }
|
||||
/// Contents returned by server as error description
|
||||
var errorDescription: String? { get }
|
||||
}
|
||||
|
||||
extension FileProviderHTTPError {
|
||||
public var description: String {
|
||||
return code.description
|
||||
}
|
||||
}
|
||||
|
||||
class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDelegate {
|
||||
|
||||
weak var fileProvider: FileProvider?
|
||||
@@ -84,121 +102,170 @@ class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDeleg
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
|
||||
self.didSendDataHandler?(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
|
||||
|
||||
guard let desc = task.taskDescription, let json = jsonToDictionary(desc) else {
|
||||
return
|
||||
}
|
||||
guard let type = json["type"] as? String, let source = json["source"] as? String else {
|
||||
return
|
||||
}
|
||||
let dest = json["dest"] as? String
|
||||
let op : FileOperationType
|
||||
switch type {
|
||||
case "Create":
|
||||
op = .create(path: source)
|
||||
case "Copy":
|
||||
guard let dest = dest else { return }
|
||||
op = .copy(source: source, destination: dest)
|
||||
case "Move":
|
||||
guard let dest = dest else { return }
|
||||
op = .move(source: source, destination: dest)
|
||||
case "Modify":
|
||||
op = .modify(path: source)
|
||||
case "Remove":
|
||||
op = .remove(path: source)
|
||||
case "Link":
|
||||
guard let dest = dest else { return }
|
||||
op = .link(link: source, target: dest)
|
||||
default:
|
||||
guard let desc = task.taskDescription, let json = jsonToDictionary(desc),
|
||||
let op = FileOperationType(json: json), let fileProvider = fileProvider else {
|
||||
return
|
||||
}
|
||||
|
||||
let progress = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
|
||||
|
||||
fileProvider?.delegate?.fileproviderProgress(fileProvider!, operation: op, progress: progress)
|
||||
fileProvider.delegate?.fileproviderProgress(fileProvider, operation: op, progress: progress)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||
self.didReceivedData?(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
|
||||
|
||||
guard let desc = downloadTask.taskDescription, let json = jsonToDictionary(desc), let source = json["source"] as? String, let dest = json["dest"] as? String else {
|
||||
guard let desc = downloadTask.taskDescription, let json = jsonToDictionary(desc),
|
||||
let op = FileOperationType(json: json), let fileProvider = fileProvider else {
|
||||
return
|
||||
}
|
||||
|
||||
fileProvider?.delegate?.fileproviderProgress(fileProvider!, operation: .copy(source: source, destination: dest), progress: Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
|
||||
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) {
|
||||
let deposition: Foundation.URLSession.AuthChallengeDisposition = credential != nil ? .useCredential : .performDefaultHandling
|
||||
completionHandler(deposition, credential)
|
||||
authenticate(didReceive: challenge, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
let deposition: Foundation.URLSession.AuthChallengeDisposition = credential != nil ? .useCredential : .performDefaultHandling
|
||||
completionHandler(deposition, credential)
|
||||
authenticate(didReceive: challenge, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func authenticate(didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
switch (challenge.previousFailureCount, credential != nil) {
|
||||
case (0...1, true):
|
||||
completionHandler(.useCredential, credential)
|
||||
case (0, false):
|
||||
completionHandler(.useCredential, challenge.proposedCredential)
|
||||
default:
|
||||
completionHandler(.performDefaultHandling, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum FileProviderHTTPErrorCode: Int {
|
||||
/// HTTP status codes as an enum.
|
||||
public enum FileProviderHTTPErrorCode: Int, CustomStringConvertible {
|
||||
/// `Continue` informational status with HTTP code 100
|
||||
case `continue` = 100
|
||||
/// `Switching Protocols` informational status with HTTP code 101
|
||||
case switchingProtocols = 101
|
||||
/// `Processing` informational status with HTTP code 102
|
||||
case processing = 102
|
||||
/// `OK` success status with HTTP code 200
|
||||
case ok = 200
|
||||
/// `Created` success status with HTTP code 201
|
||||
case created = 201
|
||||
/// `Accepted` success status with HTTP code 202
|
||||
case accepted = 202
|
||||
/// `Non Authoritative Information` success status with HTTP code 203
|
||||
case nonAuthoritativeInformation = 203
|
||||
/// `No Content` success status with HTTP code 204
|
||||
case noContent = 204
|
||||
/// `ResetcContent` success status with HTTP code 205
|
||||
case resetContent = 205
|
||||
/// `Partial Content` success status with HTTP code 206
|
||||
case partialContent = 206
|
||||
/// `Multi Status` success status with HTTP code 207
|
||||
case multiStatus = 207
|
||||
/// `Already Reported` success status with HTTP code 208
|
||||
case alreadyReported = 208
|
||||
/// `IM Used` success status with HTTP code 226
|
||||
case imUsed = 226
|
||||
/// `Multiple Choices` redirection status with HTTP code 300
|
||||
case multipleChoices = 300
|
||||
/// `Moved Permanently` redirection status with HTTP code 301
|
||||
case movedPermanently = 301
|
||||
/// `Found` redirection status with HTTP code 302
|
||||
case found = 302
|
||||
/// `See Other` redirection status with HTTP code 303
|
||||
case seeOther = 303
|
||||
/// `Not Modified` redirection status with HTTP code 304
|
||||
case notModified = 304
|
||||
/// `Use Proxy` redirection status with HTTP code 305
|
||||
case useProxy = 305
|
||||
/// `Switch Proxy` redirection status with HTTP code 306
|
||||
case switchProxy = 306
|
||||
/// `Temporary Redirect` redirection status with HTTP code 307
|
||||
case temporaryRedirect = 307
|
||||
/// `Permanent Redirect` redirection status with HTTP code 308
|
||||
case permanentRedirect = 308
|
||||
/// `Bad Request` client error status with HTTP code 400
|
||||
case badRequest = 400
|
||||
/// `Unauthorized` client error status with HTTP code 401
|
||||
case unauthorized = 401
|
||||
/// `Payment Required` client error status with HTTP code 402
|
||||
case paymentRequired = 402
|
||||
/// `Forbidden` client error status with HTTP code 403
|
||||
case forbidden = 403
|
||||
/// `Not Found` client error status with HTTP code 404
|
||||
case notFound = 404
|
||||
/// `Method Not Allowed` client error status with HTTP code 405
|
||||
case methodNotAllowed = 405
|
||||
/// `Not Acceptable` client error status with HTTP code 406
|
||||
case notAcceptable = 406
|
||||
/// `Proxy Authentication Required` client error status with HTTP code 407
|
||||
case proxyAuthenticationRequired = 407
|
||||
/// `Request Timeout` client error status with HTTP code 408
|
||||
case requestTimeout = 408
|
||||
/// `Conflict` client error status with HTTP code 409
|
||||
case conflict = 409
|
||||
/// `Gone` client error status with HTTP code 410
|
||||
case gone = 410
|
||||
/// `Length Required` client error status with HTTP code 411
|
||||
case lengthRequired = 411
|
||||
/// `Precondition Failed` client error status with HTTP code 412
|
||||
case preconditionFailed = 412
|
||||
/// `Payload Too Large` client error status with HTTP code 413
|
||||
case payloadTooLarge = 413
|
||||
/// `URI Too Long` client error status with HTTP code 414
|
||||
case uriTooLong = 414
|
||||
/// `Unsupported Media Type` status with HTTP code 415
|
||||
case unsupportedMediaType = 415
|
||||
/// `Range Not Satisfiable` client error status with HTTP code 416
|
||||
case rangeNotSatisfiable = 416
|
||||
/// `Expectation Failed` client error status with HTTP code 417
|
||||
case expectationFailed = 417
|
||||
/// `Misdirected Request` client error status with HTTP code 421
|
||||
case misdirectedRequest = 421
|
||||
/// `Unprocessable Entity` client error status with HTTP code 422
|
||||
case unprocessableEntity = 422
|
||||
/// `Locked` client error status with HTTP code 423
|
||||
case locked = 423
|
||||
/// `Failed Dependency` client error status with HTTP code 424
|
||||
case failedDependency = 424
|
||||
/// `Unordered Collection` client error status with HTTP code 425
|
||||
case unorderedCollection = 425
|
||||
/// `Upgrade Required` client error status with HTTP code 426
|
||||
case upgradeRequired = 426
|
||||
/// `Precondition Required` client error status with HTTP code 428
|
||||
case preconditionRequired = 428
|
||||
/// `Too Many Requests` client error status with HTTP code 429
|
||||
case tooManyRequests = 429
|
||||
/// `Request Header Fields Too Large` client error status with HTTP code 431
|
||||
case requestHeaderFieldsTooLarge = 431
|
||||
/// `Unavailable For Legal Reasons` client error status with HTTP code 451
|
||||
case unavailableForLegalReasons = 451
|
||||
/// `Internal Server Error` server error status with HTTP code 500
|
||||
case internalServerError = 500
|
||||
/// `Bad Gateway` server error status with HTTP code 502
|
||||
case badGateway = 502
|
||||
/// `Service Unavailable` server error status with HTTP code 503
|
||||
case serviceUnavailable = 503
|
||||
/// `Gateway Timeout` server error status with HTTP code 504
|
||||
case gatewayTimeout = 504
|
||||
/// `HTTP Version Not Supported` server error status with HTTP code 505
|
||||
case httpVersionNotSupported = 505
|
||||
case variantlsoNegotiates = 506
|
||||
/// `Variant Also Negotiates` server error status with HTTP code 506
|
||||
case variantAlsoNegotiates = 506
|
||||
/// `Insufficient Storage` server error status with HTTP code 507
|
||||
case insufficientStorage = 507
|
||||
/// `Loop Detected` server error status with HTTP code 508
|
||||
case loopDetected = 508
|
||||
/// `Bandwidth Limit Exceeded` server error status with HTTP code 509
|
||||
case bandwidthLimitExceeded = 509
|
||||
/// `Not Extended` server error status with HTTP code 510
|
||||
case notExtended = 510
|
||||
/// `Network Authentication Required` server error status with HTTP code 511
|
||||
case networkAuthenticationRequired = 511
|
||||
|
||||
fileprivate static let status1xx: [Int: String] = [100: "Continue", 101: "Switching Protocols", 102: "Processing"]
|
||||
@@ -219,6 +286,7 @@ public enum FileProviderHTTPErrorCode: Int {
|
||||
}
|
||||
}
|
||||
|
||||
/// Description of status based on first digit which indicated fail or success.
|
||||
public var typeDescription: String {
|
||||
switch self.rawValue {
|
||||
case 100...199: return "Informational"
|
||||
@@ -226,7 +294,7 @@ public enum FileProviderHTTPErrorCode: Int {
|
||||
case 300...399: return "Redirection"
|
||||
case 400...499: return "Client Error"
|
||||
case 500...599: return "Server Error"
|
||||
default: return "Server Error"
|
||||
default: return "Unknown Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,11 +68,8 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
}
|
||||
let mId = messageId()
|
||||
let smbHeader = SMB2.Header(command: .TREE_CONNECT, creditRequestResponse: 123, messageId: mId, treeId: 0, sessionId: sessionId)
|
||||
var share = ""
|
||||
let cmp = url.pathComponents
|
||||
if cmp.count > 0 {
|
||||
share = cmp[0]
|
||||
}
|
||||
let share = cmp.first ?? ""
|
||||
let tcHeader = SMB2.TreeConnectRequest.Header(flags: [])
|
||||
let msg = SMB2.TreeConnectRequest(header: tcHeader, host: host, share: share)
|
||||
let data = createSMB2Message(header: smbHeader, message: msg!)
|
||||
|
||||
@@ -10,7 +10,6 @@ import Foundation
|
||||
|
||||
class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
open class var type: String { return "SMB" }
|
||||
open var isPathRelative: Bool = true
|
||||
open var baseURL: URL?
|
||||
open var currentPath: String = ""
|
||||
open var dispatch_queue: DispatchQueue
|
||||
@@ -44,6 +43,10 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate: FileOperationDelegate?
|
||||
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
@@ -96,7 +99,7 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
return nil
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) {
|
||||
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
@@ -122,7 +125,7 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
}
|
||||
|
||||
// MARK: basic CIFS interactivity
|
||||
public enum SMBFileProviderError: Int, Error, CustomStringConvertible {
|
||||
enum SMBFileProviderError: Int, Error, CustomStringConvertible {
|
||||
case badHeader
|
||||
case incompatibleHeader
|
||||
case incorrectParamsLength
|
||||
|
||||
@@ -106,6 +106,7 @@ extension SMB2 {
|
||||
let flags: WriteRequest.Flags
|
||||
}
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
init(fileId: FileId, offset: UInt64, remainingBytes: UInt32 = 0, data: Data, channel: Channel = .NONE, channelInfo: ChannelInfo? = nil, flags: WriteRequest.Flags = []) {
|
||||
var channelInfoOffset: UInt16 = 0
|
||||
var channelInfoLength: UInt16 = 0
|
||||
@@ -118,6 +119,7 @@ extension SMB2 {
|
||||
self.channelInfo = channelInfo
|
||||
self.fileData = data
|
||||
}
|
||||
// codebeat:enable[ARITY]
|
||||
|
||||
func data() -> Data {
|
||||
var result = Data(value: self.header)
|
||||
|
||||
@@ -8,12 +8,19 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Because this class uses NSURLSession, it's necessary to disable App Transport Security
|
||||
/// in case of using this class with unencrypted HTTP connection.
|
||||
|
||||
/**
|
||||
Allows accessing to WebDAV server files. This provider doesn't cache or save files internally, however you can
|
||||
set `useCache` and `cache` properties to use Foundation `NSURLCache` system.
|
||||
|
||||
WebDAV system supported by many cloud services including [Box.net](https://www.box.com/home)
|
||||
and [Yandex disk](https://disk.yandex.com) and [ownCloud](https://owncloud.org).
|
||||
|
||||
- Important: Because this class uses `URLSession`, it's necessary to disable App Transport Security
|
||||
in case of using this class with unencrypted HTTP connection.
|
||||
[Read this to know how](http://iosdevtips.co/post/121756573323/ios-9-xcode-7-http-connect-server-error).
|
||||
*/
|
||||
open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
open class var type: String { return "WebDAV" }
|
||||
open let isPathRelative: Bool
|
||||
open let baseURL: URL?
|
||||
open var currentPath: String
|
||||
|
||||
@@ -51,14 +58,13 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
- Parameters:
|
||||
- baseURL: Location of WebDAV server.
|
||||
- credential: An `URLCredential` object with `user` and `password`.
|
||||
- cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used.
|
||||
- cache: A URLCache to cache downloaded files and contents.
|
||||
*/
|
||||
public init? (baseURL: URL, credential: URLCredential?, cache: URLCache? = nil) {
|
||||
if !["http", "https"].contains(baseURL.uw_scheme.lowercased()) {
|
||||
return nil
|
||||
}
|
||||
self.baseURL = baseURL.appendingPathComponent("")
|
||||
self.isPathRelative = true
|
||||
self.baseURL = baseURL.path.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
|
||||
self.currentPath = ""
|
||||
self.useCache = false
|
||||
self.validatingCache = true
|
||||
@@ -89,7 +95,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
runDataTask(with: request, operationHandle: RemoteOperationHandle(operationType: opType, tasks: []), 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, url: url)
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
var fileObjects = [WebDavFileObject]()
|
||||
if let data = data {
|
||||
@@ -116,7 +122,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
runDataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode, code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, url: url)
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
if let data = data {
|
||||
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
@@ -156,12 +162,57 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
})
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [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)
|
||||
runDataTask(with: request, completionHandler: { (data, response, error) in
|
||||
// FIXME: paginating results
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
if let data = data {
|
||||
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
var fileObjects = [WebDavFileObject]()
|
||||
for attr in xresponse {
|
||||
let fileObject = WebDavFileObject(attr)
|
||||
if !query.evaluate(with: fileObject.mapPredicate()) {
|
||||
continue
|
||||
}
|
||||
|
||||
fileObjects.append(fileObject)
|
||||
foundItemHandler?(fileObject)
|
||||
}
|
||||
completionHandler(fileObjects, responseError ?? error)
|
||||
return
|
||||
}
|
||||
completionHandler([], responseError ?? error)
|
||||
})
|
||||
}
|
||||
|
||||
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
var request = URLRequest(url: baseURL!)
|
||||
request.httpMethod = "PROPFIND"
|
||||
request.setValue("0", 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:prop><D:quota-available-bytes/><D:quota-used-bytes/></D:prop>\n</D:propfind>".data(using: .utf8)
|
||||
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
|
||||
runDataTask(with: request, completionHandler: { (data, response, error) in
|
||||
let status = (response as? HTTPURLResponse)?.statusCode ?? 400
|
||||
completionHandler(status < 300)
|
||||
})
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate: FileOperationDelegate?
|
||||
}
|
||||
|
||||
extension WebDAVFileProvider: FileProviderOperations {
|
||||
@discardableResult
|
||||
public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
@@ -172,7 +223,7 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, url: url)
|
||||
responseError = FileProviderWebDavError(code: rCode, path: url.relativePath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
@@ -183,7 +234,7 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
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
|
||||
@@ -194,7 +245,7 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
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, url: url)
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
@@ -205,7 +256,7 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.move(source: path, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
@@ -214,7 +265,7 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
@@ -223,7 +274,7 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.remove(path: path)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
@@ -232,7 +283,8 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
}
|
||||
|
||||
func doOperation(operation opType: FileOperationType, overwrite: Bool? = nil, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let sourceURL = self.url(of:opType.source!)
|
||||
let source = opType.source!
|
||||
let sourceURL = self.url(of: source)
|
||||
var request = URLRequest(url: sourceURL)
|
||||
if let dest = opType.destination {
|
||||
request.setValue(url(of:dest).absoluteString, forHTTPHeaderField: "Destination")
|
||||
@@ -255,12 +307,13 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let response = response as? HTTPURLResponse, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
|
||||
if response.statusCode >= 300 {
|
||||
responseError = FileProviderWebDavError(code: code, url: sourceURL)
|
||||
responseError = FileProviderWebDavError(code: code, path: source, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: sourceURL)
|
||||
}
|
||||
if code == .multiStatus, let data = data {
|
||||
let xresponses = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
for xresponse in xresponses where (xresponse.status ?? 0) >= 300 {
|
||||
completionHandler?(FileProviderWebDavError(code: code, url: sourceURL))
|
||||
let error = FileProviderWebDavError(code: code, path: source, errorDescription: String(data: data, encoding: .utf8), url: sourceURL)
|
||||
completionHandler?(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,19 +329,21 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
// TODO: Make use of overwrite parameter
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let url = self.url(of:toPath)
|
||||
var request = URLRequest(url: url)
|
||||
if !overwrite {
|
||||
request.setValue("F", forHTTPHeaderField: "Overwrite")
|
||||
}
|
||||
request.httpMethod = "PUT"
|
||||
let task = session.uploadTask(with: request, fromFile: localFile, 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, url: url)
|
||||
responseError = FileProviderWebDavError(code: rCode, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
@@ -299,7 +354,7 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
@@ -309,7 +364,7 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
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, url: url)
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: nil, url: url)
|
||||
}
|
||||
if let sourceFileURL = sourceFileURL {
|
||||
do {
|
||||
@@ -330,8 +385,11 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
|
||||
extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
@discardableResult
|
||||
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
if length == 0 {
|
||||
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -348,7 +406,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
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, url: url)
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
completionHandler(data, responseError ?? error)
|
||||
})
|
||||
@@ -356,7 +414,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public 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
|
||||
@@ -371,7 +429,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
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, url: self.url(of: path))
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: self.url(of: path))
|
||||
}
|
||||
defer {
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
@@ -389,38 +447,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
|
||||
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [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)
|
||||
runDataTask(with: request, completionHandler: { (data, response, error) in
|
||||
// FIXME: paginating results
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, url: url)
|
||||
}
|
||||
if let data = data {
|
||||
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
var fileObjects = [WebDavFileObject]()
|
||||
for attr in xresponse {
|
||||
let path = attr.href.path
|
||||
if !((path as NSString).lastPathComponent.contains(query)) {
|
||||
continue
|
||||
}
|
||||
let fileObject = WebDavFileObject(attr)
|
||||
fileObjects.append(fileObject)
|
||||
foundItemHandler?(fileObject)
|
||||
}
|
||||
completionHandler(fileObjects, responseError ?? error)
|
||||
return
|
||||
}
|
||||
completionHandler([], responseError ?? error)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
|
||||
/* There is no unified api for monitoring WebDAV server content change/update
|
||||
* Microsoft Exchange uses SUBSCRIBE method, Apple uses push notification system.
|
||||
@@ -432,7 +459,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
}
|
||||
fileprivate func unregisterNotifcation(path: String) {
|
||||
NotImplemented()
|
||||
}
|
||||
}*/
|
||||
// TODO: implements methods for lock mechanism
|
||||
}
|
||||
|
||||
@@ -537,29 +564,27 @@ struct DavResponse {
|
||||
}
|
||||
|
||||
static func parse(xmlResponse: Data, baseURL: URL?) -> [DavResponse] {
|
||||
guard let xml = try? AEXMLDocument(xml: xmlResponse) else { return [] }
|
||||
var result = [DavResponse]()
|
||||
do {
|
||||
let xml = try AEXMLDocument(xml: xmlResponse)
|
||||
var rootnode = xml.root
|
||||
var responsetag = "response"
|
||||
for node in rootnode.all ?? [] where node.name.lowercased().hasSuffix("multistatus") {
|
||||
rootnode = node
|
||||
var rootnode = xml.root
|
||||
var responsetag = "response"
|
||||
for node in rootnode.all ?? [] where node.name.lowercased().hasSuffix("multistatus") {
|
||||
rootnode = node
|
||||
}
|
||||
for node in rootnode.children where node.name.lowercased().hasSuffix("response") {
|
||||
responsetag = node.name
|
||||
break
|
||||
}
|
||||
for responseNode in rootnode[responsetag].all ?? [] {
|
||||
if let davResponse = DavResponse(responseNode, baseURL: baseURL) {
|
||||
result.append(davResponse)
|
||||
}
|
||||
for node in rootnode.children where node.name.lowercased().hasSuffix("response") {
|
||||
responsetag = node.name
|
||||
break
|
||||
}
|
||||
for responseNode in rootnode[responsetag].all ?? [] {
|
||||
if let davResponse = DavResponse(responseNode, baseURL: baseURL) {
|
||||
result.append(davResponse)
|
||||
}
|
||||
}
|
||||
} catch _ {
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/// Containts path, url and attributes of a WebDAV file or resource.
|
||||
public final class WebDavFileObject: FileObject {
|
||||
internal init(_ davResponse: DavResponse) {
|
||||
let href = davResponse.href
|
||||
@@ -576,32 +601,32 @@ public final class WebDavFileObject: FileObject {
|
||||
self.entryTag = davResponse.prop["getetag"]
|
||||
}
|
||||
|
||||
/// MIME type of the file
|
||||
/// MIME type of the file.
|
||||
open internal(set) var contentType: String {
|
||||
get {
|
||||
return allValues["NSURLContentTypeKey"] as? String ?? ""
|
||||
return allValues[.mimeType] as? String ?? ""
|
||||
}
|
||||
set {
|
||||
allValues["NSURLContentTypeKey"] = newValue
|
||||
allValues[.mimeType] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP E-Tag, can be used to mark changed files
|
||||
/// HTTP E-Tag, can be used to mark changed files.
|
||||
open internal(set) var entryTag: String? {
|
||||
get {
|
||||
return allValues["NSURLEntryTagKey"] as? String
|
||||
return allValues[.entryTag] as? String
|
||||
}
|
||||
set {
|
||||
allValues["NSURLEntryTagKey"] = newValue
|
||||
allValues[.entryTag] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct FileProviderWebDavError: Error, CustomStringConvertible {
|
||||
/// Error returned by WebDAV server when trying to access or do operations on a file or folder.
|
||||
public struct FileProviderWebDavError: FileProviderHTTPError {
|
||||
public let code: FileProviderHTTPErrorCode
|
||||
public let path: String
|
||||
public let errorDescription: String?
|
||||
/// URL of resource caused error.
|
||||
public let url: URL
|
||||
|
||||
public var description: String {
|
||||
return code.description
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Reference in New Issue
Block a user