Compare commits

...

13 Commits

Author SHA1 Message Date
Amir Abbas Mousavian 1045901d7c Added NSCoding support
- Better relative path handling in WebDAV
- obsolete deprecated methods
2017-03-25 19:19:49 +04:30
Amir Abbas 528d5eebc3 Fixed relativePath(of:) crash 2017-03-18 15:58:44 +03:30
Amir Abbas 079f8f4b77 Refactored methods to extensions 2017-03-17 15:52:58 +03:30
Amir Abbas e12f386a9d Refactored DispatchTime, better ExposureTime calculation 2017-03-11 03:02:02 +03:30
Amir Abbas 38e217bc19 Better LocalFileObject initialization with empty path 2017-03-09 17:34:00 +03:30
Amir Abbas 0b41abd4ef Optimized PDF thumbnail/meta handling
- Fixed ISO speed and GPS Area image meta
- Fixed Dropbox `name ! BEGINSWiTH %` search query
2017-03-01 13:28:44 +03:30
Amir Abbas aa781adeb2 Fixed searchFiles() from string, fixing “BEGINSWITH” typo 2017-02-24 16:53:59 +03:30
Amir Abbas d61e51ba1c Fixes #29 (WebDAV authentication), minor lints/optimiziations 2017-02-24 16:24:34 +03:30
Amir Abbas cdff7db32e Fixed OneDriveProvider bugs
- fixed and enhanced searching files in Dropbox
2017-02-21 00:48:59 +03:30
Amir Abbas 9533a0e3c9 Removed redundant isPathRelative property. Now is always true.
- Note: Check documentation to workaround
- Improvement: Disabling `LocalFileProviderMonitor` while handler is running
2017-02-20 00:14:55 +03:30
Amir Abbas 194673b3b6 Added NSPredicate to searchFiles method
- public functions became open, now is overridable
- fixed urlCache documentation
2017-02-19 13:34:55 +03:30
Amir Abbas 194c8a41aa Fixed readme 2017-02-18 09:08:55 +03:30
Amir Abbas Mousavian c290377433 Updated Readme, travis.yml 2017-02-16 20:56:24 +03:30
26 changed files with 1149 additions and 832 deletions
+3 -3
View File
@@ -17,7 +17,7 @@ env:
- 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="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:
@@ -50,7 +50,7 @@ script:
# Run `pod lib lint` if specified
- if [ $POD == "YES" ]; then
pod lib lint;
pod lib lint --quick;
fi
after_success:
@@ -77,4 +77,4 @@ deploy:
on:
repo: amosavian/$PROJECTNAME
tags: true
comdition: "$CARTHAGEDEPLOY = YES"
condition: "$CARTHAGEDEPLOY = YES"
+1 -1
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
#
s.name = "FileProvider"
s.version = "0.12.9"
s.version = "0.14.5"
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.
+10 -4
View File
@@ -47,6 +47,9 @@
794C220E1D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */; };
794C220F1D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */; };
794C22101D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */; };
796807551E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */; };
796807561E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */; };
796807571E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */; };
799396AA1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; };
799396AB1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; };
799396AC1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; };
@@ -134,6 +137,7 @@
794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropboxHelper.swift; sourceTree = "<group>"; };
794C22091D5893F800EC49B8 /* SMB2Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2Notification.swift; sourceTree = "<group>"; };
794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2QueryTypes.swift; sourceTree = "<group>"; };
796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileProviderExtensions.swift; sourceTree = "<group>"; };
799396671D48B7F600086753 /* FileProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
799396751D48B80D00086753 /* FileProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
799396821D48B82700086753 /* FileProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -281,6 +285,7 @@
799396991D48C02300086753 /* SMBTypes */,
799396941D48C02300086753 /* FileProvider.h */,
799396951D48C02300086753 /* FileProvider.swift */,
796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */,
79F5745A1DFDB10A00179ABF /* FileObject.swift */,
799396961D48C02300086753 /* LocalFileProvider.swift */,
792572401DF23BDA006A1526 /* LocalHelper.swift */,
@@ -475,6 +480,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
796807551E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */,
799396B31D48C02300086753 /* LocalFileProvider.swift in Sources */,
799396D41D48C02300086753 /* SMB2Tree.swift in Sources */,
7924B1A21D89DAE000589DB7 /* Options.swift in Sources */,
@@ -515,6 +521,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
796807561E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */,
799396B41D48C02300086753 /* LocalFileProvider.swift in Sources */,
799396D51D48C02300086753 /* SMB2Tree.swift in Sources */,
7924B1A31D89DAE000589DB7 /* Options.swift in Sources */,
@@ -555,6 +562,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
796807571E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */,
799396B51D48C02300086753 /* LocalFileProvider.swift in Sources */,
799396D61D48C02300086753 /* SMB2Tree.swift in Sources */,
7924B1A41D89DAE000589DB7 /* Options.swift in Sources */,
@@ -597,7 +605,7 @@
799396601D48B7BF00086753 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.12.9;
BUNDLE_VERSION_STRING = 0.14.5;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -627,7 +635,7 @@
799396611D48B7BF00086753 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.12.9;
BUNDLE_VERSION_STRING = 0.14.5;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -657,7 +665,6 @@
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
APPLICATION_EXTENSION_API_ONLY = YES;
BUNDLE_VERSION_STRING = 0.8.2;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
@@ -710,7 +717,6 @@
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
APPLICATION_EXTENSION_API_ONLY = YES;
BUNDLE_VERSION_STRING = 0.8.2;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0810"
LastUpgradeVersion = "0820"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0810"
LastUpgradeVersion = "0820"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
+24 -14
View File
@@ -22,13 +22,13 @@
This library provides implementaion of WebDav, Dropbox, OneDrive and SMB2 (incomplete) and local files.
All functions are async calls and it wont block your main thread.
All functions do async calls and it wont block your main thread.
## Features
- [x] **LocalFileProvider** a wrapper around `FileManager` with some additions like searching and reading a portion of file.
- [x] **CloudFileProvider** A wrapper around app's ubiquitous container to iCloud Drive in iOS 8+ API.
- [x] **WebDAVFileProvider** WebDAV protocol is defacto file transmission standard, replaced FTP.
- [x] **LocalFileProvider** a wrapper around `FileManager` with some additions like builtin coordinating, searching and reading a portion of file.
- [x] **CloudFileProvider** A wrapper around app's ubiquitous container API of iCloud Drive.
- [x] **WebDAVFileProvider** WebDAV protocol is defacto file transmission standard, used by many cloud services like Yandex.
- [x] **DropboxFileProvider** A wrapper around Dropbox Web API.
* For now it has limitation in uploading files up to 150MB.
- [x] **OneDriveFileProvider** A wrapper around OneDrive REST API, works with `onedrive.com` and compatible (business) servers.
@@ -42,7 +42,7 @@ All functions are async calls and it wont block your main thread.
## Requirements
- **Swift 3**
- **Swift 3.0 +**
- iOS 8.0 , OSX 10.10
- XCode 8.0
@@ -93,7 +93,7 @@ Then you can do either:
* Copy Source folder to your project and Voila!
* Drop FileProvider.xcodeproj to you Xcode workspace and add the framework to your Embeded Binaries in target.
* Drop `FileProvider.xcodeproj` to you Xcode workspace and add the framework to your Embeded Binaries in target.
## Usage
@@ -107,7 +107,7 @@ For LocalFileProvider if you want to deal with `Documents` folder
let documentsProvider = LocalFileProvider()
// Equals with:
let documentsProvider = LocalFileProvider(directory: .documentDirectory, domainMask: = .userDomainMask)
let documentsProvider = LocalFileProvider(for: .documentDirectory, in: .userDomainMask)
// Equals with:
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
@@ -118,7 +118,7 @@ Also for using group shared container:
```swift
let documentsProvider = LocalFileProvider(sharedContainerId: "group.yourcompany.appContainer")
// Replace your group identifier with string above
// Replace your group identifier
```
You can't change the base url later. and all paths are related to this base url by default.
@@ -149,7 +149,7 @@ You can use `url(of:)` method if provider to get direct access url (local or rem
For updating User interface please consider using delegate method instead of completion handlers. Delegate methods are guaranteed to run in main thread to avoid bugs.
It's simply three method which indicated whether the operation failed, succeed and how much of operation has been done (suitable for uploading and downloading operations).
There's simply three method which indicated whether the operation failed, succeed and how much of operation has been done (suitable for uploading and downloading operations).
Your class should conforms `FileProviderDelegate` class:
@@ -158,7 +158,7 @@ override func viewDidLoad() {
documentsProvider.delegate = self as FileProviderDelegate
}
func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperation) {
func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperationType) {
switch operation {
case .copy(source: let source, destination: let dest):
print("\(source) copied to \(dest).")
@@ -169,7 +169,7 @@ func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: File
}
}
func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperation) {
func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperationType) {
switch operation {
case .copy(source: let source, destination: let dest):
print("copy of \(source) failed.")
@@ -180,7 +180,7 @@ func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileO
}
}
func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperation, progress: Float) {
func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperationType, progress: Float) {
switch operation {
case .copy(source: let source, destination: let dest):
print("Copy\(source) to \(dest): \(progress * 100) completed.")
@@ -202,7 +202,7 @@ You can also implement `FileOperationDelegate` protocol to control behaviour of
`fileProvider(shouldProceedAfterError:, operation:)` will be called if an error occured during file operations. Return `true` if you want to continue operation on next files or `false` if you want stop operation further. Default value is false if you don't implement delegate.
**Note: these methods will be called for files in a directory and its subfolders recursively.**
**Note: In `LocalFileProvider`, these methods will be called for files in a directory and its subfolders recursively.**
### Directory contents and file attributes
@@ -304,7 +304,7 @@ documentsProvider.removeItem(path: "new.txt", completionHandler: nil)
### Fetching Contents of File
There is two method for this purpose, one of them loads entire file into NSData and another can load a portion of file.
There is two method for this purpose, one of them loads entire file into `Data` and another can load a portion of file.
```swift
documentsProvider.contents(path: "old.txt", completionHandler: {
@@ -382,6 +382,10 @@ Creating/Copying/Deleting functions return a `OperationHandle` for remote operat
It's not supported by native `(NS)FileManager` so `LocalFileProvider`, but this functionality will be added to future `PosixFileProvider` class.
### File Coordination
`LocalFileProvider` and its descendents has a `isCoordinating` property. By setting this, provider will use `NSFileCoordinating` class to do all file operations. It's mandatory for iCloud, while recommended when using shared container or anywhere that simultaneous operations on a file/folder is common.
### 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.
@@ -439,6 +443,12 @@ if documentsProvider..propertiesOfFile(path: file.path, completionHandler: { (pr
We would love for you to contribute to **FileProvider**, check the `LICENSE` file for more info.
Things to do:
- [ ] Implement Test-case (XCTest)
- [ ] Add Sample project for iOS
- [ ] Add Sample project for macOS
## Projects in use
* [EDM - Browse and Receive Files](https://itunes.apple.com/us/app/edm-browse-and-receive-files/id948397575?ls=1&mt=8)
+171 -109
View File
@@ -9,7 +9,7 @@
import Foundation
/**
Allows accessing to iCloud Drive stored files. Determine scope when initializing,to either access
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
@@ -81,13 +81,40 @@ open class CloudFileProvider: LocalFileProvider {
try? fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true)
}
public required convenience init?(coder aDecoder: NSCoder) {
guard let containerId = aDecoder.decodeObject(forKey: "containerId") as? String,
let scopeString = aDecoder.decodeObject(forKey: "scope") as? String,
let scope = UbiquitousScope(rawValue: scopeString) else {
return nil
}
self.init(containerId: containerId, scope: scope)
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
self.isCoorinating = aDecoder.decodeBool(forKey: "isCoorinating")
}
open override func encode(with aCoder: NSCoder) {
aCoder.encode(self.containerId, forKey: "containerId")
aCoder.encode(self.scope.rawValue, forKey: "scope")
aCoder.encode(self.currentPath, forKey: "currentPath")
aCoder.encode(self.isCoorinating, forKey: "isCoorinating")
}
open override func copy(with zone: NSZone? = nil) -> Any {
let copy = CloudFileProvider(containerId: self.containerId, scope: self.scope)
copy?.currentPath = self.currentPath
copy?.delegate = self.delegate
copy?.fileOperationDelegate = self.fileOperationDelegate
return copy as Any
}
/**
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.
- Parameter completionHandler: a closure with result of directory entries or error.
`contents`: An array of `FileObject` identifying the the directory entries.
`error`: Error returned by system.
*/
@@ -156,7 +183,7 @@ open class CloudFileProvider: LocalFileProvider {
If the directory contains no entries or an error is occured, this method will return the empty `FileObject`.
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
- Parameter completionHandler: a block with result of directory entries or error.
- Parameter completionHandler: a closure with result of directory entries or error.
`attributes`: A `FileObject` containing the attributes of the item.
`error`: Error returned by system.
*/
@@ -205,6 +232,130 @@ open class CloudFileProvider: LocalFileProvider {
}
}
/**
Search files inside directory using query asynchronously.
- Note: For now only it's limited to file names. `query` parameter may take `NSPredicate` format in near future.
- Parameters:
- path: location of directory to start search
- recursive: Searching subdirectories of path
- query: Simple string of file name to be search (for now).
- foundItemHandler: Closure which is called when a file is found
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
*/
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
let mapDict: [String: String] = ["url": NSMetadataItemURLKey, "name": NSMetadataItemFSNameKey, "path": NSMetadataItemPathKey, "filesize": NSMetadataItemFSSizeKey, "modifiedDate": NSMetadataItemFSContentChangeDateKey, "creationDate": NSMetadataItemFSCreationDateKey, "contentType": NSMetadataItemContentTypeKey]
func updateQueryKeys(_ queryComponent: NSPredicate) -> NSPredicate {
if let cQuery = queryComponent as? NSCompoundPredicate {
let newSub = cQuery.subpredicates.map { updateQueryKeys($0 as! NSPredicate) }
switch cQuery.compoundPredicateType {
case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub)
case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub.first!)
case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub)
}
} else if let cQuery = queryComponent as? NSComparisonPredicate {
var newLeft = cQuery.leftExpression
var newRight = cQuery.rightExpression
if newLeft.expressionType == .keyPath, let newKey = mapDict[newLeft.keyPath] {
newLeft = NSExpression(forKeyPath: newKey)
}
if newRight.expressionType == .keyPath, let newKey = mapDict[newRight.keyPath] {
newRight = NSExpression(forKeyPath: newKey)
}
if newLeft.expressionType == .keyPath, newLeft.keyPath == "type" {
newRight = NSExpression(forConstantValue: newRight.constantValue as? String == "directory" ? "public.directory": "public.data")
}
if newRight.expressionType == .keyPath, newRight.keyPath == "type" {
newLeft = NSExpression(forConstantValue: newLeft.constantValue as? String == "directory" ? "public.directory": "public.data")
}
return NSComparisonPredicate(leftExpression: newLeft, rightExpression: newRight, modifier: cQuery.comparisonPredicateModifier, type: cQuery.predicateOperatorType, options: cQuery.options)
} else {
return queryComponent
}
}
dispatch_queue.async {
let pathURL = self.url(of: path)
let mdquery = NSMetadataQuery()
mdquery.predicate = NSPredicate(format: "(%K BEGINSWITH %@) && (\(updateQueryKeys(query).predicateFormat))", NSMetadataItemPathKey, pathURL.path)
mdquery.searchScopes = [self.scope.rawValue]
var lastReportedCount = 0
if let foundItemHandler = foundItemHandler {
var updateObserver: NSObjectProtocol?
// FIXME: Remove this section as it won't work as expected on iCloud
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryGatheringProgress, object: mdquery, queue: nil, using: { (notification) in
mdquery.disableUpdates()
guard mdquery.resultCount > lastReportedCount else { return }
for index in lastReportedCount..<mdquery.resultCount {
guard let attribs = (mdquery.result(at: index) as? NSMetadataItem)?.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
continue
}
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
continue
}
if let file = self.mapFileObject(attributes: attribs) {
foundItemHandler(file)
}
}
lastReportedCount = mdquery.resultCount
mdquery.enableUpdates()
})
}
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: mdquery, queue: nil, using: { (notification) in
defer {
mdquery.stop()
NotificationCenter.default.removeObserver(finishObserver!)
}
guard let results = mdquery.results as? [NSMetadataItem] else {
return
}
mdquery.disableUpdates()
var contents = [FileObject]()
for result in results {
guard let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
continue
}
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
continue
}
if let file = self.mapFileObject(attributes: attribs) {
contents.append(file)
}
}
self.dispatch_queue.async {
completionHandler(contents, nil)
}
})
DispatchQueue.main.async {
if !mdquery.start() {
self.dispatch_queue.async {
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
}
}
}
}
}
open override func isReachable(completionHandler: @escaping (Bool) -> Void) {
dispatch_queue.async {
completionHandler(self.fileManager.ubiquityIdentityToken != nil)
@@ -378,7 +529,7 @@ open class CloudFileProvider: LocalFileProvider {
- Parameters:
- path: Path of file.
- completionHandler: a block with result of file contents or error.
- completionHandler: a closure with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress.
@@ -397,7 +548,7 @@ open class CloudFileProvider: LocalFileProvider {
- path: Path of file.
- offset: First byte index which should be read. **Starts from 0.**
- length: Bytes count of data. Pass `-1` to read until the end of file.
- completionHandler: a block with result of file contents or error.
- completionHandler: a closure with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress.
@@ -425,98 +576,6 @@ open class CloudFileProvider: LocalFileProvider {
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)
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "(%K BEGINSWITH %@) && (%K LIKE %@)", NSMetadataItemPathKey, pathURL.path, NSMetadataItemFSNameKey, query)
query.searchScopes = [self.scope.rawValue]
var lastReportedCount = 0
if let foundItemHandler = foundItemHandler {
var updateObserver: NSObjectProtocol?
// FIXME: Remove this section as it won't work as expected on iCloud
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryGatheringProgress, object: query, queue: nil, using: { (notification) in
query.disableUpdates()
guard query.resultCount > lastReportedCount else { return }
for index in lastReportedCount..<query.resultCount {
guard let attribs = (query.result(at: index) as? NSMetadataItem)?.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
continue
}
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
continue
}
if let file = self.mapFileObject(attributes: attribs) {
foundItemHandler(file)
}
}
lastReportedCount = query.resultCount
query.enableUpdates()
})
}
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
defer {
query.stop()
NotificationCenter.default.removeObserver(finishObserver!)
}
guard let results = query.results as? [NSMetadataItem] else {
return
}
query.disableUpdates()
var contents = [FileObject]()
for result in results {
guard let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
continue
}
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
continue
}
if let file = self.mapFileObject(attributes: attribs) {
contents.append(file)
}
}
self.dispatch_queue.async {
completionHandler(contents, nil)
}
})
DispatchQueue.main.async {
if !query.start() {
self.dispatch_queue.async {
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
}
}
}
}
}
fileprivate var monitors = [String: (NSMetadataQuery, NSObjectProtocol)]()
/**
@@ -532,7 +591,7 @@ open class CloudFileProvider: LocalFileProvider {
- Parameters:
- path: path of directory.
- eventHandler: Block executed after change, on a secondary thread.
- eventHandler: Closure executed after change, on a secondary thread.
*/
open override func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
self.unregisterNotifcation(path: path)
@@ -579,15 +638,6 @@ open class CloudFileProvider: LocalFileProvider {
return monitors[path] != nil
}
open override func copy(with zone: NSZone? = nil) -> Any {
let copy = CloudFileProvider(containerId: self.containerId)
copy?.currentPath = self.currentPath
copy?.delegate = self.delegate
copy?.fileOperationDelegate = self.fileOperationDelegate
copy?.isPathRelative = self.isPathRelative
return copy as Any
}
fileprivate func mapFileObject(attributes attribs: [String: Any]) -> FileObject? {
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardizedFileURL, let name = attribs[NSMetadataItemFSNameKey] as? String else {
return nil
@@ -622,7 +672,19 @@ open class CloudFileProvider: LocalFileProvider {
}
}
/// Returns a pulic url with expiration date, can be shared with other people.
/**
Genrates a public url to a file to be shared with other users and can be downloaded without authentication.
- Important: URL will be available for a limitied time, determined in `expiration` argument.
- Parameters:
- to: path of file, including file/directory name.
- completionHandler: a closure with result of directory entries or error.
`link`: a url returned by Dropbox to share.
`attribute`: a `FileObject` containing the attributes of the item.
`expiration`: a `Date` object, determines when the public url will expires.
`error`: Error returned by Dropbox.
*/
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
operation_queue.addOperation {
do {
@@ -754,7 +816,7 @@ open class CloudOperationHandle: OperationHandle {
DispatchQueue.main.async {
query.start()
}
_ = group.wait(timeout: DispatchTime.now() + 30)
_ = group.wait(timeout: .now() + 30)
return item
}
}
+102 -60
View File
@@ -18,7 +18,6 @@ import CoreGraphics
*/
open class DropboxFileProvider: FileProviderBasicRemote {
open class var type: String { return "DropBox" }
open let isPathRelative: Bool
open let baseURL: URL?
open var currentPath: String
@@ -53,6 +52,16 @@ open class DropboxFileProvider: FileProviderBasicRemote {
return _session!
}
fileprivate var _longpollSession: URLSession?
internal var longpollSession: URLSession {
if _longpollSession == nil {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 600
_longpollSession = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
}
return _longpollSession!
}
/**
Initializer for Dropbox provider with given client ID and Token.
These parameters must be retrieved via [OAuth2 API of Dropbox](https://www.dropbox.com/developers/reference/oauth-guide).
@@ -61,11 +70,10 @@ open class DropboxFileProvider: FileProviderBasicRemote {
The latter is easier to use and prefered. Also you can use [auth0/Lock](https://github.com/auth0/Lock.iOS-OSX) which provides graphical user interface.
- Parameter credential: a `URLCredential` object with Client ID set as `user` and Token set as `password`.
- Parameter cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used.
- Parameter cache: A URLCache to cache downloaded files and contents.
*/
public init(credential: URLCredential?, cache: URLCache? = nil) {
self.baseURL = nil
self.isPathRelative = true
self.currentPath = ""
self.useCache = false
self.validatingCache = true
@@ -78,7 +86,34 @@ open class DropboxFileProvider: FileProviderBasicRemote {
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
operation_queue = OperationQueue()
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
}
public required convenience init?(coder aDecoder: NSCoder) {
self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
self.useCache = aDecoder.decodeBool(forKey: "useCache")
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
}
public func encode(with aCoder: NSCoder) {
aCoder.encode(self.credential, forKey: "credential")
aCoder.encode(self.currentPath, forKey: "currentPath")
aCoder.encode(self.useCache, forKey: "useCache")
aCoder.encode(self.validatingCache, forKey: "validatingCache")
}
public static var supportsSecureCoding: Bool {
return true
}
open func copy(with zone: NSZone? = nil) -> Any {
let copy = DropboxFileProvider(credential: self.credential, cache: self.cache)
copy.currentPath = self.currentPath
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
copy.useCache = self.useCache
copy.validatingCache = self.validatingCache
return copy
}
deinit {
@@ -102,14 +137,14 @@ open class DropboxFileProvider: FileProviderBasicRemote {
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderDropboxError?
var fileObject: DropboxFileObject?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let file = DropboxFileObject(json: json) {
if let json = data?.deserializeJSON(), let file = DropboxFileObject(json: json) {
fileObject = file
}
}
@@ -126,7 +161,7 @@ open class DropboxFileProvider: FileProviderBasicRemote {
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var totalSize: Int64 = -1
var usedSize: Int64 = 0
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
if let json = data?.deserializeJSON() {
totalSize = ((json["allocation"] as? NSDictionary)?["allocated"] as? NSNumber)?.int64Value ?? -1
usedSize = (json["used"] as? NSNumber)?.int64Value ?? 0
}
@@ -135,6 +170,33 @@ open class DropboxFileProvider: FileProviderBasicRemote {
task.resume()
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
var foundFiles = [DropboxFileObject]()
if let queryStr = query.findValue(forKey: "name", operator: .beginsWith) as? String {
// Dropbox only support searching for file names begin with query in non-enterprise accounts.
// We will use it if there is a `name BEGINSWITH[c] "query"` in predicate, then filter to form final result.
search(path, query: queryStr, foundItem: { (file) in
if query.evaluate(with: file.mapPredicate()) {
foundFiles.append(file)
foundItemHandler?(file)
}
}, completionHandler: { (error) in
completionHandler(foundFiles, error)
})
} else {
// Dropbox doesn't support searching attributes natively. The workaround is to fallback to listing all files
// and filter it locally. It may have a network burden in case there is many files in Dropbox, so please use it concisely.
list(path, recursive: true, progressHandler: { (files, _, error) in
for file in files where query.evaluate(with: file.mapPredicate()) {
foundItemHandler?(file)
}
}, completionHandler: { (files, _, error) in
let predicatedFiles = files.filter { query.evaluate(with: $0.mapPredicate()) }
completionHandler(predicatedFiles, error)
})
}
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
self.storageProperties { total, _ in
completionHandler(total > 0)
@@ -145,25 +207,25 @@ open class DropboxFileProvider: FileProviderBasicRemote {
}
extension DropboxFileProvider: FileProviderOperations {
public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
return doOperation(.create(path: path), completionHandler: completionHandler)
}
public func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let filePath = (path as NSString).appendingPathComponent(fileName)
return self.writeContents(path: filePath, contents: data ?? Data(), completionHandler: completionHandler)
}
public func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
}
public func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
}
public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.remove(path: path), completionHandler: completionHandler)
}
@@ -197,7 +259,7 @@ extension DropboxFileProvider: FileProviderOperations {
} else {
requestDictionary["path"] = correctPath(sourcePath) as NSString?
}
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderDropboxError?
if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
@@ -211,7 +273,7 @@ extension DropboxFileProvider: FileProviderOperations {
return RemoteOperationHandle(operationType: operation, tasks: [task])
}
public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -219,7 +281,7 @@ extension DropboxFileProvider: FileProviderOperations {
return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
}
public func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -229,7 +291,7 @@ extension DropboxFileProvider: FileProviderOperations {
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
let requestDictionary: [String: AnyObject] = ["path": path as NSString]
let requestJson = dictionaryToJSON(requestDictionary) ?? ""
let requestJson = String(jsonDictionary: requestDictionary) ?? ""
request.setValue(requestJson, forHTTPHeaderField: "Dropbox-API-Arg")
let task = session.downloadTask(with: request, completionHandler: { (cacheURL, response, error) in
guard let cacheURL = cacheURL, let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
@@ -253,7 +315,7 @@ extension DropboxFileProvider: FileProviderOperations {
}
extension DropboxFileProvider: FileProviderReadWrite {
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
@@ -272,7 +334,7 @@ extension DropboxFileProvider: FileProviderReadWrite {
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
}
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
request.setValue(String(jsonDictionary: requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderDropboxError?
if let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: httpResponse.statusCode) {
@@ -295,16 +357,6 @@ extension DropboxFileProvider: FileProviderReadWrite {
return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
}
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
var foundFiles = [DropboxFileObject]()
search(path, query: query, foundItem: { (file) in
foundFiles.append(file)
foundItemHandler?(file)
}, completionHandler: { (error) in
completionHandler(foundFiles, error)
})
}
/*
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
/* There is two ways to monitor folders changing in Dropbox. Either using webooks
@@ -323,16 +375,16 @@ extension DropboxFileProvider: FileProviderReadWrite {
}
extension DropboxFileProvider {
/// *DEPRECATED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead.
@available(*, deprecated, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
/// *OBSOLETED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead.
@available(*, obsoleted: 1.0, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
self.publicLink(to: path) { (url, file, _, error) in
completionHandler(url, file, error)
}
}
/// *DEPRECATED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead.
@available(*, deprecated, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
/// *OBSOLETED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead.
@available(*, obsoleted: 1.0, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
self.publicLink(to: path) { (url, file, expiration, error) in
completionHandler(url, file, expiration, error)
@@ -346,7 +398,7 @@ extension DropboxFileProvider {
- Parameters:
- to: path of file, including file/directory name.
- completionHandler: a block with result of directory entries or error.
- completionHandler: a closure with result of directory entries or error.
`link`: a url returned by Dropbox to share.
`attribute`: a `FileObject` containing the attributes of the item.
`expiration`: a `Date` object, determines when the public url will expires.
@@ -359,7 +411,7 @@ extension DropboxFileProvider {
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderDropboxError?
var link: URL?
@@ -367,7 +419,7 @@ extension DropboxFileProvider {
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
if let json = data?.deserializeJSON() {
if let linkStr = json["link"] as? String {
link = URL(string: linkStr)
}
@@ -389,7 +441,7 @@ extension DropboxFileProvider {
- Parameters:
- remoteURL: a valid remote url to file.
- to: Destination path of file, including file/directory name.
- completionHandler: a block with result of directory entries or error.
- completionHandler: a closure with result of directory entries or error.
`jobId`: Job ID returned by Dropbox to monitor the copy/download progress.
`attribute`: A `FileObject` containing the attributes of the item.
`error`: Error returned by Dropbox.
@@ -405,7 +457,7 @@ extension DropboxFileProvider {
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "url" : remoteURL.absoluteString as NSString]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderDropboxError?
var jobId: String?
@@ -413,7 +465,7 @@ extension DropboxFileProvider {
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderDropboxError(code: code!, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
if let json = data?.deserializeJSON() {
jobId = json["async_job_id"] as? String
if let attribDic = json["metadata"] as? [String: AnyObject] {
fileObject = DropboxFileObject(json: attribDic)
@@ -440,7 +492,7 @@ extension DropboxFileProvider {
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "copy_reference" : reference as NSString]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderDropboxError?
if let response = response as? HTTPURLResponse {
@@ -454,7 +506,7 @@ extension DropboxFileProvider {
}
extension DropboxFileProvider: ExtendedFileProvider {
public func thumbnailOfFileSupported(path: String) -> Bool {
open func thumbnailOfFileSupported(path: String) -> Bool {
switch (path as NSString).pathExtension.lowercased() {
case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff":
return true
@@ -469,7 +521,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
}
}
public func propertiesOfFileSupported(path: String) -> Bool {
open func propertiesOfFileSupported(path: String) -> Bool {
let fileExt = (path as NSString).pathExtension.lowercased()
switch fileExt {
case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff":
@@ -484,7 +536,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
}
/// Default value for dimension is 64x64, according to Dropbox documentation
public func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
let url: URL
switch (path as NSString).pathExtension.lowercased() {
case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff":
@@ -505,17 +557,17 @@ extension DropboxFileProvider: ExtendedFileProvider {
if let dimension = dimension {
requestDictionary["size"] = "w\(Int(dimension.width))h\(Int(dimension.height))" as NSString
}
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
request.setValue(String(jsonDictionary: requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in
var image: ImageClass? = nil
if let r = response as? HTTPURLResponse, let result = r.allHeaderFields["Dropbox-API-Result"] as? String, let jsonResult = jsonToDictionary(result) {
if let r = response as? HTTPURLResponse, let result = r.allHeaderFields["Dropbox-API-Result"] as? String, let jsonResult = result.deserializeJSON() {
if jsonResult["error"] != nil {
completionHandler(nil, self.throwError(path, code: URLError.cannotDecodeRawData as FoundationErrorEnum))
}
}
if let data = data {
if DropboxFileProvider.dataIsPDF(data) {
image = DropboxFileProvider.convertToImage(pdfData: data)
if data.isPDF, let pageImage = DropboxFileProvider.convertToImage(pdfData: data) {
image = pageImage
} else if let contentType = (response as? HTTPURLResponse)?.allHeaderFields["Content-Type"] as? String, contentType.contains("text/html") {
// TODO: Implement converting html returned type of get_preview to image
} else {
@@ -527,14 +579,14 @@ extension DropboxFileProvider: ExtendedFileProvider {
task.resume()
}
public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
let url = URL(string: "files/get_metadata", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderDropboxError?
var dic = [String: Any]()
@@ -542,7 +594,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let properties = json["media_info"] as? [String: Any] {
if let json = data?.deserializeJSON(), let properties = json["media_info"] as? [String: Any] {
(dic, keys) = self.mapMediaInfo(properties)
}
}
@@ -552,14 +604,4 @@ extension DropboxFileProvider: ExtendedFileProvider {
}
}
extension DropboxFileProvider: FileProvider {
open func copy(with zone: NSZone? = nil) -> Any {
let copy = DropboxFileProvider(credential: self.credential, cache: self.cache)
copy.currentPath = self.currentPath
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
copy.useCache = self.useCache
copy.validatingCache = self.validatingCache
return copy
}
}
extension DropboxFileProvider: FileProvider { }
+22 -21
View File
@@ -22,7 +22,7 @@ public final class DropboxFileObject: FileObject {
}
internal convenience init? (jsonStr: String) {
guard let json = jsonToDictionary(jsonStr) else { return nil }
guard let json = jsonStr.deserializeJSON() else { return nil }
self.init(json: json)
}
@@ -75,7 +75,7 @@ public final class DropboxFileObject: FileObject {
// codebeat:disable[ARITY]
internal extension DropboxFileProvider {
func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, session: URLSession? = nil, progressHandler: ((_ contents: [FileObject], _ nextCursor: String?, _ error: Error?) -> Void)? = nil, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
var requestDictionary = [String: AnyObject]()
let url: URL
if let cursor = cursor {
@@ -90,30 +90,32 @@ internal extension DropboxFileProvider {
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = (session ?? self.session).dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderDropboxError?
var files = prevContents
var files = [DropboxFileObject]()
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderDropboxError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
if let data = data, let jsonStr = String(data: data, encoding: .utf8) {
let json = jsonToDictionary(jsonStr)
if let entries = json?["entries"] as? [AnyObject] , entries.count > 0 {
if let json = data?.deserializeJSON() {
if let entries = json["entries"] as? [AnyObject] , entries.count > 0 {
files.reserveCapacity(entries.count)
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
files.append(file)
}
}
let ncursor = json?["cursor"] as? String
let hasmore = (json?["has_more"] as? NSNumber)?.boolValue ?? false
let ncursor = json["cursor"] as? String
let hasmore = (json["has_more"] as? NSNumber)?.boolValue ?? false
if hasmore {
self.list(path, cursor: ncursor, prevContents: files, completionHandler: completionHandler)
progressHandler?(files, ncursor, responseError ?? error)
self.list(path, cursor: ncursor, prevContents: prevContents + files, completionHandler: completionHandler)
return
}
}
}
completionHandler(files, nil, responseError ?? error)
progressHandler?(files, nil, responseError ?? error)
completionHandler(prevContents + files, nil, responseError ?? error)
})
task.taskDescription = FileOperationType.fetch(path: path).json
task.resume()
@@ -136,7 +138,7 @@ internal extension DropboxFileProvider {
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
request.setValue(String(jsonDictionary: requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
request.httpBody = data
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
var responseError: FileProviderDropboxError?
@@ -169,7 +171,7 @@ internal extension DropboxFileProvider {
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
request.setValue(String(jsonDictionary: requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
var responseError: FileProviderDropboxError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
@@ -193,22 +195,21 @@ internal extension DropboxFileProvider {
requestDictionary["query"] = query as NSString
requestDictionary["start"] = start as NSNumber
requestDictionary["max_results"] = maxResultPerPage as NSNumber
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderDropboxError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderDropboxError(code: rCode, path: startPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
if let data = data, let jsonStr = String(data: data, encoding: .utf8) {
let json = jsonToDictionary(jsonStr)
if let entries = json?["matches"] as? [AnyObject] , entries.count > 0 {
if let json = data?.deserializeJSON() {
if let entries = json["matches"] as? [AnyObject] , entries.count > 0 {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
foundItem(file)
}
}
let rstart = json?["start"] as? Int
let hasmore = (json?["more"] as? NSNumber)?.boolValue ?? false
let rstart = json["start"] as? Int
let hasmore = (json["more"] as? NSNumber)?.boolValue ?? false
if hasmore, let rstart = rstart {
self.search(startPath, query: query, start: rstart + entries.count, maxResultPerPage: maxResultPerPage, foundItem: foundItem, completionHandler: completionHandler)
} else {
@@ -251,7 +252,7 @@ internal extension DropboxFileProvider {
}
if let duration = json["duration"] as? UInt64 {
keys.append("Duration")
dic["Duration"] = DropboxFileProvider.formatshort(interval: TimeInterval(duration))
dic["Duration"] = TimeInterval(duration).formatshort
}
return (dic, keys)
}
+25 -45
View File
@@ -10,11 +10,6 @@ import Foundation
import ImageIO
import CoreGraphics
import AVFoundation
#if os(iOS) || os(tvOS)
import UIKit
#elseif os(macOS)
import Cocoa
#endif
extension LocalFileProvider: ExtendedFileProvider {
public func thumbnailOfFileSupported(path: String) -> Bool {
@@ -230,8 +225,7 @@ public struct LocalFileInformationGenerator {
/// 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)
return LocalFileProvider.convertToImage(pdfURL: fileURL)
}
/// Thumbnail generator closure for office document files.
@@ -252,7 +246,7 @@ public struct LocalFileInformationGenerator {
var keys = [String]()
func add(key: String, value: Any?) {
if let value = value {
if let value = value, !((value as? String)?.isEmpty ?? false) {
keys.append(key)
dic[key] = value
}
@@ -287,28 +281,25 @@ public struct LocalFileInformationGenerator {
add(key: "Device model", value: tiffDict[kCGImagePropertyTIFFModel as String])
add(key: "Lens model", value: exifDict[kCGImagePropertyExifLensModel as String])
add(key: "Artist", value: tiffDict[kCGImagePropertyTIFFArtist as String] as? String)
if let cr = tiffDict[kCGImagePropertyTIFFCopyright as String] as? String , !cr.isEmpty {
add(key: "Copyright", value: cr)
}
if let date = tiffDict[kCGImagePropertyTIFFDateTime as String] as? String , !date.isEmpty {
add(key: "Date taken", value: date)
}
add(key: "Copyright", value: tiffDict[kCGImagePropertyTIFFCopyright as String] as? String)
add(key: "Date taken", value: tiffDict[kCGImagePropertyTIFFDateTime as String] as? String)
if let latitude = tiffDict[kCGImagePropertyGPSLatitude as String] as? NSNumber, let longitude = tiffDict[kCGImagePropertyGPSLongitude as String] as? NSNumber {
add(key: "Location", value: "\(latitude), \(longitude)")
}
add(key: "Altitude", value: tiffDict[kCGImagePropertyGPSAltitude as String] as? NSNumber)
add(key: "Area", value: tiffDict[kCGImagePropertyGPSAreaInformation as String])
add(key: "Color space", value: imageDict[kCGImagePropertyColorModel as String])
add(key: "Focal length", value: exifDict[kCGImagePropertyExifFocalLength as String])
add(key: "F number", value: exifDict[kCGImagePropertyExifFNumber as String])
add(key: "Exposure program", value: exifDict[kCGImagePropertyExifExposureProgram as String])
if let exp = exifDict[kCGImagePropertyExifExposureTime as String] as? NSNumber {
let expfrac = simplify(Int64(exp.doubleValue * 10_000_000_000_000), 10_000_000_000_000)
let expfrac = simplify(Int64(exp.doubleValue * 1_163_962_800_000), 1_163_962_800_000)
add(key: "Exposure time", value: "\(expfrac.newTop)/\(expfrac.newBottom)")
}
if let iso = exifDict[kCGImagePropertyExifISOSpeedRatings as String] as? NSArray , iso.count > 0 {
add(key: "ISO speed", value: iso[0])
}
add(key: "ISO speed", value: (exifDict[kCGImagePropertyExifISOSpeedRatings as String] as? [NSNumber])?.first)
return (dic, keys)
}
@@ -347,7 +338,7 @@ public struct LocalFileInformationGenerator {
}
}
if let ap = try? AVAudioPlayer(contentsOf: fileURL) {
add(key: "Duration", value: LocalFileProvider.formatshort(interval: ap.duration))
add(key: "Duration", value: ap.duration.formatshort)
add(key: "Bitrate", value: ap.settings[AVSampleRateKey] as? Int)
}
}
@@ -376,17 +367,17 @@ public struct LocalFileInformationGenerator {
}
let asset = AVURLAsset(url: fileURL, options: nil)
let videoTracks = asset.tracks(withMediaType: AVMediaTypeVideo)
if videoTracks.count > 0 {
if let videoTrack = videoTracks.first {
var bitrate: Float = 0
let width = Int(videoTracks[0].naturalSize.width)
let height = Int(videoTracks[0].naturalSize.height)
let width = Int(videoTrack.naturalSize.width)
let height = Int(videoTrack.naturalSize.height)
add(key: "Dimensions", value: "\(width)x\(height)")
var duration: Int64 = 0
for track in videoTracks {
duration += track.timeRange.duration.timescale > 0 ? track.timeRange.duration.value / Int64(track.timeRange.duration.timescale) : 0
bitrate += track.estimatedDataRate
}
add(key: "Duration", value: LocalFileProvider.formatshort(interval: TimeInterval(duration)))
add(key: "Duration", value: TimeInterval(duration).formatshort)
add(key: "Video Bitrate", value: "\(Int(ceil(bitrate / 1000))) kbps")
}
let audioTracks = asset.tracks(withMediaType: AVMediaTypeAudio)
@@ -405,7 +396,7 @@ public struct LocalFileInformationGenerator {
var keys = [String]()
func add(key: String, value: Any?) {
if let value = value {
if let value = value, !((value as? String)?.isEmpty ?? false) {
keys.append(key)
dic[key] = value
}
@@ -419,7 +410,8 @@ public struct LocalFileInformationGenerator {
return nil
}
func convertDate(_ date: String) -> Date? {
func convertDate(_ date: String?) -> Date? {
guard let date = date else { return nil }
var dateStr = date
if dateStr.hasPrefix("D:") {
dateStr = date.substring(from: date.characters.index(date.startIndex, offsetBy: 2))
@@ -436,16 +428,10 @@ public struct LocalFileInformationGenerator {
return nil
}
if let data = try? Data(contentsOf: fileURL), let provider = CGDataProvider(data: data as CFData), let reference = CGPDFDocument(provider), let dict = reference.info {
if let title = getKey("Title", from: dict), !title.isEmpty {
add(key: "Title", value: title)
}
if let author = getKey("Author", from: dict), !author.isEmpty {
add(key: "Author", value: author)
}
if let subject = getKey("Subject", from: dict), !subject.isEmpty {
add(key: "Subject", value: subject)
}
if let provider = CGDataProvider(url: fileURL as CFURL), let reference = CGPDFDocument(provider), let dict = reference.info {
add(key: "Title", value: getKey("Title", from: dict))
add(key: "Author", value: getKey("Author", from: dict))
add(key: "Subject", value: getKey("Subject", from: dict))
var majorVersion: Int32 = 0
var minorVersion: Int32 = 0
reference.getVersion(majorVersion: &majorVersion, minorVersion: &minorVersion)
@@ -458,15 +444,9 @@ public struct LocalFileInformationGenerator {
let size = pageRef.getBoxRect(CGPDFBox.mediaBox).size
add(key: "Resolution", value: "\(Int(size.width))x\(Int(size.height))")
}
if let creator = getKey("Creator", from: dict), !creator.isEmpty {
add(key: "Content creator", value: creator)
}
if let creationDateString = getKey("CreationDate", from: dict) {
add(key: "Creation date", value: convertDate(creationDateString))
}
if let modifiedDateString = getKey("ModDate", from: dict) {
add(key: "Modified date", value: convertDate(modifiedDateString))
}
add(key: "Content creator", value: getKey("Creator", from: dict))
add(key: "Creation date", value: convertDate(getKey("CreationDate", from: dict)))
add(key: "Modified date", value: convertDate(getKey("ModDate", from: dict)))
add(key: "Security", value: reference.isEncrypted ? "Present" : "None")
add(key: "Allows printing", value: reference.allowsPrinting ? "Yes" : "No")
add(key: "Allows copying", value: reference.allowsCopying ? "Yes" : "No")
+4 -5
View File
@@ -13,7 +13,6 @@ 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
internal class FPSStreamTask: URLSessionTask, StreamDelegate {
fileprivate var inputStream: InputStream?
fileprivate var outputStream: OutputStream?
@@ -240,10 +239,10 @@ internal class FPSStreamTask: URLSessionTask, StreamDelegate {
var timedOut: Bool = false
dispatch_queue.async {
if timeout > 0 {
self.dispatch_queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(timeout * 1_000_000_000)) / Double(NSEC_PER_SEC), execute: {
self.dispatch_queue.asyncAfter(deadline: .now() + 1) {
timedOut = true
completionHandler(nil, inputStream.streamStatus == .atEnd, inputStream.streamError as NSError?)
})
}
}
while (self.dataReceived.length == 0 || self.dataReceived.length < minBytes) && !timedOut {
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1));
@@ -278,10 +277,10 @@ internal class FPSStreamTask: URLSessionTask, StreamDelegate {
var timedOut: Bool = false
dispatch_queue.async {
if timeout > 0 {
self.dispatch_queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(timeout * 1_000_000_000)) / Double(NSEC_PER_SEC), execute: {
self.dispatch_queue.asyncAfter(deadline: .now() + 1) {
timedOut = true
completionHandler(self._error)
})
}
}
self.dataToBeSent.append(data)
+48 -67
View File
@@ -25,7 +25,7 @@ open class FileObject: Equatable {
}
/// url to access the resource, not supported by Dropbox provider
@available(*, deprecated, renamed: "url", message: "Use url.absoluteURL instead.")
@available(*, obsoleted: 1.0, renamed: "url", message: "Use url.absoluteURL instead.")
open var absoluteURL: URL? {
return url?.absoluteURL
}
@@ -101,12 +101,6 @@ open class FileObject: Equatable {
}
}
/// **OBSOLETED:** Use `type` property instead.
@available(*, obsoleted: 1.0, renamed: "type", message: "Use type property instead.")
open var fileType: URLFileResourceType? {
return self.type
}
/// File is hidden either because begining with dot or filesystem flags
/// Setting this value on a file begining with dot has no effect
open internal(set) var isHidden: Bool {
@@ -156,6 +150,52 @@ open class FileObject: Equatable {
}
return rhs.path == lhs.path && rhs.size == lhs.size && rhs.modifiedDate == lhs.modifiedDate
}
internal func mapPredicate() -> [String: Any] {
let mapDict: [URLResourceKey: String] = [.fileURL: "url", .nameKey: "name", .pathKey: "path", .fileSizeKey: "filesize", .creationDateKey: "creationDate",
.contentModificationDateKey: "modifiedDate", .isHiddenKey: "isHidden", .isWritableKey: "isWritable", .serverDate: "serverDate", .entryTag: "entryTag", .mimeType: "mimeType"]
let typeDict: [URLFileResourceType: String] = [.directory: "directory", .regular: "regular", .symbolicLink: "symbolicLink", .unknown: "unknown"]
var result = [String: Any]()
for (key, value) in allValues {
if let convertkey = mapDict[key] {
result[convertkey] = value
}
}
result["eTag"] = result["entryTag"]
result["isReadOnly"] = self.isReadOnly
result["isDirectory"] = self.isDirectory
result["isRegularFile"] = self.isRegularFile
result["isSymLink"] = self.isSymLink
result["type"] = typeDict[self.type ?? .unknown] ?? "unknown"
return result
}
static public func convertPredicate(fromSpotlight query: NSPredicate) -> NSPredicate {
let mapDict: [String: URLResourceKey] = [NSMetadataItemURLKey: .fileURL, NSMetadataItemFSNameKey: .nameKey, NSMetadataItemPathKey: .pathKey,
NSMetadataItemFSSizeKey: .fileSizeKey, NSMetadataItemFSCreationDateKey: .creationDateKey,
NSMetadataItemFSContentChangeDateKey: .contentModificationDateKey, "kMDItemFSInvisible": .isHiddenKey, "kMDItemFSIsWriteable": .isWritableKey, "kMDItemKind": .mimeType]
if let cQuery = query as? NSCompoundPredicate {
let newSub = cQuery.subpredicates.map { convertPredicate(fromSpotlight: $0 as! NSPredicate) }
switch cQuery.compoundPredicateType {
case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub)
case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub[0])
case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub)
}
} else if let cQuery = query as? NSComparisonPredicate {
var newLeft = cQuery.leftExpression
var newRight = cQuery.rightExpression
if newLeft.expressionType == .keyPath, let newKey = mapDict[newLeft.keyPath] {
newLeft = NSExpression(forKeyPath: newKey.rawValue)
}
if newRight.expressionType == .keyPath, let newKey = mapDict[newRight.keyPath] {
newRight = NSExpression(forKeyPath: newKey.rawValue)
}
return NSComparisonPredicate(leftExpression: newLeft, rightExpression: newRight, modifier: cQuery.comparisonPredicateModifier, type: cQuery.predicateOperatorType, options: cQuery.options)
} else {
return query
}
}
}
internal func resolve(dateString: String) -> Date? {
@@ -245,7 +285,7 @@ public struct FileObjectSorting {
self.isDirectoriesFirst = isDirectoriesFirst
}
/// Sorts array of `FileObject`s by criterias set in properties
/// Sorts array of `FileObject`s by criterias set in attributes.
public func sort(_ files: [FileObject]) -> [FileObject] {
return files.sorted {
if isDirectoriesFirst {
@@ -281,62 +321,3 @@ public struct FileObjectSorting {
}
}
}
extension Array where Element: FileObject {
/// Returns a sorted array of `FileObject`s by criterias set in properties.
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.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
case FileAttributeType.typeDirectory: self = .directory
case FileAttributeType.typeBlockSpecial: self = .blockSpecial
case FileAttributeType.typeRegular: self = .regular
case FileAttributeType.typeSymbolicLink: self = .symbolicLink
case FileAttributeType.typeSocket: self = .socket
case FileAttributeType.typeUnknown: self = .unknown
default: self = .unknown
}
}
}
internal extension URLResourceKey {
static let fileURL = URLResourceKey(rawValue: "NSURLFileURLKey")
static let serverDate = URLResourceKey(rawValue: "NSURLServerDateKey")
static let entryTag = URLResourceKey(rawValue: "NSURLEntryTagKey")
static let mimeType = URLResourceKey(rawValue: "NSURLMIMETypeIdentifierKey")
}
internal extension URL {
var uw_scheme: String {
return self.scheme ?? ""
}
}
internal func jsonToDictionary(_ jsonString: String) -> [String: AnyObject]? {
guard let data = jsonString.data(using: .utf8) else {
return nil
}
if let dic = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions()) as? [String: AnyObject] {
return dic
}
return nil
}
internal func dictionaryToJSON(_ dictionary: [String: AnyObject]) -> String? {
if let data = try? JSONSerialization.data(withJSONObject: dictionary, options: JSONSerialization.WritingOptions()) {
return String(data: data, encoding: .utf8)
}
return nil
}
+174 -143
View File
@@ -19,16 +19,13 @@ public typealias ImageClass = NSImage
public typealias SimpleCompletionHandler = ((_ error: Error?) -> Void)?
/// This protocol defines FileProvider neccesary functions and properties to connect and get contents list
public protocol FileProviderBasic: class {
public protocol FileProviderBasic: class, NSCoding, NSSecureCoding {
/// An string to identify type of provider.
static var type: String { get }
/// An string to identify type of provider.
var type: String { get }
/// The paths in arguments should resolved against base url.
var isPathRelative: Bool { get }
/// The url of which paths should resolve against.
var baseURL: URL? { get }
@@ -68,7 +65,7 @@ public protocol FileProviderBasic: class {
If the directory contains no entries or an error is occured, this method will return the empty array.
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
- Parameter completionHandler: a block with result of directory entries or error.
- Parameter completionHandler: a closure with result of directory entries or error.
`contents`: An array of `FileObject` identifying the the directory entries.
`error`: Error returned by system.
*/
@@ -80,7 +77,7 @@ public protocol FileProviderBasic: class {
If the directory contains no entries or an error is occured, this method will return the empty `FileObject`.
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
- Parameter completionHandler: a block with result of directory entries or error.
- Parameter completionHandler: a closure with result of directory entries or error.
`attributes`: A `FileObject` containing the attributes of the item.
`error`: Error returned by system.
*/
@@ -90,6 +87,43 @@ public protocol FileProviderBasic: class {
/// Returns total and used capacity in provider container asynchronously.
func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void))
/**
Search files inside directory using query asynchronously.
- Note: Query string is limited to file name, to search based on other file attributes, use NSPredicate version.
- Parameters:
- path: location of directory to start search
- recursive: Searching subdirectories of path
- query: Simple string that file name begins with to be search, case-insensitive.
- foundItemHandler: Closure which is called when a file is found
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
*/
func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void))
/**
Search files inside directory using query asynchronously.
Sample predicates:
```
NSPredicate(format: "(name CONTAINS[c] 'hello') && (filesize >= 10000)")
NSPredicate(format: "(modifiedDate >= %@)", Date())
NSPredicate(format: "(path BEGINSWITH %@)", "folder/child folder")
```
- Note: Don't pass Spotlight predicates to this method directly, use `FileProvider.convertSpotlightPredicateTo()` method to get usable predicate.
- Important: A file name criteria should be provided for Dropbox.
- Parameters:
- path: location of directory to start search
- recursive: Searching subdirectories of path
- query: An `NSPredicate` object with keys like `FileObject` members, except `size` which becomes `filesize`.
- foundItemHandler: Closure which is called when a file is found
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
*/
func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void))
/**
Returns an independent url to access the file. Some providers like `Dropbox` due to their nature.
don't return an absolute url to be used to access file directly.
@@ -103,6 +137,11 @@ public protocol FileProviderBasic: class {
}
extension FileProviderBasic {
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
let predicate = NSPredicate(format: "name BEGINSWITH[c] %@", query)
self.searchFiles(path: path, recursive: recursive, query: predicate, foundItemHandler: foundItemHandler, completionHandler: completionHandler)
}
/// The maximum number of queued operations that can execute at the same time.
///
/// The default value of this property is `OperationQueue.defaultMaxConcurrentOperationCount`.
@@ -114,6 +153,8 @@ extension FileProviderBasic {
operation_queue.maxConcurrentOperationCount = newValue
}
}
}
/// Checking equality of two file provider, regardless of current path queues and delegates.
@@ -122,10 +163,10 @@ public func ==(lhs: FileProviderBasic, rhs: FileProviderBasic) -> Bool {
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
return lhs.type == rhs.type && lhs.baseURL == rhs.baseURL && lhs.credential == rhs.credential
}
/// Cancels all active underlying tasks
/// Cancels all active underlying tasks when deallocating remote providers
public var fileProviderCancelTasksOnInvalidating = true
/// Extending `FileProviderBasic` for web-based file providers
@@ -133,10 +174,18 @@ public protocol FileProviderBasicRemote: FileProviderBasic {
/// Underlying URLSession instance used for HTTP/S requests
var session: URLSession { get }
/// A `URLCache` to cache downloaded files and contents.
///
/// If set to nil, `URLCache.shared` object will be used.
/// - Note: It has no effect unless setting `useCache` property to `true`.
/**
A `URLCache` to cache downloaded files and contents.
- Note: It has no effect unless setting `useCache` property to `true`.
- Warning: FileProvider doesn't manage/free `URLCache` object in a memory pressure scenario. It's upon you to clear
cache memory when receiving `didReceiveMemoryWarning` or via observing `.UIApplicationDidReceiveMemoryWarning` notification.
To clear memory usage use this code:
```
provider.cache?.removeAllCachedResponses()
```
*/
var cache: URLCache? { get }
/// Determine to use `cache` property to cache downloaded file objects. Doesn't have effect on query type methods.
@@ -167,7 +216,7 @@ internal extension FileProviderBasicRemote {
}
group.leave()
}).resume()
_ = group.wait(timeout: DispatchTime.now() + self.session.configuration.timeoutIntervalForRequest)
_ = group.wait(timeout: .now() + self.session.configuration.timeoutIntervalForRequest)
}
if validatedCache {
completionHandler(response.data, response.response, nil)
@@ -224,7 +273,6 @@ public protocol FileProviderOperations: FileProviderBasic {
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
@@ -363,7 +411,7 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- Parameters:
- path: Path of file.
- completionHandler: a block with result of file contents or error.
- completionHandler: a closure with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
@@ -379,7 +427,7 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- path: Path of file.
- offset: First byte index which should be read. **Starts from 0.**
- length: Bytes count of data. Pass `-1` to read until the end of file.
- completionHandler: a block with result of file contents or error.
- completionHandler: a closure with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
@@ -442,20 +490,6 @@ public protocol FileProviderReadWrite: FileProviderBasic {
*/
@discardableResult
func writeContents(path: String, contents: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Search files inside directory using query asynchronously.
- Note: For now only it's limited to file names. `query` parameter may take `NSPredicate` format in near future.
- Parameters:
- path: location of directory to start search
- recursive: Searching subdirectories of path
- query: Simple string of file name to be search (for now).
- foundItemHandler: Block which is called when a file is found
- completionHandler: Block which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
*/
func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void))
}
extension FileProviderReadWrite {
@@ -496,7 +530,7 @@ public protocol FileProviderMonitor: FileProviderBasic {
- Parameters:
- path: path of directory.
- eventHandler: Block executed after change, on a secondary thread.
- eventHandler: Closure executed after change, on a secondary thread.
*/
func registerNotifcation(path: String, eventHandler: @escaping (() -> Void))
@@ -572,31 +606,16 @@ extension FileProviderBasic {
return type(of: self).type
}
/// path without heading and trailing slash
public var bareCurrentPath: String {
return currentPath.trimmingCharacters(in: pathTrimSet)
}
func escaped(path: String) -> String {
return path.trimmingCharacters(in: pathTrimSet).addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
}
/// **OBSOLETED:** Use `url(of:).absoluteURL` instead.
@available(*, obsoleted: 1.0, renamed: "url(of:)", message: "Use url(of:).absoluteURL instead.")
public func absoluteURL(_ path: String? = nil) -> URL {
return url(of: path).absoluteURL
}
/// **OBSOLETED** This property never worked as expected and is redundant as only supported by `LocalFileProvider`.
/// To simulate `false` value, assign `URL(fileURLWithPath: "/")` to `baseURL`.
@available(*, obsoleted: 1.0, message: "Redundant property, now is always true.")
var isPathRelative: Bool { return true }
public func url(of path: String? = nil) -> URL {
var rpath: String
if let path = path {
rpath = path
} else {
rpath = self.currentPath
}
var rpath: String = path ?? self.currentPath
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath
if let baseURL = baseURL {
if isPathRelative && rpath.hasPrefix("/") {
if rpath.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
}
return URL(string: rpath, relativeTo: baseURL) ?? baseURL
@@ -614,15 +633,17 @@ extension FileProviderBasic {
/// - Returns: A `String` contains relative path of url against base url.
public func relativePathOf(url: URL) -> String {
// check if url derieved from current base url
if url.relativeString.isEmpty, url.baseURL == self.baseURL {
return url.relativePath.removingPercentEncoding!
let relativePath = url.relativePath
if !relativePath.isEmpty, url.baseURL == self.baseURL {
return relativePath.removingPercentEncoding ?? relativePath
}
// resolve url string against baseurl
guard let baseURL = self.baseURL?.standardizedFileURL else { return url.absoluteString }
let standardPath = url.absoluteString.replacingOccurrences(of: "file:///private/var/", with: "file:///var/", options: .anchored)
let standardBase = baseURL.absoluteString.replacingOccurrences(of: "file:///private/var/", with: "file:///var/", options: .anchored)
return standardPath.replacingOccurrences(of: standardBase, with: "/").removingPercentEncoding!
let standardRelativePath = standardPath.replacingOccurrences(of: standardBase, with: "/")
return standardRelativePath.removingPercentEncoding ?? standardRelativePath
}
internal func correctPath(_ path: String?) -> String? {
@@ -667,7 +688,7 @@ extension FileProviderBasic {
}
group.leave()
}
_ = group.wait(timeout: DispatchTime.now() + 5)
_ = group.wait(timeout: .now() + 5)
let finalFile = result + (!fileExt.isEmpty ? "." + fileExt : "")
return (dirPath as NSString).appendingPathComponent(finalFile)
}
@@ -710,7 +731,7 @@ public protocol ExtendedFileProvider: FileProviderBasic {
- Parameters:
- path: path of file.
- completionHandler: a block with result of preview image or error.
- completionHandler: a closure with result of preview image or error.
`image`: `NSImage`/`UIImage` object contains preview.
`error`: Error returned by system.
*/
@@ -726,7 +747,7 @@ public protocol ExtendedFileProvider: FileProviderBasic {
- Parameters:
- path: path of file.
- dimension: width and height of result preview image.
- completionHandler: a block with result of preview image or error.
- completionHandler: a closure with result of preview image or error.
`image`: `NSImage`/`UIImage` object contains preview.
`error`: Error returned by system.
*/
@@ -741,7 +762,7 @@ public protocol ExtendedFileProvider: FileProviderBasic {
- Parameters:
- path: path of file.
- completionHandler: a block with result of preview image or error.
- completionHandler: a closure with result of preview image or error.
`propertiesDictionary`: A `Dictionary` of proprty keys and values.
`keys`: An `Array` contains ordering of keys.
`error`: Error returned by system.
@@ -754,96 +775,82 @@ extension ExtendedFileProvider {
self.thumbnailOfFile(path: path, dimension: nil, completionHandler: completionHandler)
}
internal static func formatshort(interval: TimeInterval) -> String {
var result = "0:00"
if interval < TimeInterval(Int32.max) {
result = ""
var time = DateComponents()
time.hour = Int(interval / 3600)
time.minute = Int((interval.truncatingRemainder(dividingBy: 3600)) / 60)
time.second = Int(interval.truncatingRemainder(dividingBy: 60))
let formatter = NumberFormatter()
formatter.paddingCharacter = "0"
formatter.minimumIntegerDigits = 2
formatter.maximumFractionDigits = 0
let formatterFirst = NumberFormatter()
formatterFirst.maximumFractionDigits = 0
if time.hour! > 0 {
result = "\(formatterFirst.string(from: NSNumber(value: time.hour!))!):\(formatter.string(from: NSNumber(value: time.minute!))!):\(formatter.string(from: NSNumber(value: time.second!))!)"
} else {
result = "\(formatterFirst.string(from: NSNumber(value: time.minute!))!):\(formatter.string(from: NSNumber(value: time.second!))!)"
}
}
result = result.trimmingCharacters(in: CharacterSet(charactersIn: ": "))
return result
}
internal static func dataIsPDF(_ data: Data) -> Bool {
return data.count > 4 && data.scanString(length: 4, encoding: .ascii) == "%PDF"
}
internal static func convertToImage(pdfData: Data?, page: Int = 1) -> ImageClass? {
guard let pdfData = pdfData else { return nil }
let cfPDFData: CFData = pdfData as CFData
if let provider = CGDataProvider(data: cfPDFData), let reference = CGPDFDocument(provider), let pageRef = reference.page(at: page) {
let frame = pageRef.getBoxRect(CGPDFBox.mediaBox)
var size = frame.size
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
#if os(macOS)
let ppp = Int(NSScreen.main()?.backingScaleFactor ?? 1) // fetch device is retina or not
size.width *= CGFloat(ppp)
size.height *= CGFloat(ppp)
let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSCalibratedRGBColorSpace,
bytesPerRow: 0, bitsPerPixel: 0)
guard let context = NSGraphicsContext(bitmapImageRep: rep!) else {
return nil
}
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.setCurrent(context)
let transform = pageRef.getDrawingTransform(CGPDFBox.mediaBox, rect: rect, rotate: 0, preserveAspectRatio: true)
context.cgContext.concatenate(transform)
context.cgContext.translateBy(x: 0, y: size.height)
context.cgContext.scaleBy(x: CGFloat(ppp), y: CGFloat(-ppp))
context.cgContext.drawPDFPage(pageRef)
let resultingImage = NSImage(size: size)
resultingImage.addRepresentation(rep!)
return resultingImage
#else
let ppp = Int(UIScreen.main.scale) // fetch device is retina or not
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
size.width *= CGFloat(ppp)
size.height *= CGFloat(ppp)
UIGraphicsBeginImageContext(size)
context.saveGState()
let transform = pageRef.getDrawingTransform(CGPDFBox.mediaBox, rect: rect, rotate: 0, preserveAspectRatio: true)
context.concatenate(transform)
context.translateBy(x: 0, y: size.height)
context.scaleBy(x: CGFloat(ppp), y: CGFloat(-ppp))
context.drawPDFPage(pageRef)
context.restoreGState()
let resultingImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return resultingImage
#endif
return self.convertToImage(pdfPage: pageRef)
}
return nil
}
internal static func convertToImage(pdfURL: URL, page: Int = 1) -> ImageClass? {
// To accelerate, supporting only local file URL
guard pdfURL.isFileURL else { return nil }
if let reference = CGPDFDocument(pdfURL as CFURL), let pageRef = reference.page(at: page) {
return self.convertToImage(pdfPage: pageRef)
}
return nil
}
private static func convertToImage(pdfPage: CGPDFPage) -> ImageClass? {
let frame = pdfPage.getBoxRect(CGPDFBox.mediaBox)
var size = frame.size
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
#if os(macOS)
let ppp = Int(NSScreen.main()?.backingScaleFactor ?? 1) // fetch device is retina or not
size.width *= CGFloat(ppp)
size.height *= CGFloat(ppp)
let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSCalibratedRGBColorSpace,
bytesPerRow: 0, bitsPerPixel: 0)
guard let context = NSGraphicsContext(bitmapImageRep: rep!) else {
return nil
}
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.setCurrent(context)
let transform = pdfPage.getDrawingTransform(CGPDFBox.mediaBox, rect: rect, rotate: 0, preserveAspectRatio: true)
context.cgContext.concatenate(transform)
context.cgContext.translateBy(x: 0, y: size.height)
context.cgContext.scaleBy(x: CGFloat(ppp), y: CGFloat(-ppp))
context.cgContext.drawPDFPage(pdfPage)
let resultingImage = NSImage(size: size)
resultingImage.addRepresentation(rep!)
return resultingImage
#else
let ppp = Int(UIScreen.main.scale) // fetch device is retina or not
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
size.width *= CGFloat(ppp)
size.height *= CGFloat(ppp)
UIGraphicsBeginImageContext(size)
context.saveGState()
let transform = pdfPage.getDrawingTransform(CGPDFBox.mediaBox, rect: rect, rotate: 0, preserveAspectRatio: true)
context.concatenate(transform)
context.translateBy(x: 0, y: size.height)
context.scaleBy(x: CGFloat(ppp), y: CGFloat(-ppp))
context.drawPDFPage(pdfPage)
context.restoreGState()
let resultingImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return resultingImage
#endif
}
internal static func scaleDown(image: ImageClass, toSize maxSize: CGSize) -> ImageClass {
let height, width: CGFloat
if image.size.width > image.size.height {
@@ -925,11 +932,37 @@ public enum FileOperationType: CustomStringConvertible {
return mirror.children.dropFirst().first?.value as? String
}
init? (json: [String: AnyObject]) {
guard let type = json["type"] as? String, let source = json["source"] as? String else {
return nil
}
let dest = json["dest"] as? String
switch type {
case "Create":
self = .create(path: source)
case "Modify":
self = .modify(path: source)
case "Remove":
self = .remove(path: source)
case "Copy":
guard let dest = dest else { return nil }
self = .copy(source: source, destination: dest)
case "Move":
guard let dest = dest else { return nil }
self = .move(source: source, destination: dest)
case "Link":
guard let dest = dest else { return nil }
self = .link(link: source, target: dest)
default:
return nil
}
}
internal var json: String? {
var dictionary: [String: AnyObject] = ["type": self.description as NSString]
dictionary["source"] = source as NSString?
dictionary["dest"] = destination as NSString?
return dictionaryToJSON(dictionary)
return String(jsonDictionary: dictionary)
}
}
@@ -1001,5 +1034,3 @@ public protocol FoundationErrorEnum {
var rawValue: Int { get }
}
extension URLError.Code: FoundationErrorEnum {}
extension CocoaError.Code: FoundationErrorEnum {}
+182
View File
@@ -0,0 +1,182 @@
//
// FileProviderExtensions.swift
// FileProvider
//
// Created by Amir Abbas on 12/27/1395 AP.
//
//
import Foundation
extension Array where Element: FileObject {
/// Returns a sorted array of `FileObject`s by criterias set in attributes.
public func sort(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) -> [Element] {
let sorting = FileObjectSorting(type: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
return sorting.sort(self) as! [Element]
}
/// Sorts array of `FileObject`s by criterias set in attributes.
public mutating func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) {
self = self.sort(by: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
}
}
extension URLFileResourceType {
/// Returns corresponding `URLFileResourceType` of a `FileAttributeType` value
public init(fileTypeValue: FileAttributeType) {
switch fileTypeValue {
case FileAttributeType.typeCharacterSpecial: self = .characterSpecial
case FileAttributeType.typeDirectory: self = .directory
case FileAttributeType.typeBlockSpecial: self = .blockSpecial
case FileAttributeType.typeRegular: self = .regular
case FileAttributeType.typeSymbolicLink: self = .symbolicLink
case FileAttributeType.typeSocket: self = .socket
case FileAttributeType.typeUnknown: self = .unknown
default: self = .unknown
}
}
}
internal extension URLResourceKey {
static let fileURL = URLResourceKey(rawValue: "NSURLFileURLKey")
static let serverDate = URLResourceKey(rawValue: "NSURLServerDateKey")
static let entryTag = URLResourceKey(rawValue: "NSURLEntryTagKey")
static let mimeType = URLResourceKey(rawValue: "NSURLMIMETypeIdentifierKey")
}
internal extension URL {
var uw_scheme: String {
return self.scheme ?? ""
}
var fileIsDirectory: Bool {
return (try? self.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
}
var fileSize: Int64 {
return Int64((try? self.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1)
}
var fileExists: Bool {
return self.isFileURL && FileManager.default.fileExists(atPath: self.path)
}
}
internal extension Data {
internal var isPDF: Bool {
return self.count > 4 && self.scanString(length: 4, using: .ascii) == "%PDF"
}
init? (jsonDictionary dictionary: [String: AnyObject]) {
guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else {
return nil
}
self = data
}
func deserializeJSON() -> [String: AnyObject]? {
if let dic = try? JSONSerialization.jsonObject(with: self, options: []) as? [String: AnyObject] {
return dic
}
return nil
}
init<T>(value: T) {
var value = value
self = Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
}
func scanValue<T>() -> T? {
guard MemoryLayout<T>.size <= self.count else { return nil }
return self.withUnsafeBytes { $0.pointee }
}
func scanValue<T>(start: Int) -> T? {
let length = MemoryLayout<T>.size
guard self.count >= start + length else { return nil }
return self.subdata(in: start..<start+length).withUnsafeBytes { $0.pointee }
}
func scanString(start: Int = 0, length: Int, using encoding: String.Encoding = .utf8) -> String? {
guard self.count >= start + length else { return nil }
return String(data: self.subdata(in: start..<start+length), encoding: encoding)
}
static func mapMemory<T, U>(from: T) -> U? {
guard MemoryLayout<T>.size >= MemoryLayout<U>.size else { return nil }
let data = Data(value: from)
return data.scanValue()
}
}
internal extension String {
init? (jsonDictionary: [String: AnyObject]) {
guard let data = Data(jsonDictionary: jsonDictionary) else {
return nil
}
self.init(data: data, encoding: .utf8)
}
func deserializeJSON(using encoding: String.Encoding = .utf8) -> [String: AnyObject]? {
guard let data = self.data(using: encoding) else {
return nil
}
return data.deserializeJSON()
}
}
internal extension TimeInterval {
internal var formatshort: String {
var result = "0:00"
if self < TimeInterval(Int32.max) {
result = ""
var time = DateComponents()
time.hour = Int(self / 3600)
time.minute = Int((self.truncatingRemainder(dividingBy: 3600)) / 60)
time.second = Int(self.truncatingRemainder(dividingBy: 60))
let formatter = NumberFormatter()
formatter.paddingCharacter = "0"
formatter.minimumIntegerDigits = 2
formatter.maximumFractionDigits = 0
let formatterFirst = NumberFormatter()
formatterFirst.maximumFractionDigits = 0
if time.hour! > 0 {
result = "\(formatterFirst.string(from: NSNumber(value: time.hour!))!):\(formatter.string(from: NSNumber(value: time.minute!))!):\(formatter.string(from: NSNumber(value: time.second!))!)"
} else {
result = "\(formatterFirst.string(from: NSNumber(value: time.minute!))!):\(formatter.string(from: NSNumber(value: time.second!))!)"
}
}
result = result.trimmingCharacters(in: CharacterSet(charactersIn: ": "))
return result
}
}
extension NSPredicate {
func findValue(forKey key: String?, operator op: NSComparisonPredicate.Operator? = nil) -> Any? {
let val = findAllValues(forKey: key).lazy.filter { (op == nil || $0.operator == op!) && !$0.not }
return val.first?.value
}
func findAllValues(forKey key: String?) -> [(value: Any, operator: NSComparisonPredicate.Operator, not: Bool)] {
if let cQuery = self as? NSCompoundPredicate {
let find = cQuery.subpredicates.flatMap { ($0 as! NSPredicate).findAllValues(forKey: key) }
if cQuery.compoundPredicateType == .not {
return find.map { return ($0.value, $0.operator, !$0.not) }
}
return find
} else if let cQuery = self as? NSComparisonPredicate {
if cQuery.leftExpression.expressionType == .keyPath, key == nil || cQuery.leftExpression.keyPath == key!, let const = cQuery.rightExpression.constantValue {
return [(value: const, operator: cQuery.predicateOperatorType, false)]
}
if cQuery.rightExpression.expressionType == .keyPath, key == nil || cQuery.rightExpression.keyPath == key!, let const = cQuery.leftExpression.constantValue {
return [(value: const, operator: cQuery.predicateOperatorType, false)]
}
return []
} else {
return []
}
}
}
extension URLError.Code: FoundationErrorEnum {}
extension CocoaError.Code: FoundationErrorEnum {}
+80 -66
View File
@@ -16,15 +16,16 @@ import Foundation
*/
open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndoable {
open class var type: String { return "Local" }
open var isPathRelative: Bool
open fileprivate(set) var baseURL: URL?
open var currentPath: String
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue
open weak var delegate: FileProviderDelegate?
open internal(set) var credential: URLCredential?
/// Underlying `FileManager` object for listing and metadata fetching.
open private(set) var fileManager = FileManager()
/// Underlying `FileManager` object for operationa like copying, moving, etc.
open private(set) var opFileManager = FileManager()
fileprivate var fileProviderManagerDelegate: LocalFileProviderManagerDelegate? = nil
@@ -46,10 +47,10 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
default values are `directory: .documentDirectory, domainMask: .userDomainMask`.
- Parameters:
- directory: The search path directory. The supported values are described in `FileManager.SearchPathDirectory`.
- domainMask: The file system domain to search. The value for this parameter is one or more of the constants described in `FileManager.SearchPathDomainMask`.
*/
public convenience init (directory: FileManager.SearchPathDirectory = .documentDirectory, domainMask: FileManager.SearchPathDomainMask = .userDomainMask) {
- for: The search path directory. The supported values are described in `FileManager.SearchPathDirectory`.
- in: Base locations for directory to search. The value for this parameter is one or more of the constants described in `FileManager.SearchPathDomainMask`.
*/
public convenience init (for directory: FileManager.SearchPathDirectory = .documentDirectory, in domainMask: FileManager.SearchPathDomainMask = .userDomainMask) {
self.init(baseURL: FileManager.default.urls(for: directory, in: domainMask).first!)
}
@@ -61,7 +62,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
default values are `directory: .documentDirectory`.
- Parameters:
- sharedContainerId: Same with `App Group` identifier defined in project settings.
- sharedContainerId: Same with `App Group` identifier defined in project settings.
- directory: The search path directory. The supported values are described in `FileManager.SearchPathDirectory`.
*/
public convenience init? (sharedContainerId: String, directory: FileManager.SearchPathDirectory = .documentDirectory) {
@@ -85,6 +86,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
self.init(baseURL: finalBaseURL)
self.isCoorinating = true
try? fileManager.createDirectory(at: finalBaseURL, withIntermediateDirectories: true)
}
@@ -97,7 +99,6 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
fatalError("Cannot initialize a Local provider from remote URL.")
}
self.baseURL = baseURL
self.isPathRelative = true
self.currentPath = ""
self.credential = nil
self.isCoorinating = false
@@ -108,11 +109,39 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self)
opFileManager.delegate = fileProviderManagerDelegate
}
/// **DEPRECATED:** No longer is in use and overriding this method has no effect anymore.
@available(*, deprecated, message: "Overriding this method has no effect anymore.")
public required convenience init?(coder aDecoder: NSCoder) {
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else {
return nil
}
self.init(baseURL: baseURL)
self.currentPath = aDecoder.decodeObject(of: NSString.self, forKey: "currentPath") as? String ?? ""
self.isCoorinating = aDecoder.decodeBool(forKey: "isCoorinating")
}
open func encode(with aCoder: NSCoder) {
aCoder.encode(self.baseURL, forKey: "currentPath")
aCoder.encode(self.currentPath, forKey: "currentPath")
aCoder.encode(self.isCoorinating, forKey: "isCoorinating")
}
public static var supportsSecureCoding: Bool {
return true
}
public func copy(with zone: NSZone? = nil) -> Any {
let copy = LocalFileProvider(baseURL: self.baseURL!)
copy.currentPath = self.currentPath
copy.undoManager = self.undoManager
copy.isCoorinating = self.isCoorinating
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
return copy
}
/// **OBSOLETED:** No longer is in use and overriding this method has no effect anymore.
@available(*, obsoleted: 1.0, message: "Overriding this method has no effect anymore.")
open class func defaultBaseURL() -> URL {
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
}
@@ -132,6 +161,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
}
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
dispatch_queue.async {
completionHandler(LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), nil)
}
}
open func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) {
let values = try? baseURL?.resourceValues(forKeys: [.volumeTotalCapacityKey, .volumeAvailableCapacityKey])
let totalSize = Int64(values??.volumeTotalCapacity ?? -1)
@@ -139,9 +174,21 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
completionHandler(totalSize, totalSize - freeSize)
}
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
dispatch_queue.async {
completionHandler(LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), nil)
let iterator = self.fileManager.enumerator(at: self.url(of: path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in
completionHandler([], e)
return true
}
var result = [LocalFileObject]()
while let fileURL = iterator?.nextObject() as? URL {
let path = self.relativePathOf(url: fileURL)
if let fileObject = LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), query.evaluate(with: fileObject.mapPredicate()) {
result.append(fileObject)
foundItemHandler?(fileObject)
}
}
completionHandler(result, nil)
}
}
@@ -227,26 +274,24 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
@discardableResult
fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
func urlofpath(path: String) -> URL {
if path.hasPrefix("file://") {
let removedSchemePath = path.replacingOccurrences(of: "file://", with: "", options: .anchored)
let pDecodedPath = removedSchemePath.removingPercentEncoding ?? removedSchemePath
return URL(fileURLWithPath: pDecodedPath)
} else {
return self.url(of: path)
}
}
guard let sourcePath = opType.source else { return nil }
let destPath = opType.destination
let source: URL
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 source: URL = urlofpath(path: sourcePath)
let dest: URL?
if let destPath = 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)
}
dest = urlofpath(path: destPath)
} else {
dest = nil
}
@@ -268,7 +313,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
if sourcePath.hasSuffix("/") {
try self.opFileManager.createDirectory(at: source, withIntermediateDirectories: true, attributes: [:])
} else {
try data?.write(to: source, options: Data.WritingOptions.atomic)
try data?.write(to: source, options: .atomic)
}
case .modify:
try data?.write(to: source, options: atomically ? [.atomic] : [])
@@ -307,8 +352,8 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
if isCoorinating {
var intents = [NSFileAccessIntent]()
successfulSecurityScopedResourceAccess = source.startAccessingSecurityScopedResource()
var intents = [NSFileAccessIntent]()
switch opType {
case .create, .modify:
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forReplacing))
@@ -452,32 +497,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
return self.doOperation(opType, data: data, atomically: atomically, completionHandler: completionHandler)
}
open func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
dispatch_queue.async {
let iterator = self.fileManager.enumerator(at: self.url(of: path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in
completionHandler([], e)
return true
}
var result = [LocalFileObject]()
while let fileURL = iterator?.nextObject() as? URL {
if fileURL.lastPathComponent.lowercased().contains(query.lowercased()) {
let path = self.relativePathOf(url: fileURL)
if let fileObject = LocalFileObject(fileWithPath: path, relativeTo: self.baseURL) {
result.append(fileObject)
foundItemHandler?(fileObject)
}
}
}
completionHandler(result, nil)
}
}
fileprivate var monitors = [LocalFolderMonitor]()
open func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
self.unregisterNotifcation(path: path)
let dirurl = self.url(of: path)
let isdir = (try? dirurl.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false) ?? false
let isdir = (try? dirurl.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
if !isdir {
return
}
@@ -500,18 +525,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
open func isRegisteredForNotification(path: String) -> Bool {
return monitors.map( { self.relativePathOf(url: $0.url) } ).contains(path)
}
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
return copy
return monitors.map( { self.relativePathOf(url: $0.url) } ).contains(path.trimmingCharacters(in: CharacterSet(charactersIn: "/")))
}
}
@@ -570,7 +584,7 @@ internal extension LocalFileProvider {
errorHandler?(error)
return
}
completionHandler(intents[0].url)
completionHandler(intents.first!.url)
}
}
@@ -581,8 +595,8 @@ internal extension LocalFileProvider {
errorHandler?(error)
return
}
let newSource: URL = intents[0].url
let newDest: URL? = intents.count > 1 ? intents[1].url : nil
guard let newSource: URL = intents.first?.url else { return }
let newDest: URL? = intents.dropFirst().first?.url
if moving, let newDest = newDest {
coordinator.item(at: newSource, willMoveTo: newDest)
}
+18 -35
View File
@@ -21,15 +21,12 @@ public final class LocalFileObject: FileObject {
if relativeURL != nil && rpath.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
}
if rpath.isEmpty {
fileURL = relativeURL
if #available(iOS 9.0, macOS 10.11, tvOS 9.0, *) {
fileURL = URL(fileURLWithPath: rpath, relativeTo: relativeURL)
} else {
if #available(iOS 9.0, macOS 10.11, tvOS 9.0, *) {
fileURL = URL(fileURLWithPath: rpath, relativeTo: relativeURL)
} else {
fileURL = URL(string: rpath, relativeTo: relativeURL)
}
fileURL = URL(string: rpath.isEmpty ? "./" : rpath, relativeTo: relativeURL)
}
if let fileURL = fileURL {
self.init(fileWithURL: fileURL)
} else {
@@ -96,18 +93,21 @@ internal final class LocalFolderMonitor {
init(url: URL, handler: @escaping ()->Void) {
self.url = url
descriptor = open((url as NSURL).fileSystemRepresentation, O_EVTONLY)
source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: DispatchSource.FileSystemEvent.write, queue: qq)
source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: .write, queue: qq)
// Folder monitoring is recursive and deep. Monitoring a root folder may be very costly
// We have a 0.2 second delay to ensure we wont call handler 1000s times when there is
// a huge file operation. This ensures app will work smoothly while this 250 milisec won't
// affect user experince much
let main_handler: ()->Void = {
let main_handler: ()->Void = { [weak self] in
guard let `self` = self else { return }
if Date().timeIntervalSinceReferenceDate < self.monitoredTime + 0.2 {
return
}
self.monitoredTime = Date().timeIntervalSinceReferenceDate
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.25, execute: {
self.source.suspend()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: {
handler()
self.source.resume()
})
}
source.setEventHandler(handler: main_handler)
@@ -296,18 +296,15 @@ open class LocalOperationHandle: OperationHandle {
let fp = FileManager()
let filesList = fp.enumerator(at: pathURL, includingPropertiesForKeys: keys, options: enumOpt, errorHandler: nil)
while let fileURL = filesList?.nextObject() as? URL {
do {
let values = try fileURL.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey])
let isdir = values.isDirectory ?? false
let size = Int64(values.fileSize ?? 0)
if isdir {
folders += 1
} else {
files += 1
}
totalsize += size
} catch _ {
guard let values = try? fileURL.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey]) else { continue }
let isdir = values.isDirectory ?? false
let size = Int64(values.fileSize ?? 0)
if isdir {
folders += 1
} else {
files += 1
}
totalsize += size
}
return (folders, files, totalsize)
@@ -326,17 +323,3 @@ class UndoBox: NSObject {
self.undoOperation = undoOperation
}
}
internal extension URL {
var fileIsDirectory: Bool {
return (try? self.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
}
var fileSize: Int64 {
return Int64((try? self.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1)
}
var fileExists: Bool {
return self.isFileURL && FileManager.default.fileExists(atPath: self.path)
}
}
+105 -66
View File
@@ -19,16 +19,10 @@ import CoreGraphics
*/
open class OneDriveFileProvider: FileProviderBasicRemote {
open class var type: String { return "OneDrive" }
open let isPathRelative: Bool
open let baseURL: URL?
/// OneDrive server url, equals with unwrapped `baseURL`
open var serverURL: URL { return baseURL! }
/// Drive name for user, default is `root`. Changing its value will effect on new operations.
open var drive: String
/// Generated storage url from server url and drive name
open var driveURL: URL {
return URL(string: "/drive/\(drive):/", relativeTo: baseURL)!
}
open var currentPath: String
open var dispatch_queue: DispatchQueue
@@ -71,13 +65,12 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
- serverURL: server url, Set it if you are trying to connect OneDrive Business server, otherwise leave it
`nil` to connect to OneDrive Personal uses.
- drive: drive name for user on server, default value is `root`.
- cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used.
- cache: A URLCache to cache downloaded files and contents.
*/
public init(credential: URLCredential?, serverURL: URL? = nil, drive: String = "root", cache: URLCache? = nil) {
let baseURL = serverURL ?? URL(string: "https://api.onedrive.com/")!
self.baseURL = baseURL.path.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
self.drive = drive
self.isPathRelative = true
self.currentPath = ""
self.useCache = false
self.validatingCache = true
@@ -88,6 +81,38 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
}
public required convenience init?(coder aDecoder: NSCoder) {
self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential,
serverURL: aDecoder.decodeObject(forKey: "baseURL") as? URL,
drive: aDecoder.decodeObject(forKey: "drive") as? String ?? "root")
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
self.useCache = aDecoder.decodeBool(forKey: "useCache")
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
}
open func encode(with aCoder: NSCoder) {
aCoder.encode(self.credential, forKey: "credential")
aCoder.encode(self.baseURL, forKey: "baseURL")
aCoder.encode(self.drive, forKey: "drive")
aCoder.encode(self.currentPath, forKey: "currentPath")
aCoder.encode(self.useCache, forKey: "useCache")
aCoder.encode(self.validatingCache, forKey: "validatingCache")
}
public static var supportsSecureCoding: Bool {
return true
}
open func copy(with zone: NSZone? = nil) -> Any {
let copy = OneDriveFileProvider(credential: self.credential, serverURL: self.baseURL, drive: self.drive, cache: self.cache)
copy.currentPath = self.currentPath
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
copy.useCache = self.useCache
copy.validatingCache = self.validatingCache
return copy
}
deinit {
if fileProviderCancelTasksOnInvalidating {
_session?.invalidateAndCancel()
@@ -103,8 +128,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
}
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
let url = URL(string: escaped(path: path), relativeTo: driveURL)!
var request = URLRequest(url: url)
var request = URLRequest(url: url(of: path))
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
@@ -113,7 +137,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: json) {
if let json = data?.deserializeJSON(), let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: json) {
fileObject = file
}
}
@@ -123,14 +147,13 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
}
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
let url = URL(string: "/drive/root", relativeTo: baseURL)!
var request = URLRequest(url: url)
var request = URLRequest(url: url())
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var totalSize: Int64 = -1
var usedSize: Int64 = 0
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
if let json = data?.deserializeJSON() {
totalSize = (json["total"] as? NSNumber)?.int64Value ?? -1
usedSize = (json["used"] as? NSNumber)?.int64Value ?? 0
}
@@ -139,9 +162,49 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
task.resume()
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
var foundFiles = [OneDriveFileObject]()
var queryStr: String?
queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first
guard let finalQueryStr = queryStr else { return }
search(path, query: finalQueryStr, foundItem: { (file) in
if query.evaluate(with: file.mapPredicate()) {
foundFiles.append(file)
foundItemHandler?(file)
}
}, completionHandler: { (error) in
completionHandler(foundFiles, error)
})
}
open func url(of path: String? = nil, modifier: String? = nil) -> URL {
var rpath: String
if let path = path {
rpath = path
} else {
rpath = self.currentPath
}
if rpath.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
}
if rpath.isEmpty {
if let modifier = modifier {
return baseURL!.appendingPathComponent("drive/\(drive)/\(modifier)")
}
return baseURL!.appendingPathComponent("drive/\(drive)")
}
let driveURL = baseURL!.appendingPathComponent("drive/\(drive):/")
rpath = (rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath)
rpath = rpath.trimmingCharacters(in: pathTrimSet)
if let modifier = modifier {
rpath = rpath + ":/" + modifier
}
return URL(string: rpath, relativeTo: driveURL) ?? driveURL
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
let url = URL(string: "/drive/root", relativeTo: baseURL)!
var request = URLRequest(url: url)
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
@@ -157,25 +220,25 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
extension OneDriveFileProvider: FileProviderOperations {
public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
return doOperation(.create(path: path), completionHandler: completionHandler)
}
public func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let filePath = (path as NSString).appendingPathComponent(fileName)
return self.writeContents(path: filePath, contents: data ?? Data(), completionHandler: completionHandler)
}
public func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
}
public func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
}
public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.remove(path: path), completionHandler: completionHandler)
}
@@ -185,7 +248,7 @@ extension OneDriveFileProvider: FileProviderOperations {
}
guard let sourcePath = operation.source else { return nil }
let destPath = operation.destination
var request = URLRequest(url: URL(string: sourcePath, relativeTo: driveURL)!)
var request = URLRequest(url: url(of: sourcePath))
switch operation {
case .create:
request.httpMethod = "CREATE"
@@ -205,7 +268,7 @@ extension OneDriveFileProvider: FileProviderOperations {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
requestDictionary["parentReference"] = ("/drive/\(drive):" + dest.deletingLastPathComponent) as NSString
requestDictionary["name"] = dest.lastPathComponent as NSString
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
request.httpBody = Data(jsonDictionary: requestDictionary)
}
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderOneDriveError?
@@ -220,7 +283,7 @@ extension OneDriveFileProvider: FileProviderOperations {
return RemoteOperationHandle(operationType: operation, tasks: [task])
}
public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -228,13 +291,12 @@ extension OneDriveFileProvider: FileProviderOperations {
return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
}
public func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = URL(string: escaped(path: path) + ":/content", relativeTo: driveURL)!
var request = URLRequest(url: url)
var request = URLRequest(url: self.url(of: path, modifier: "content"))
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
let task = session.downloadTask(with: request, completionHandler: { (cacheURL, response, error) in
@@ -259,7 +321,7 @@ extension OneDriveFileProvider: FileProviderOperations {
}
extension OneDriveFileProvider: FileProviderReadWrite {
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
@@ -268,8 +330,7 @@ extension OneDriveFileProvider: FileProviderReadWrite {
}
let opType = FileOperationType.fetch(path: path)
let url = URL(string: escaped(path: path) + ":/content", relativeTo: driveURL)!
var request = URLRequest(url: url)
var request = URLRequest(url: self.url(of: path, modifier: "content"))
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
if length > 0 {
@@ -290,7 +351,7 @@ extension OneDriveFileProvider: FileProviderReadWrite {
return RemoteOperationHandle(operationType: opType, tasks: [task])
}
public func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -299,16 +360,6 @@ extension OneDriveFileProvider: FileProviderReadWrite {
return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
}
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
var foundFiles = [OneDriveFileObject]()
search(path, query: query, foundItem: { (file) in
foundFiles.append(file)
foundItemHandler?(file)
}, completionHandler: { (error) in
completionHandler(foundFiles, error)
})
}
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
/* There is two ways to monitor folders changing in OneDrive. Either using webooks
* which means you have to implement a server to translate it to push notifications
@@ -327,25 +378,24 @@ extension OneDriveFileProvider: FileProviderReadWrite {
- Parameters:
- to: path of file, including file/directory name.
- completionHandler: a block with result of directory entries or error.
- completionHandler: a closure with result of directory entries or error.
`link`: a url returned by OneDrive to share.
`attribute`: `nil` for OneDrive.
`expiration`: `nil` for OneDrive, as it doesn't expires.
`error`: Error returned by OneDrive.
*/
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: OneDriveFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
let url = URL(string: escaped(path: path) + ":/action.createLink", relativeTo: driveURL)!
var request = URLRequest(url: url)
var request = URLRequest(url: self.url(of: path, modifier: "action.createLink"))
request.httpMethod = "POST"
let requestDictionary: [String: AnyObject] = ["type": "view" as NSString]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderOneDriveError?
var link: URL?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
if let json = data?.deserializeJSON() {
if let linkDic = json["link"] as? NSDictionary, let linkStr = linkDic["webUrl"] as? String {
link = URL(string: linkStr)
}
@@ -360,11 +410,11 @@ extension OneDriveFileProvider: FileProviderReadWrite {
extension OneDriveFileProvider: ExtendedFileProvider {
public func thumbnailOfFileSupported(path: String) -> Bool {
open func thumbnailOfFileSupported(path: String) -> Bool {
return true
}
public func propertiesOfFileSupported(path: String) -> Bool {
open func propertiesOfFileSupported(path: String) -> Bool {
let fileExt = (path as NSString).pathExtension.lowercased()
switch fileExt {
case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff":
@@ -378,12 +428,12 @@ extension OneDriveFileProvider: ExtendedFileProvider {
}
}
public func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
let url: URL
if let dimension = dimension {
url = URL(string: escaped(path: path) + ":/thumbnails/0/=c\(dimension.width)x\(dimension.height)/content", relativeTo: driveURL)!
url = self.url(of: path, modifier: "thumbnails/0/=c\(dimension.width)x\(dimension.height)/content")
} else {
url = URL(string: escaped(path: path) + ":/thumbnails/0/small/content", relativeTo: driveURL)!
url = self.url(of: path, modifier: "thumbnails/0/small/content")
}
var request = URLRequest(url: url)
@@ -403,9 +453,8 @@ extension OneDriveFileProvider: ExtendedFileProvider {
task.resume()
}
public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
let url = URL(string: escaped(path: path), relativeTo: driveURL)!
var request = URLRequest(url: url)
open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
var request = URLRequest(url: url(of: path))
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
@@ -415,7 +464,7 @@ extension OneDriveFileProvider: ExtendedFileProvider {
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
if let json = data?.deserializeJSON() {
(dic, keys) = self.mapMediaInfo(json)
}
}
@@ -425,14 +474,4 @@ extension OneDriveFileProvider: ExtendedFileProvider {
}
}
extension OneDriveFileProvider: FileProvider {
open func copy(with zone: NSZone? = nil) -> Any {
let copy = OneDriveFileProvider(credential: self.credential, serverURL: self.baseURL, drive: self.drive, cache: self.cache)
copy.currentPath = self.currentPath
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
copy.useCache = self.useCache
copy.validatingCache = self.validatingCache
return copy
}
}
extension OneDriveFileProvider: FileProvider { }
+21 -32
View File
@@ -27,14 +27,16 @@ public final class OneDriveFileObject: FileObject {
}
internal convenience init? (baseURL: URL?, drive: String, jsonStr: String) {
guard let json = jsonToDictionary(jsonStr) else { return nil }
guard let json = jsonStr.deserializeJSON() else { return nil }
self.init(baseURL: baseURL, drive: drive, json: json)
}
internal convenience init? (baseURL: URL?, drive: String, json: [String: AnyObject]) {
guard let name = json["name"] as? String else { return nil }
guard let path = (json["parentReference"] as? NSDictionary)?["path"] as? String else { return nil }
let lPath = path.replacingOccurrences(of: "/drive/\(drive):", with: "/", options: .anchored, range: nil)
var lPath = path.replacingOccurrences(of: "/drive/\(drive)", with: "/", options: .anchored, range: nil)
lPath = lPath.replacingOccurrences(of: "/:", with: "", options: .anchored)
lPath = lPath.replacingOccurrences(of: "//", with: "", options: .anchored)
self.init(baseURL: baseURL, name: name, path: lPath)
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
self.modifiedDate = resolve(dateString: json["lastModifiedDateTime"] as? String ?? "")
@@ -79,13 +81,8 @@ public final class OneDriveFileObject: FileObject {
// codebeat:disable[ARITY]
internal extension OneDriveFileProvider {
func list(_ path: String, cursor: String? = nil, prevContents: [OneDriveFileObject] = [], completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
let url: URL
if let cursor = cursor {
url = URL(string: cursor)!
} else {
url = URL(string: escaped(path: path), relativeTo: driveURL)!
}
func list(_ path: String, cursor: URL? = nil, prevContents: [OneDriveFileObject] = [], completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
let url = cursor ?? self.url(of: path, modifier: "children")
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
@@ -95,15 +92,14 @@ internal extension OneDriveFileProvider {
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderOneDriveError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
if let data = data, let jsonStr = String(data: data, encoding: .utf8) {
let json = jsonToDictionary(jsonStr)
if let entries = json?["value"] as? [AnyObject] , entries.count > 0 {
if let json = data?.deserializeJSON() {
if let entries = json["value"] as? [AnyObject] , entries.count > 0 {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: entry) {
files.append(file)
}
}
let ncursor = json?["@odata.nextLink"] as? String
let ncursor: URL? = (json["@odata.nextLink"] as? String).flatMap { URL(string: $0) }
let hasmore = ncursor != nil
if hasmore {
self.list(path, cursor: ncursor, prevContents: files, completionHandler: completionHandler)
@@ -125,7 +121,7 @@ internal extension OneDriveFileProvider {
return nil
}
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
let url = URL(string: escaped(path: targetPath) + ":/content" + queryStr, relativeTo: driveURL)!
let url = self.url(of: targetPath, modifier: "content\(queryStr)")
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
@@ -137,7 +133,7 @@ internal extension OneDriveFileProvider {
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
completionHandler?(responseError ?? error)
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
self.delegateNotify(operation, error: responseError ?? error)
})
task.taskDescription = operation.json
task.resume()
@@ -153,7 +149,7 @@ internal extension OneDriveFileProvider {
return nil
}
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
let url = URL(string: escaped(path: targetPath) + ":/content" + queryStr, relativeTo: driveURL)!
let url = self.url(of: targetPath, modifier: "content\(queryStr)")
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
@@ -164,22 +160,17 @@ internal extension OneDriveFileProvider {
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
completionHandler?(responseError ?? error)
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
self.delegateNotify(operation, error: responseError ?? error)
})
task.taskDescription = operation.json
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
}
func search(_ startPath: String = "", query: String, next: String? = nil, foundItem:@escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
func search(_ startPath: String = "", query: String, next: URL? = nil, foundItem:@escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
let url: URL
if let next = next {
url = URL(string: next)!
} else if self.escaped(path: startPath) == "" {
url = URL(string: "/drive/\(drive)/view.search?q=\(query)", relativeTo: baseURL)!
} else {
url = URL(string: "\(escaped(path: startPath))/view.search?q=\(query)", relativeTo: driveURL)!
}
let q = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
url = next ?? self.url(of: startPath, modifier: "view.search?q=\(q)")
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
@@ -189,17 +180,15 @@ internal extension OneDriveFileProvider {
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderOneDriveError(code: rCode, path: startPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
if let data = data, let jsonStr = String(data: data, encoding: .utf8) {
let json = jsonToDictionary(jsonStr)
if let entries = json?["value"] as? [AnyObject] , entries.count > 0 {
if let json = data?.deserializeJSON() {
if let entries = json["value"] as? [AnyObject] , entries.count > 0 {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: entry) {
foundItem(file)
}
}
let next = json?["@odata.nextLink"] as? String
let hasmore = next != nil
if hasmore, let next = next {
let next: URL? = (json["@odata.nextLink"] as? String).flatMap { URL(string: $0) }
if let next = next {
self.search(startPath, query: query, next: next, foundItem: foundItem, completionHandler: completionHandler)
} else {
completionHandler(responseError ?? error)
@@ -256,7 +245,7 @@ internal extension OneDriveFileProvider {
add(key: "Location", value: "\(latStr), \(longStr)")
}
if let parent = json["image"] as? [String: Any] ?? json["video"] as? [String: Any], let duration = parent["duration"] as? UInt64 {
add(key: "Duration", value: OneDriveFileProvider.formatshort(interval: TimeInterval(duration) / 1000))
add(key: "Duration", value: (TimeInterval(duration) / 1000).formatshort)
}
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = resolve(dateString: timeTakenStr) {
OneDriveFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
+28 -39
View File
@@ -8,7 +8,7 @@
import Foundation
/// Allows to get progress or cancel an in-progress operation, for remote -URLSession based- providers.
/// Allows to get progress or cancel an in-progress operation, for remote, `URLSession` based providers.
open class RemoteOperationHandle: OperationHandle {
internal var tasks: [Weak<URLSessionTask>]
@@ -43,7 +43,7 @@ open class RemoteOperationHandle: OperationHandle {
if let task = $1.value as? URLSessionUploadTask {
return $0 + task.countOfBytesExpectedToSend
} else {
return $0 + ($1.value?.countOfBytesExpectedToSend ?? 0)
return $0 + ($1.value?.countOfBytesExpectedToReceive ?? 0)
}
}
}
@@ -71,26 +71,28 @@ public protocol FileProviderHTTPError: Error, CustomStringConvertible {
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
}
public var localizedDescription: String {
return description
}
}
class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDelegate {
weak var fileProvider: FileProvider?
weak var fileProvider: (FileProviderBasicRemote & FileProviderOperations)?
var credential: URLCredential?
var finishDownloadHandler: ((_ session: Foundation.URLSession, _ downloadTask: URLSessionDownloadTask, _ didFinishDownloadingToURL: URL) -> Void)?
var didSendDataHandler: ((_ session: Foundation.URLSession, _ task: URLSessionTask, _ bytesSent: Int64, _ totalBytesSent: Int64, _ totalBytesExpectedToSend: Int64) -> Void)?
var didReceivedData: ((_ session: Foundation.URLSession, _ downloadTask: URLSessionDownloadTask, _ bytesWritten: Int64, _ totalBytesWritten: Int64, _ totalBytesExpectedToWrite: Int64) -> Void)?
init(fileProvider: FileProvider, credential: URLCredential?) {
init(fileProvider: FileProviderBasicRemote & FileProviderOperations, credential: URLCredential?) {
self.fileProvider = fileProvider
self.credential = credential
}
@@ -104,57 +106,44 @@ class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDeleg
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
self.didSendDataHandler?(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
guard let desc = task.taskDescription, let json = jsonToDictionary(desc) else {
return
}
guard let type = json["type"] as? String, let source = json["source"] as? String else {
return
}
let dest = json["dest"] as? String
let op : FileOperationType
switch type {
case "Create":
op = .create(path: source)
case "Copy":
guard let dest = dest else { return }
op = .copy(source: source, destination: dest)
case "Move":
guard let dest = dest else { return }
op = .move(source: source, destination: dest)
case "Modify":
op = .modify(path: source)
case "Remove":
op = .remove(path: source)
case "Link":
guard let dest = dest else { return }
op = .link(link: source, target: dest)
default:
guard let json = task.taskDescription?.deserializeJSON(),
let op = FileOperationType(json: json), let fileProvider = fileProvider else {
return
}
let progress = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
fileProvider?.delegate?.fileproviderProgress(fileProvider!, operation: op, progress: progress)
fileProvider.delegate?.fileproviderProgress(fileProvider, operation: op, progress: progress)
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
self.didReceivedData?(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
guard let desc = downloadTask.taskDescription, let json = jsonToDictionary(desc), let source = json["source"] as? String, let dest = json["dest"] as? String else {
guard let json = downloadTask.taskDescription?.deserializeJSON(),
let op = FileOperationType(json: json), let fileProvider = fileProvider else {
return
}
fileProvider?.delegate?.fileproviderProgress(fileProvider!, operation: .copy(source: source, destination: dest), progress: Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
fileProvider.delegate?.fileproviderProgress(fileProvider, operation: op, progress: Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
}
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
let deposition: Foundation.URLSession.AuthChallengeDisposition = credential != nil ? .useCredential : .performDefaultHandling
completionHandler(deposition, credential)
authenticate(didReceive: challenge, completionHandler: completionHandler)
}
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
let deposition: Foundation.URLSession.AuthChallengeDisposition = credential != nil ? .useCredential : .performDefaultHandling
completionHandler(deposition, credential)
authenticate(didReceive: challenge, completionHandler: completionHandler)
}
func authenticate(didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
switch (challenge.previousFailureCount, credential != nil) {
case (0...1, true):
completionHandler(.useCredential, credential)
case (0, false):
completionHandler(.useCredential, challenge.proposedCredential)
default:
completionHandler(.performDefaultHandling, nil)
}
}
}
@@ -309,7 +298,7 @@ public enum FileProviderHTTPErrorCode: Int, CustomStringConvertible {
case 300...399: return "Redirection"
case 400...499: return "Client Error"
case 500...599: return "Server Error"
default: return "Server Error"
default: return "Unknown Error"
}
}
}
+1 -4
View File
@@ -68,11 +68,8 @@ class SMB2ProtocolClient: FPSStreamTask {
}
let mId = messageId()
let smbHeader = SMB2.Header(command: .TREE_CONNECT, creditRequestResponse: 123, messageId: mId, treeId: 0, sessionId: sessionId)
var share = ""
let cmp = url.pathComponents
if cmp.count > 0 {
share = cmp[0]
}
let share = cmp.first ?? ""
let tcHeader = SMB2.TreeConnectRequest.Header(flags: [])
let msg = SMB2.TreeConnectRequest(header: tcHeader, host: host, share: share)
let data = createSMB2Message(header: smbHeader, message: msg!)
+23 -5
View File
@@ -10,7 +10,6 @@ import Foundation
class SMBFileProvider: FileProvider, FileProviderMonitor {
open class var type: String { return "SMB" }
open var isPathRelative: Bool = true
open var baseURL: URL?
open var currentPath: String = ""
open var dispatch_queue: DispatchQueue
@@ -20,7 +19,7 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
public typealias FileObjectClass = FileObject
public init? (baseURL: URL, credential: URLCredential, afterInitialized: SimpleCompletionHandler) {
public init? (baseURL: URL, credential: URLCredential?) {
guard baseURL.uw_scheme.lowercased() == "smb" else {
return nil
}
@@ -31,7 +30,26 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
self.credential = credential
}
public required convenience init?(coder aDecoder: NSCoder) {
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else {
return nil
}
self.init(baseURL: baseURL,
credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
}
open func encode(with aCoder: NSCoder) {
aCoder.encode(self.baseURL, forKey: "baseURL")
aCoder.encode(self.credential, forKey: "credential")
aCoder.encode(self.currentPath, forKey: "currentPath")
}
public static var supportsSecureCoding: Bool {
return true
}
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObjectClass], _ error: Error?) -> Void)) {
NotImplemented()
}
@@ -100,7 +118,7 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
return nil
}
open func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) {
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) {
NotImplemented()
}
@@ -117,7 +135,7 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
}
open func copy(with zone: NSZone? = nil) -> Any {
let copy = SMBFileProvider(baseURL: self.baseURL!, credential: self.credential!, afterInitialized: { _ in })!
let copy = SMBFileProvider(baseURL: self.baseURL!, credential: self.credential!)!
copy.currentPath = self.currentPath
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
-29
View File
@@ -66,32 +66,3 @@ struct SMBTime {
return Date(timeIntervalSince1970: Double(self.time) / 10000000 - 11644473600)
}
}
extension Data {
init<T>(value: T) {
var value = value
self = Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
}
func scanValue<T>() -> T? {
guard MemoryLayout<T>.size <= self.count else { return nil }
return self.withUnsafeBytes { $0.pointee }
}
func scanValue<T>(start: Int) -> T? {
let length = MemoryLayout<T>.size
guard self.count >= start + length else { return nil }
return self.subdata(in: start..<start+length).withUnsafeBytes { $0.pointee }
}
func scanString(start: Int = 0, length: Int, encoding: String.Encoding) -> String? {
guard self.count >= start + length else { return nil }
return String(data: self.subdata(in: start..<start+length), encoding: encoding)
}
static func mapMemory<T, U>(from: T) -> U? {
guard MemoryLayout<T>.size >= MemoryLayout<U>.size else { return nil }
let data = Data(value: from)
return data.scanValue()
}
}
+1 -1
View File
@@ -243,7 +243,7 @@ extension SMB2 {
if data.count < offset + 48 {
return nil
}
let datestring = data.scanString(start: offset, length: 48, encoding: .utf16)
let datestring = data.scanString(start: offset, length: 48, using: .utf16)
if let datestring = datestring, let date = dateFormatter.date(from: datestring) {
snapshots.append(SMBTime(date: date))
}
+1 -1
View File
@@ -92,7 +92,7 @@ extension SMB2 {
}
let fileNameLen = Int(data.scanValue(start: offset + 8) as UInt32? ?? 0)
let fileName = data.scanString(start: offset + 12, length: fileNameLen, encoding: .utf16) ?? ""
let fileName = data.scanString(start: offset + 12, length: fileNameLen, using: .utf16) ?? ""
result.append((action: action, fileName: fileName))
offset += Int(nextOffset)
+6 -6
View File
@@ -85,7 +85,7 @@ extension SMB2 {
return []
}
let headersize = MemoryLayout.size(ofValue: header)
let fileName = buffer.scanString(start: headersize, length: Int(header.fileNameLength), encoding: .utf16) ?? ""
let fileName = buffer.scanString(start: headersize, length: Int(header.fileNameLength), using: .utf16) ?? ""
result.append((header: header, fileName: fileName))
if header.nextEntryOffset == 0 {
break
@@ -216,12 +216,12 @@ extension SMB2 {
var asAllInformation: (header: FileAllInformationHeader, name: String) {
let header: FileAllInformationHeader = buffer.scanValue()!
let headersize = MemoryLayout<FileAllInformationHeader>.size
let name = buffer.scanString(start: headersize, length: Int(header.nameLength), encoding: .utf16) ?? ""
let name = buffer.scanString(start: headersize, length: Int(header.nameLength), using: .utf16) ?? ""
return (header, name)
}
var asAlternateNameInformation: String {
return buffer.scanString(start: 0, length: buffer.count, encoding: .utf16) ?? ""
return buffer.scanString(start: 0, length: buffer.count, using: .utf16) ?? ""
}
var asAttributeTagInformation: FileAttributeTagInformation {
@@ -280,14 +280,14 @@ extension SMB2 {
var asStreamInformation: (header: FileStreamInformationHeader, name: String) {
let header: FileStreamInformationHeader = buffer.scanValue()!
let headersize = MemoryLayout<FileStreamInformationHeader>.size
let name = buffer.scanString(start: headersize, length: Int(header.streamNameLength), encoding: .utf16) ?? ""
let name = buffer.scanString(start: headersize, length: Int(header.streamNameLength), using: .utf16) ?? ""
return (header, name)
}
var asFsVolumeInformation: (header: FileFsVolumeInformationHeader, name: String) {
let header: FileFsVolumeInformationHeader = buffer.scanValue()!
let headersize = MemoryLayout<FileFsVolumeInformationHeader>.size
let name = buffer.scanString(start: headersize, length: Int(header.labelLength), encoding: .utf16) ?? ""
let name = buffer.scanString(start: headersize, length: Int(header.labelLength), using: .utf16) ?? ""
return (header, name)
}
@@ -302,7 +302,7 @@ extension SMB2 {
var asFsAttributeInformation: (header: FileFsAttributeInformationHeader, name: String) {
let header: FileFsAttributeInformationHeader = buffer.scanValue()!
let headersize = MemoryLayout<FileFsAttributeInformationHeader>.size
let name = buffer.scanString(start: headersize, length: Int(header.nameLength), encoding: .utf16) ?? ""
let name = buffer.scanString(start: headersize, length: Int(header.nameLength), using: .utf16) ?? ""
return (header, name)
}
+97 -74
View File
@@ -13,7 +13,7 @@ import Foundation
set `useCache` and `cache` properties to use Foundation `NSURLCache` system.
WebDAV system supported by many cloud services including [Box.net](https://www.box.com/home)
and [Yandex disk](https://disk.yandex.com).
and [Yandex disk](https://disk.yandex.com) and [ownCloud](https://owncloud.org).
- Important: Because this class uses `URLSession`, it's necessary to disable App Transport Security
in case of using this class with unencrypted HTTP connection.
@@ -21,7 +21,6 @@ import Foundation
*/
open class WebDAVFileProvider: FileProviderBasicRemote {
open class var type: String { return "WebDAV" }
open let isPathRelative: Bool
open let baseURL: URL?
open var currentPath: String
@@ -59,14 +58,13 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
- Parameters:
- baseURL: Location of WebDAV server.
- credential: An `URLCredential` object with `user` and `password`.
- cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used.
- cache: A URLCache to cache downloaded files and contents.
*/
public init? (baseURL: URL, credential: URLCredential?, cache: URLCache? = nil) {
if !["http", "https"].contains(baseURL.uw_scheme.lowercased()) {
return nil
}
self.baseURL = baseURL.path.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
self.isPathRelative = true
self.currentPath = ""
self.useCache = false
self.validatingCache = true
@@ -77,6 +75,39 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
}
public required convenience init?(coder aDecoder: NSCoder) {
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else {
return nil
}
self.init(baseURL: baseURL,
credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
self.useCache = aDecoder.decodeBool(forKey: "useCache")
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
}
open func encode(with aCoder: NSCoder) {
aCoder.encode(self.baseURL, forKey: "baseURL")
aCoder.encode(self.credential, forKey: "credential")
aCoder.encode(self.currentPath, forKey: "currentPath")
aCoder.encode(self.useCache, forKey: "isCoorinating")
aCoder.encode(self.validatingCache, forKey: "undoManager")
}
public static var supportsSecureCoding: Bool {
return true
}
open func copy(with zone: NSZone? = nil) -> Any {
let copy = WebDAVFileProvider(baseURL: self.baseURL!, credential: self.credential, cache: self.cache)!
copy.currentPath = self.currentPath
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
copy.useCache = self.useCache
copy.validatingCache = self.validatingCache
return copy
}
deinit {
if fileProviderCancelTasksOnInvalidating {
_session?.invalidateAndCancel()
@@ -164,6 +195,38 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
})
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PROPFIND"
//request.setValue("1", forHTTPHeaderField: "Depth")
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:allprop/></D:propfind>".data(using: .utf8)
runDataTask(with: request, completionHandler: { (data, response, error) in
// FIXME: paginating results
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
}
if let data = data {
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
var fileObjects = [WebDavFileObject]()
for attr in xresponse {
let fileObject = WebDavFileObject(attr)
if !query.evaluate(with: fileObject.mapPredicate()) {
continue
}
fileObjects.append(fileObject)
foundItemHandler?(fileObject)
}
completionHandler(fileObjects, responseError ?? error)
return
}
completionHandler([], responseError ?? error)
})
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
var request = URLRequest(url: baseURL!)
request.httpMethod = "PROPFIND"
@@ -182,7 +245,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
extension WebDAVFileProvider: FileProviderOperations {
@discardableResult
public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -204,7 +267,7 @@ extension WebDAVFileProvider: FileProviderOperations {
}
@discardableResult
public func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.create(path: (path as NSString).appendingPathComponent(fileName))
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -226,7 +289,7 @@ extension WebDAVFileProvider: FileProviderOperations {
}
@discardableResult
public func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.move(source: path, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -235,7 +298,7 @@ extension WebDAVFileProvider: FileProviderOperations {
}
@discardableResult
public func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -244,7 +307,7 @@ extension WebDAVFileProvider: FileProviderOperations {
}
@discardableResult
public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.remove(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -299,7 +362,7 @@ extension WebDAVFileProvider: FileProviderOperations {
}
@discardableResult
public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -324,7 +387,7 @@ extension WebDAVFileProvider: FileProviderOperations {
}
@discardableResult
public func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -355,7 +418,7 @@ extension WebDAVFileProvider: FileProviderOperations {
extension WebDAVFileProvider: FileProviderReadWrite {
@discardableResult
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
@@ -384,7 +447,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
}
@discardableResult
public func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -417,38 +480,6 @@ extension WebDAVFileProvider: FileProviderReadWrite {
return RemoteOperationHandle(operationType: opType, tasks: [task])
}
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PROPFIND"
//request.setValue("1", forHTTPHeaderField: "Depth")
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:allprop/></D:propfind>".data(using: .utf8)
runDataTask(with: request, completionHandler: { (data, response, error) in
// FIXME: paginating results
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
}
if let data = data {
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
var fileObjects = [WebDavFileObject]()
for attr in xresponse {
let path = attr.href.path
if !((path as NSString).lastPathComponent.contains(query)) {
continue
}
let fileObject = WebDavFileObject(attr)
fileObjects.append(fileObject)
foundItemHandler?(fileObject)
}
completionHandler(fileObjects, responseError ?? error)
return
}
completionHandler([], responseError ?? error)
})
}
/*
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
/* There is no unified api for monitoring WebDAV server content change/update
@@ -465,17 +496,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
// TODO: implements methods for lock mechanism
}
extension WebDAVFileProvider: FileProvider {
open func copy(with zone: NSZone? = nil) -> Any {
let copy = WebDAVFileProvider(baseURL: self.baseURL!, credential: self.credential, cache: self.cache)!
copy.currentPath = self.currentPath
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
copy.useCache = self.useCache
copy.validatingCache = self.validatingCache
return copy
}
}
extension WebDAVFileProvider: FileProvider { }
// MARK: WEBDAV XML response implementation
@@ -526,8 +547,13 @@ struct DavResponse {
guard let hrefString = node[hreftag].value else { return nil }
// trying to figure out relative path out of href
let hrefAbsolute = URL(string: hrefString, relativeTo: baseURL)?.absoluteString ?? hrefString
let relativePath = hrefAbsolute.replacingOccurrences(of: baseURL?.absoluteString ?? "", with: "", options: .anchored, range: nil)
let hrefAbsolute = URL(string: hrefString, relativeTo: baseURL)?.absoluteURL
let relativePath: String
if hrefAbsolute?.host?.replacingOccurrences(of: "www.", with: "", options: .anchored) == baseURL?.host?.replacingOccurrences(of: "www.", with: "", options: .anchored) {
relativePath = hrefAbsolute?.path.replacingOccurrences(of: baseURL?.absoluteURL.path ?? "", with: "", options: .anchored, range: nil) ?? hrefString
} else {
relativePath = hrefAbsolute?.absoluteString.replacingOccurrences(of: baseURL?.absoluteString ?? "", with: "", options: .anchored, range: nil) ?? hrefString
}
let hrefURL = URL(string: removeSlash(relativePath), relativeTo: baseURL) ?? baseURL
guard let href = hrefURL?.standardized else { return nil }
@@ -566,24 +592,21 @@ struct DavResponse {
}
static func parse(xmlResponse: Data, baseURL: URL?) -> [DavResponse] {
guard let xml = try? AEXMLDocument(xml: xmlResponse) else { return [] }
var result = [DavResponse]()
do {
let xml = try AEXMLDocument(xml: xmlResponse)
var rootnode = xml.root
var responsetag = "response"
for node in rootnode.all ?? [] where node.name.lowercased().hasSuffix("multistatus") {
rootnode = node
var rootnode = xml.root
var responsetag = "response"
for node in rootnode.all ?? [] where node.name.lowercased().hasSuffix("multistatus") {
rootnode = node
}
for node in rootnode.children where node.name.lowercased().hasSuffix("response") {
responsetag = node.name
break
}
for responseNode in rootnode[responsetag].all ?? [] {
if let davResponse = DavResponse(responseNode, baseURL: baseURL) {
result.append(davResponse)
}
for node in rootnode.children where node.name.lowercased().hasSuffix("response") {
responsetag = node.name
break
}
for responseNode in rootnode[responsetag].all ?? [] {
if let davResponse = DavResponse(responseNode, baseURL: baseURL) {
result.append(davResponse)
}
}
} catch _ {
}
return result
}