Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e899804e28 | |||
| 489a9a16d0 | |||
| 93359d5173 | |||
| 12ff3a410d | |||
| a7de09362b | |||
| b1fe37cf2a | |||
| bd59eacee2 | |||
| 42579be371 | |||
| edd4914ca7 | |||
| bd3e1bd74f | |||
| 371c481482 | |||
| dea1d01bf3 |
+62
-28
@@ -1,46 +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 build --no-skip-current --verbose
|
||||
- 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"
|
||||
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
|
||||
#
|
||||
|
||||
s.name = "FileProvider"
|
||||
s.version = "0.12.4"
|
||||
s.version = "0.12.8"
|
||||
s.summary = "FileManager replacement for Local and Remote (WebDAV/Dropbox/OneDrive/SMB2) files on iOS and macOS."
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
|
||||
@@ -162,14 +162,11 @@
|
||||
79BD63A71E2CC2940035128C /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/CoreGraphics.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63A91E2CC2BB0035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63AB1E2CC2C20035128C /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/ImageIO.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63AD1E2CC2EB0035128C /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/MediaPlayer.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63AF1E2CC3300035128C /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; };
|
||||
79BD63B11E2CC3350035128C /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; };
|
||||
79BD63B31E2CC33D0035128C /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; };
|
||||
79BD63B51E2CC3860035128C /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
|
||||
79BD63B71E2CC38D0035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
|
||||
79BD63B91E2CC39B0035128C /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/usr/lib/libxml2.tbd; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63BB1E2CC3B90035128C /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/MediaPlayer.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63BD1E2CC3C20035128C /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/ImageIO.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/CoreGraphics.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63C11E2CC3D30035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||
@@ -221,14 +218,11 @@
|
||||
79BD63C11E2CC3D30035128C /* AVFoundation.framework */,
|
||||
79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */,
|
||||
79BD63BD1E2CC3C20035128C /* ImageIO.framework */,
|
||||
79BD63BB1E2CC3B90035128C /* MediaPlayer.framework */,
|
||||
79BD63B91E2CC39B0035128C /* libxml2.tbd */,
|
||||
79BD63B71E2CC38D0035128C /* AVFoundation.framework */,
|
||||
79BD63B51E2CC3860035128C /* CoreFoundation.framework */,
|
||||
79BD63B31E2CC33D0035128C /* MediaPlayer.framework */,
|
||||
79BD63B11E2CC3350035128C /* ImageIO.framework */,
|
||||
79BD63AF1E2CC3300035128C /* libxml2.tbd */,
|
||||
79BD63AD1E2CC2EB0035128C /* MediaPlayer.framework */,
|
||||
79BD63AB1E2CC2C20035128C /* ImageIO.framework */,
|
||||
79BD63A91E2CC2BB0035128C /* AVFoundation.framework */,
|
||||
79BD63A71E2CC2940035128C /* CoreGraphics.framework */,
|
||||
@@ -603,7 +597,7 @@
|
||||
799396601D48B7BF00086753 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.12.4;
|
||||
BUNDLE_VERSION_STRING = 0.12.8;
|
||||
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.4;
|
||||
BUNDLE_VERSION_STRING = 0.12.8;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# FileProvider
|
||||

|
||||
|
||||
>This Swift library provide a swifty way to deal with local and remote files and directories in a unified way.
|
||||
|
||||
@@ -24,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:
|
||||
@@ -384,8 +388,7 @@ You can monitor updates in some file system (Local and SMB2), there is three met
|
||||
|
||||
```swift
|
||||
// to register a new notification handler
|
||||
documentsProvider.registerNotifcation(path: provider.currentPath)
|
||||
{
|
||||
documentsProvider.registerNotifcation(path: provider.currentPath) {
|
||||
// calling functions to update UI
|
||||
}
|
||||
|
||||
@@ -408,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
|
||||
}
|
||||
@@ -420,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
|
||||
@@ -445,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.
|
||||
|
||||
|
||||
+221
-18
@@ -18,6 +18,7 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
return true
|
||||
}
|
||||
set {
|
||||
assert(true, "CloudFileProvider.isCoorinating can't be set")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -28,6 +29,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 +44,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 +55,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 +73,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 +121,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 +170,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 +336,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 +364,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 +441,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 +494,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 +511,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 +550,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 +563,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 +581,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 +615,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,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?
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,10 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct FileProviderDropboxError: Error, CustomStringConvertible {
|
||||
public struct FileProviderDropboxError: FileProviderHTTPError {
|
||||
public let code: FileProviderHTTPErrorCode
|
||||
public let path: String
|
||||
public let errorDescription: String?
|
||||
|
||||
public var description: String {
|
||||
return code.description
|
||||
}
|
||||
}
|
||||
|
||||
public final class DropboxFileObject: FileObject {
|
||||
@@ -69,7 +65,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]()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
/// Containts path and attributes of a file or resource.
|
||||
open class FileObject {
|
||||
open class FileObject: Equatable {
|
||||
/// A `Dictionary` contains file information, using `URLResourceKey` keys.
|
||||
open internal(set) var allValues: [URLResourceKey: Any]
|
||||
|
||||
@@ -142,6 +142,19 @@ open class FileObject {
|
||||
open var isSymLink: Bool {
|
||||
return self.type == .symbolicLink
|
||||
}
|
||||
|
||||
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,14 +283,14 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,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 +95,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,6 +114,15 @@ public extension FileProviderBasic {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
public protocol FileProviderBasicRemote: FileProviderBasic {
|
||||
@@ -190,6 +202,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?
|
||||
@@ -547,7 +560,7 @@ public protocol FileProvider: FileProviderBasic, FileProviderOperations, FilePro
|
||||
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
|
||||
@@ -598,7 +611,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? {
|
||||
@@ -643,7 +658,7 @@ extension FileProviderBasic {
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
_ = group.wait(timeout: DispatchTime.distantFuture)
|
||||
_ = group.wait(timeout: DispatchTime.now() + 0.5)
|
||||
let finalFile = result + (!fileExt.isEmpty ? "." + fileExt : "")
|
||||
return (dirPath as NSString).appendingPathComponent(finalFile)
|
||||
}
|
||||
@@ -832,14 +847,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
|
||||
@@ -876,7 +891,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"
|
||||
}
|
||||
|
||||
@@ -73,7 +73,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 +139,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 +167,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 +179,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 +196,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
|
||||
@@ -207,14 +220,27 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
}
|
||||
|
||||
@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 = sourcePath.hasPrefix("file://") ? URL(string: sourcePath)! : self.url(of: sourcePath)
|
||||
let dest: URL?
|
||||
let source: URL
|
||||
if sourcePath.hasPrefix("file://") {
|
||||
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 {
|
||||
dest = destPath.hasPrefix("file://") ? URL(string: destPath)! : self.url(of: destPath)
|
||||
if destPath.hasPrefix("file://") {
|
||||
let removedSchemePath = destPath.replacingOccurrences(of: "file://", with: "", options: .anchored)
|
||||
let pDecodedPath = removedSchemePath.removingPercentEncoding ?? removedSchemePath
|
||||
dest = URL(fileURLWithPath: pDecodedPath)
|
||||
} else {
|
||||
dest = self.url(of: destPath)
|
||||
}
|
||||
} else {
|
||||
dest = nil
|
||||
}
|
||||
@@ -227,8 +253,9 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
undoManager.endUndoGrouping()
|
||||
}
|
||||
|
||||
var successfulSecurityScopedResourceAccess = false
|
||||
|
||||
let operationHandler: (URL, URL?) -> Void = { source, dest in
|
||||
let successfulSecurityScopedResourceAccess = source.startAccessingSecurityScopedResource()
|
||||
do {
|
||||
switch opType {
|
||||
case .create:
|
||||
@@ -254,7 +281,9 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
source.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
|
||||
completionHandler?(nil)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(nil)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
@@ -262,7 +291,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)
|
||||
}
|
||||
@@ -271,23 +302,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)
|
||||
}
|
||||
@@ -309,16 +344,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)
|
||||
}
|
||||
@@ -350,7 +391,9 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
guard let handle = FileHandle(forReadingAtPath: url.path) else {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileNoSuchFile as FoundationErrorEnum))
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileNoSuchFile as FoundationErrorEnum))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -360,18 +403,24 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
|
||||
let size = LocalFileObject(fileWithURL: url)?.size ?? -1
|
||||
guard size > offset else {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
|
||||
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.fileReadTooLarge as FoundationErrorEnum))
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let data = handle.readData(ofLength: length)
|
||||
|
||||
completionHandler(data, nil)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(data, nil)
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
@@ -451,11 +500,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.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy.isPathRelative = self.isPathRelative
|
||||
copy.undoManager = self.undoManager
|
||||
copy.isCoorinating = self.isCoorinating
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
return copy
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public final class LocalFileObject: FileObject {
|
||||
|
||||
public convenience init? (fileWithPath path: String, relativeTo relativeURL: URL?) {
|
||||
var fileURL: URL?
|
||||
var rpath = path.replacingOccurrences(of: relativeURL?.absoluteString ?? "", with: "", options: .anchored)
|
||||
var rpath = path.replacingOccurrences(of: relativeURL?.path ?? "", with: "", options: .anchored)
|
||||
if path.hasPrefix("/") {
|
||||
rpath.remove(at: rpath.startIndex)
|
||||
}
|
||||
|
||||
@@ -67,7 +67,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 +132,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,14 +8,10 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct FileProviderOneDriveError: Error, CustomStringConvertible {
|
||||
public struct FileProviderOneDriveError: FileProviderHTTPError {
|
||||
public let code: FileProviderHTTPErrorCode
|
||||
public let path: String
|
||||
public let errorDescription: String?
|
||||
|
||||
public var description: String {
|
||||
return code.description
|
||||
}
|
||||
}
|
||||
|
||||
public final class OneDriveFileObject: FileObject {
|
||||
|
||||
@@ -61,6 +61,20 @@ open class RemoteOperationHandle: OperationHandle {
|
||||
}
|
||||
}
|
||||
|
||||
public protocol FileProviderHTTPError: Error, CustomStringConvertible {
|
||||
var code: FileProviderHTTPErrorCode { get }
|
||||
var path: String { get }
|
||||
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?
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -57,7 +57,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 +89,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 +116,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 +156,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 +185,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 +207,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 +245,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 +269,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 +292,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 +326,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 +368,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 +391,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 +420,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)
|
||||
@@ -600,11 +617,9 @@ public final class WebDavFileObject: FileObject {
|
||||
}
|
||||
}
|
||||
|
||||
public struct FileProviderWebDavError: Error, CustomStringConvertible {
|
||||
public struct FileProviderWebDavError: FileProviderHTTPError {
|
||||
public let code: FileProviderHTTPErrorCode
|
||||
public let path: String
|
||||
public let errorDescription: String?
|
||||
public let url: URL
|
||||
|
||||
public var description: String {
|
||||
return code.description
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Reference in New Issue
Block a user