Compare commits

..

14 Commits

Author SHA1 Message Date
Amir Abbas Mousavian 330a22c45d Completed Documentation, fixed a small bug. 2017-02-16 13:00:50 +03:30
Amir Abbas e899804e28 Added isReachable method to test connectivity
- Fixed Cloud provider bug when moving/deleting evicted file
2017-02-14 22:58:07 +03:30
Amir Abbas 489a9a16d0 Updated documentation, fixed Cloud provider paths bugs 2017-02-14 12:24:06 +03:30
Amir Abbas 93359d5173 Made FileProviders equatable 2017-02-13 16:45:34 +03:30
Amir Abbas 12ff3a410d Fixed critical bug in LocalFileObject when decoding path 2017-02-13 16:31:29 +03:30
Amir Abbas a7de09362b Equatable FileObject, async completion handler in cloud provider 2017-02-13 12:06:09 +03:30
Amir Abbas b1fe37cf2a Updated Readme, fixed several bugs
- non-locking snapshot of file while uploading to iCloud
- fixed: `CloudFIleProvider.doOperation()` couldn’t handle local urls
- fixed: FileProvider.type didn’t work correctly
2017-02-12 22:45:30 +03:30
Amir Abbas bd59eacee2 Fixed bugs in providers and LocalFileObject initializing 2017-02-12 03:07:40 +03:30
Amir Abbas 42579be371 Added FileProviderHTTPError protocol
- All errors from local provider is now from Cocoa error domain. No URL domain anymore.
- All errors from WebDAV/OneDrive/Dropbox conform to FileProviderHTTPError
2017-02-11 18:05:21 +03:30
Amir Abbas edd4914ca7 Linting, fixed travis wrong device 2017-02-11 06:08:17 +03:30
Amir Abbas bd3e1bd74f Fixed typos in travis.yml 2017-02-10 02:57:15 +03:30
Amir Abbas 371c481482 Fixed project name typo in travis.yml 2017-02-09 15:02:39 +03:30
Amir Abbas dea1d01bf3 Made travis.yml more generic 2017-02-09 14:46:29 +03:30
Amir Abbas ae4cd1dff3 Fixed undo manager bugs
- Fixed contents of file with offset method bugs
- Improved `LocalFileProvider.copy()` implementation
2017-02-09 03:02:21 +03:30
20 changed files with 806 additions and 174 deletions
+62 -27
View File
@@ -1,45 +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"
# matrix:
# - SHCEME="FileProvider OSX" SDK="macosx" ACTION="build"
# - SHCEME="FileProvider iOS" SDK="iphonesimulator" ACTION="build"
# - SHCEME="FileProvider tvOS" SDK="appletvsimulator" ACTION="build"
- 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:
- 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
- pod lib lint --quick
# - xcodebuild -project $PROJECTNAME.xcodeproj -scheme "$SCHEME" -sdk $SDK $ACTION ONLY_ACTIVE_ARCH=NO
- xcodebuild -project $PROJECTNAME.xcodeproj -scheme "FileProvider OSX" -sdk macosx build | xcpretty
- xcodebuild -project $PROJECTNAME.xcodeproj -scheme "FileProvider iOS" -sdk iphonesimulator build ONLY_ACTIVE_ARCH=NO | xcpretty
- xcodebuild -project $PROJECTNAME.xcodeproj -scheme "FileProvider tvOS" -sdk appletvsimulator build ONLY_ACTIVE_ARCH=NO | xcpretty
# 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;
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:
- brew update
- brew outdated carthage || brew upgrade carthage
- carthage version
- 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:
provider: releases
api_key: "$GITHUBTOKEN"
file: $FRAMEWORK_NAME.zip
skip_cleanup: true
on:
repo: amosavian/FileProvider
tags: true
repo: amosavian/$PROJECTNAME
tags: true
comdition: "$CARTHAGEDEPLOY = YES"
+1 -1
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
#
s.name = "FileProvider"
s.version = "0.12.3"
s.version = "0.12.9"
s.summary = "FileManager replacement for Local and Remote (WebDAV/Dropbox/OneDrive/SMB2) files on iOS and macOS."
# This description is used to generate tags and improve search results.
+2 -8
View File
@@ -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.3;
BUNDLE_VERSION_STRING = 0.12.9;
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.3;
BUNDLE_VERSION_STRING = 0.12.9;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
+61 -13
View File
@@ -1,4 +1,4 @@
# FileProvider
![File Provider](fileprovider.png)
>This Swift library provide a swifty way to deal with local and remote files and directories in a unified way.
@@ -24,8 +24,6 @@ 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.
@@ -125,7 +123,7 @@ let documentsProvider = LocalFileProvider(sharedContainerId: "group.yourcompany.
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
@@ -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
@@ -402,7 +448,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.
+229 -18
View File
@@ -8,7 +8,15 @@
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" }
/// Forces file operations to use `NSFileCoordinating`,
@@ -18,6 +26,7 @@ open class CloudFileProvider: LocalFileProvider {
return true
}
set {
assert(true, "CloudFileProvider.isCoorinating can't be set")
return
}
}
@@ -28,6 +37,9 @@ open class CloudFileProvider: LocalFileProvider {
/// 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.
@@ -40,8 +52,8 @@ open class CloudFileProvider: LocalFileProvider {
- Parameter scope: Use `.documents` (default) to put documents that the user is allowed to access inside a Documents subdirectory. Otherwise use `.data` to store user-related data files that your app needs to share but that are not files you want the user to manipulate directly.
*/
public init? (containerId: String?, scope: UbiquitousScope = .documents) {
assert(!Thread.isMainThread, "LocalFileProvider.init(containerId:) is not recommended to be executed on Main Thread.")
guard FileManager.default.ubiquityIdentityToken == nil else {
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 {
@@ -51,9 +63,9 @@ open class CloudFileProvider: LocalFileProvider {
self.scope = scope
let baseURL: URL
if scope == .documents {
baseURL = ubiquityURL.standardized.appendingPathComponent("Documents/")
baseURL = ubiquityURL.appendingPathComponent("Documents/")
} else {
baseURL = ubiquityURL.standardized
baseURL = ubiquityURL
}
super.init(baseURL: baseURL)
@@ -69,13 +81,24 @@ open class CloudFileProvider: LocalFileProvider {
try? fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true)
}
// FIXME: create runloop for dispatch_queue, start query on it
/**
Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler.
If the directory contains no entries or an error is occured, this method will return the empty array.
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
- Parameter completionHandler: a block with result of directory entries or error.
`contents`: An array of `FileObject` identifying the the directory entries.
`error`: Error returned by system.
*/
open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
// FIXME: create runloop for dispatch_queue, start query on it
dispatch_queue.async {
let pathURL = self.url(of: path)
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K BEGINSWITH %@", NSMetadataItemPathKey, pathURL.path)
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]
query.searchScopes = [self.scope.rawValue]
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
@@ -106,26 +129,43 @@ open class CloudFileProvider: LocalFileProvider {
}
query.stop()
completionHandler(contents, nil)
self.dispatch_queue.async {
completionHandler(contents, nil)
}
})
DispatchQueue.main.async {
if !query.start() {
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
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 block 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.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
@@ -138,55 +178,138 @@ 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)
}
}
})
DispatchQueue.main.async {
if !query.start() {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadNoPermission))
self.dispatch_queue.async {
completionHandler(nil, 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
@@ -221,6 +344,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)
@@ -239,24 +372,71 @@ 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 block with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress.
*/
@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 block with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress.
*/
@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)
}
/**
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.
*/
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)
@@ -269,6 +449,7 @@ open class CloudFileProvider: LocalFileProvider {
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: query, queue: nil, using: { (notification) in
query.disableUpdates()
@@ -321,13 +502,16 @@ open class CloudFileProvider: LocalFileProvider {
contents.append(file)
}
}
completionHandler(contents, nil)
self.dispatch_queue.async {
completionHandler(contents, nil)
}
})
DispatchQueue.main.async {
if !query.start() {
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
self.dispatch_queue.async {
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
}
}
}
}
@@ -335,11 +519,27 @@ open class CloudFileProvider: LocalFileProvider {
fileprivate var monitors = [String: (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: Block 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.valueListAttributes = []
query.searchScopes = [self.scope.rawValue]
let updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: nil, using: { (notification) in
@@ -358,6 +558,9 @@ open class CloudFileProvider: LocalFileProvider {
}
}
/// Stops monitoring the path.
///
/// - Parameter path: path of directory.
open override func unregisterNotifcation(path: String) {
guard let (query, observer) = monitors[path] else {
return
@@ -368,6 +571,10 @@ open class CloudFileProvider: LocalFileProvider {
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[path] != nil
}
@@ -382,7 +589,7 @@ open class CloudFileProvider: LocalFileProvider {
}
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
}
@@ -416,14 +623,18 @@ 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)) {
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)
}
}
}
}
+14 -1
View File
@@ -10,6 +10,12 @@
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
@@ -129,6 +135,12 @@ open class DropboxFileProvider: FileProviderBasicRemote {
task.resume()
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
self.storageProperties { total, _ in
completionHandler(total > 0)
}
}
open weak var fileOperationDelegate: FileOperationDelegate?
}
@@ -293,6 +305,7 @@ extension DropboxFileProvider: FileProviderReadWrite {
})
}
/*
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
@@ -305,7 +318,7 @@ extension DropboxFileProvider: FileProviderReadWrite {
fileprivate func unregisterNotifcation(path: String) {
NotImplemented()
}
*/
// TODO: Implement /get_account & /get_current_account
}
+10 -6
View File
@@ -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,6 +39,7 @@ 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[.serverDate] as? Date
@@ -50,6 +49,9 @@ public final class DropboxFileObject: FileObject {
}
}
/// The document identifier is a value assigned by the Dropbox to a file.
/// This value is used to identify the document regardless of where it is moved on a volume.
/// The identifier persists across system restarts.
open internal(set) var id: String? {
get {
return allValues[.documentIdentifierKey] as? String
@@ -59,6 +61,8 @@ public final class DropboxFileObject: FileObject {
}
}
/// 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[.generationIdentifierKey] as? String
@@ -69,7 +73,7 @@ public final class DropboxFileObject: FileObject {
}
}
// 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)) {
var requestDictionary = [String: AnyObject]()
+76 -7
View File
@@ -17,7 +17,6 @@ import Cocoa
#endif
extension LocalFileProvider: ExtendedFileProvider {
public func thumbnailOfFileSupported(path: String) -> Bool {
switch (path as NSString).pathExtension.lowercased() {
case LocalFileInformationGenerator.imageThumbnailExtensions:
@@ -126,26 +125,80 @@ extension LocalFileProvider: ExtendedFileProvider {
}
}
/// 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
@@ -159,6 +212,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)
@@ -166,7 +220,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
@@ -174,19 +228,25 @@ 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]()
@@ -252,6 +312,7 @@ public struct LocalFileInformationGenerator {
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]()
@@ -293,6 +354,7 @@ public struct LocalFileInformationGenerator {
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]()
@@ -337,6 +399,7 @@ public struct LocalFileInformationGenerator {
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]()
@@ -411,10 +474,16 @@ public struct LocalFileInformationGenerator {
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
}
+5 -5
View File
@@ -14,7 +14,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 +449,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
+19 -4
View File
@@ -8,8 +8,8 @@
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: [URLResourceKey: Any]
@@ -142,6 +142,20 @@ 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 resolve(dateString: String) -> Date? {
@@ -270,18 +284,19 @@ 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] {
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
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
+52 -9
View File
@@ -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 }
@@ -85,7 +87,7 @@ public protocol FileProviderBasic: class {
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))
/**
@@ -95,9 +97,12 @@ 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 {
/// The maximum number of queued operations that can execute at the same time.
///
/// The default value of this property is `OperationQueue.defaultMaxConcurrentOperationCount`.
@@ -111,8 +116,19 @@ 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.isPathRelative == rhs.isPathRelative && 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 }
@@ -178,6 +194,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 +207,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?
@@ -337,6 +355,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.
@@ -461,6 +480,7 @@ extension FileProviderReadWrite {
}
}
/// Allows a file provider to notify changes occured
public protocol FileProviderMonitor: FileProviderBasic {
/**
@@ -492,10 +512,17 @@ 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
}
@@ -526,15 +553,23 @@ public extension FileProvideUndoable {
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
@@ -585,7 +620,9 @@ extension FileProviderBasic {
// 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? {
@@ -630,7 +667,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)
}
@@ -652,6 +689,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.
///
@@ -819,14 +857,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
@@ -863,7 +901,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"
}
@@ -895,7 +933,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 }
@@ -924,6 +962,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.
@@ -937,6 +978,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.
@@ -953,6 +995,7 @@ internal class Weak<T: AnyObject> {
}
}
/// For internal use in `FileProvider` framework
public protocol FoundationErrorEnum {
init? (rawValue: Int)
var rawValue: Int { get }
+87 -38
View File
@@ -8,6 +8,12 @@
import Foundation
/**
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
@@ -73,7 +79,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
case .cachesDirectory:
finalBaseURL = baseURL.appendingPathComponent("Library/Caches")
case .applicationSupportDirectory:
finalBaseURL = baseURL.appendingPathComponent("Library/Application%20support")
finalBaseURL = baseURL.appendingPathComponent("Library/Application support")
default:
break
}
@@ -139,6 +145,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
dispatch_queue.async {
completionHandler(self.fileManager.isReadableFile(atPath: self.baseURL!.path))
}
}
open weak var fileOperationDelegate : FileOperationDelegate?
@discardableResult
@@ -161,7 +173,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
let opType = FileOperationType.move(source: path, destination: toPath)
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
completionHandler?(self.throwError(toPath, code: URLError.cannotMoveFile as FoundationErrorEnum))
completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum))
return nil
}
@@ -173,7 +185,9 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
let opType = FileOperationType.copy(source: path, destination: toPath)
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
completionHandler?(self.throwError(toPath, code: URLError.cannotWriteToFile as FoundationErrorEnum))
self.dispatch_queue.async {
completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum))
}
return nil
}
@@ -188,9 +202,14 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
@discardableResult
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
// TODO: Make use of overwrite parameter
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, completionHandler: completionHandler)
return self.doOperation(opType, forUploading: true, completionHandler: completionHandler)
}
@discardableResult
@@ -200,26 +219,31 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
dynamic func doSimpleOperation(_ box: UndoBox) {
_ = self.doOperation(box.operation) { (_) in
guard let _ = self.undoManager else { return }
_ = self.doOperation(box.undoOperation) { (_) in
return
}
}
@discardableResult
fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard let sourcePath = opType.source else { return nil }
let destPath = opType.destination
let source: URL, dest: URL?
let source: URL
if sourcePath.hasPrefix("file://") {
source = URL(string: sourcePath)!
let removedSchemePath = sourcePath.replacingOccurrences(of: "file://", with: "", options: .anchored)
let pDecodedPath = removedSchemePath.removingPercentEncoding ?? removedSchemePath
source = URL(fileURLWithPath: pDecodedPath)
} else {
source = self.url(of: sourcePath)
}
let dest: URL?
if let destPath = destPath {
if destPath.hasPrefix("file://") {
dest = URL(string: destPath)!
let removedSchemePath = destPath.replacingOccurrences(of: "file://", with: "", options: .anchored)
let pDecodedPath = removedSchemePath.removingPercentEncoding ?? removedSchemePath
dest = URL(fileURLWithPath: pDecodedPath)
} else {
dest = self.url(of: destPath)
}
@@ -227,8 +251,17 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
dest = nil
}
if let undoManager = self.undoManager, let undoOp = self.undoOperation(for: opType) {
let undoBox = UndoBox(provider: self, operation: opType, undoOperation: undoOp)
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
let successfulSecurityScopedResourceAccess = source.startAccessingSecurityScopedResource()
do {
switch opType {
case .create:
@@ -254,12 +287,9 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
source.stopAccessingSecurityScopedResource()
}
if let undoOp = self.undoOperation(for: opType) {
let undoBox = UndoBox(provider: self, operation: undoOp)
self.undoManager?.registerUndo(withTarget: self, selector: #selector(LocalFileProvider.doSimpleOperation(_:)), object: undoBox)
self.dispatch_queue.async {
completionHandler?(nil)
}
completionHandler?(nil)
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
}
@@ -267,7 +297,9 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
if successfulSecurityScopedResourceAccess {
source.stopAccessingSecurityScopedResource()
}
completionHandler?(e)
self.dispatch_queue.async {
completionHandler?(e)
}
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
@@ -276,23 +308,27 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
if isCoorinating {
var intents = [NSFileAccessIntent]()
successfulSecurityScopedResourceAccess = source.startAccessingSecurityScopedResource()
switch opType {
case .create, .remove, .modify:
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: .withoutChanges))
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: .forDeleting))
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
completionHandler?(error)
self.dispatch_queue.async {
completionHandler?(error)
}
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
@@ -314,16 +350,22 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
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)
}
@@ -354,12 +396,10 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
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: CocoaError.fileNoSuchFile as FoundationErrorEnum))
return
}
guard let handle = FileHandle(forReadingAtPath: url.path) else {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadNoPermission as FoundationErrorEnum))
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: CocoaError.fileNoSuchFile as FoundationErrorEnum))
}
return
}
@@ -367,19 +407,26 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
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 {
@@ -459,9 +506,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
open func copy(with zone: NSZone? = nil) -> Any {
let copy = LocalFileProvider(baseURL: self.baseURL!)
copy.currentPath = self.currentPath
copy.isPathRelative = self.isPathRelative
copy.undoManager = self.undoManager
copy.isCoorinating = self.isCoorinating
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
copy.isPathRelative = self.isPathRelative
return copy
}
}
+15 -3
View File
@@ -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,6 +37,7 @@ public final class LocalFileObject: FileObject {
}
}
/// Initiates a `LocalFileObject` with attributes of file in url.
public convenience init?(fileWithURL fileURL: URL) {
do {
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey, .generationIdentifierKey, .documentIdentifierKey])
@@ -49,6 +52,7 @@ public final class LocalFileObject: FileObject {
}
}
/// The total size allocated on disk for the file
open internal(set) var allocatedSize: Int64 {
get {
return allValues[.fileAllocatedSizeKey] as? Int64 ?? 0
@@ -58,6 +62,9 @@ public final class LocalFileObject: FileObject {
}
}
/// 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
@@ -67,6 +74,8 @@ public final class LocalFileObject: FileObject {
}
}
/// 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[.generationIdentifierKey] as? Data
@@ -207,6 +216,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
@@ -308,10 +318,12 @@ open class LocalOperationHandle: OperationHandle {
class UndoBox: NSObject {
weak var provider: FileProvideUndoable?
let operation: FileOperationType
let undoOperation: FileOperationType
init(provider: FileProvideUndoable, operation: FileOperationType) {
init(provider: FileProvideUndoable, operation: FileOperationType, undoOperation: FileOperationType) {
self.provider = provider
self.operation = operation
self.undoOperation = undoOperation
}
}
+21 -1
View File
@@ -10,6 +10,13 @@
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
@@ -67,7 +74,8 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
- cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used.
*/
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 = ""
@@ -131,6 +139,18 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
task.resume()
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
let url = URL(string: "/drive/root", relativeTo: baseURL)!
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?
}
+8 -5
View File
@@ -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
@@ -46,6 +44,9 @@ 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[.documentIdentifierKey] as? String
@@ -55,6 +56,7 @@ public final class OneDriveFileObject: FileObject {
}
}
/// MIME type of file contents returned by OneDrive server.
open internal(set) var contentType: String {
get {
return allValues[.mimeType] as? String ?? ""
@@ -64,6 +66,7 @@ public final class OneDriveFileObject: FileObject {
}
}
/// HTTP E-Tag, can be used to mark changed files.
open internal(set) var entryTag: String? {
get {
return allValues[.entryTag] as? String
+85 -2
View File
@@ -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>]
@@ -61,6 +62,25 @@ 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 }
var description: String { get }
}
extension FileProviderHTTPError {
public var description: String {
return code.description
}
}
class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDelegate {
weak var fileProvider: FileProvider?
@@ -138,67 +158,129 @@ class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDeleg
}
}
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 +301,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"
+5 -1
View File
@@ -44,6 +44,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? {
@@ -122,7 +126,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
+2
View File
@@ -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)
+52 -25
View File
@@ -8,9 +8,17 @@
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).
- 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
@@ -57,7 +65,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
if !["http", "https"].contains(baseURL.uw_scheme.lowercased()) {
return nil
}
self.baseURL = baseURL.appendingPathComponent("")
self.baseURL = baseURL.path.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
self.isPathRelative = true
self.currentPath = ""
self.useCache = false
@@ -89,7 +97,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 +124,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,6 +164,19 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
})
}
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?
}
@@ -172,7 +193,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)
@@ -194,7 +215,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)
@@ -232,7 +253,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 +277,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)
}
}
}
@@ -277,18 +300,20 @@ extension WebDAVFileProvider: FileProviderOperations {
@discardableResult
public 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)
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)
@@ -309,7 +334,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 {
@@ -351,7 +376,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)
})
@@ -374,7 +399,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)
@@ -403,7 +428,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
// 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)
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)
@@ -424,6 +449,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
})
}
/*
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.
@@ -435,7 +461,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
}
fileprivate func unregisterNotifcation(path: String) {
NotImplemented()
}
}*/
// TODO: implements methods for lock mechanism
}
@@ -563,6 +589,7 @@ struct DavResponse {
}
}
/// Containts path, url and attributes of a WebDAV file or resource.
public final class WebDavFileObject: FileObject {
internal init(_ davResponse: DavResponse) {
let href = davResponse.href
@@ -579,7 +606,7 @@ 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[.mimeType] as? String ?? ""
@@ -589,7 +616,7 @@ public final class WebDavFileObject: FileObject {
}
}
/// 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[.entryTag] as? String
@@ -600,11 +627,11 @@ public final class WebDavFileObject: FileObject {
}
}
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
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB