Compare commits

...

18 Commits

Author SHA1 Message Date
Amir Abbas Mousavian 5a7dfa1039 Merge commit 'b84e6ea7abbec6f9a83144d2dcd6744b568d682f'
* commit 'b84e6ea7abbec6f9a83144d2dcd6744b568d682f':
  Updated Date
2017-02-02 13:28:08 +03:30
Amir Abbas Mousavian be7a370a42 Added Documentation, get contents handling in Local provider optimized
- Changed default thumbnail dimension to 64x64 in `LocalFileProvider`
2017-02-02 13:25:40 +03:30
Amir Abbas Mousavian b84e6ea7ab Updated Date 2017-02-02 01:03:48 +03:30
Amir Abbas Mousavian 82b1d2c450 Added new badges 2017-02-01 22:18:20 +03:30
Amir Abbas Mousavian c38ee1ccd3 standardized FileObject.path property
- FileObject.path now has heading slash in all scenarios
- fixes #27, WebDAV fileObject.path was not relative
2017-02-01 21:23:11 +03:30
Amir Abbas Mousavian 1a44df3fd7 Refactoring, better webdav response handling
- Added Dropbox copy from reference method
- refactored `mapToFileObject` methods into `FileObject` initializers
- fixed `requestDictionary` type to `[String: AnyObject]`
2017-02-01 14:08:28 +03:30
Amir Abbas Mousavian eff3725680 Added release version badge 2017-02-01 00:21:48 +03:30
Amir Abbas Mousavian 9368f636d3 Added carthage badge 2017-01-31 23:53:06 +03:30
Amir Abbas Mousavian 3f2fda638f bugfix: path in LocalFileObject is not relative 2017-01-31 23:12:29 +03:30
Amir Abbas Mousavian 6737f98978 Updated travis.yml 2017-01-31 19:07:11 +03:30
Amir Abbas Mousavian 3e55cc60f7 Replaced absoluteURL with relative url (resolved #27), improving performance.
- renamed `DropboxFileProvider.copyItem(path:toRemoteURL:) ` to `DropboxFileProvider.copyItem(remoteURL:toPath:)` due to logic
2017-01-31 17:19:45 +03:30
Alek Slater d7ae709cf7 contentURL was missing the trailing slash causing it to fail for all contentURL calls with the dropbox api (#26) 2017-01-31 13:05:20 +03:30
Amir Abbas Mousavian 4f6e3c7bd5 Uncommented CloudFileProvider decription 2017-01-31 02:11:28 +03:30
Amir Abbas Mousavian b121fe6182 rectified macOS and tvOS version check 2017-01-31 01:42:03 +03:30
Amir Abbas Mousavian 29b7f720f5 Added iCloud provider change monitor and CloudOperationHandle
- fixed copyItem functions for local interoperability
2017-01-31 01:35:38 +03:30
Amir Abbas Mousavian 067e14b76f Added CloudFileProvider and coordination to LocalFileProvider
- changed `operation_queue` type to `OperationQueue`
- added `LocalFileProvider.init(sharedContainerId:)`
- added `destination(ofSymbolicLink:)` to LocalFileProvider
- added expirationDate to `temporaryLink(to:)` methods
- known issue: `CloudFileProvider` monitoring is not implemented
- known issue: `CloudFileProvider` copy from local and to local works imperfect
2017-01-30 18:51:06 +03:30
Amir Abbas Mousavian 09e39ada82 Resolved issue #24, off-by-one bug in Range header 2017-01-30 18:37:25 +03:30
Amir Abbas 6889b302b7 Removed CloudProvider due to incompleteness 2017-01-27 02:47:11 +03:30
17 changed files with 1655 additions and 410 deletions
+15 -10
View File
@@ -1,9 +1,16 @@
language: objective-c
osx_image: xcode8
osx_image: xcode8.2
xcode_project: FileProvider.xcodeproj
env:
global:
- FRAMEWORK_NAME=FileProvider.framework
- PROJECTNAME="FileProvider"
- FRAMEWORK_NAME="FileProvider.framework"
matrix:
- SHCEME="FileProvider OSX" SDK="macosx" ACTION="build"
- SHCEME="FileProvider iOS" SDK="iphonesimulator" ACTION="build"
- SHCEME="FileProvider tvOS" SDK="appletvsimulator" ACTION="build"
before_install:
- gem install xcpretty --no-rdoc --no-ri --no-document --quiet
- brew update
@@ -11,17 +18,15 @@ before_install:
script:
- set pipefail
- xcodebuild -version
- xcodebuild -project FileProvider.xcodeproj -scheme "FileProvider OSX" -sdk macosx | xcpretty
- xcodebuild -project FileProvider.xcodeproj -scheme "FileProvider iOS" -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO | xcpretty
- xcodebuild -project FileProvider.xcodeproj -scheme "FileProvider tvOS" -sdk appletvsimulator ONLY_ACTIVE_ARCH=NO | xcpretty
- pod lib lint --quick
after_success:
- bash <(curl -s https://codecov.io/bash)
- xcodebuild -project $PROJECT.xcodeproj -scheme "$SCHEME" -sdk $SDK $ACTION ONLY_ACTIVE_ARCH=NO | xcpretty
# - pod lib lint --quick
# after_success:
# - bash <(curl -s https://codecov.io/bash)
before_deploy:
- carthage build --no-skip-current
- carthage archive FileProvider
- carthage archive $PROJECTNAME
deploy:
file: FileProvider.framework.zip
file: $PROJECTNAME.framework.zip
skip_cleanup: true
on:
tags: true
+1 -1
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
#
s.name = "FileProvider"
s.version = "0.10.3"
s.version = "0.12.0"
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 -2
View File
@@ -35,6 +35,9 @@
792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; };
792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; };
792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; };
79480FF61E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; };
79480FF71E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; };
79480FF81E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; };
794C21FE1D58912A00EC49B8 /* DropboxHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */; };
794C21FF1D58912A00EC49B8 /* DropboxHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */; };
794C22001D58912A00EC49B8 /* DropboxHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */; };
@@ -127,6 +130,7 @@
7924B1921D89DAE000589DB7 /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FPSStreamTask.swift; sourceTree = "<group>"; };
792572401DF23BDA006A1526 /* LocalHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalHelper.swift; sourceTree = "<group>"; };
79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudFileProvider.swift; sourceTree = "<group>"; };
794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropboxHelper.swift; sourceTree = "<group>"; };
794C22091D5893F800EC49B8 /* SMB2Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2Notification.swift; sourceTree = "<group>"; };
794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2QueryTypes.swift; sourceTree = "<group>"; };
@@ -286,6 +290,7 @@
79F5745A1DFDB10A00179ABF /* FileObject.swift */,
799396961D48C02300086753 /* LocalFileProvider.swift */,
792572401DF23BDA006A1526 /* LocalHelper.swift */,
79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */,
79BD638B1E2CC2300035128C /* ExtendedLocalFileProvider.swift */,
7902C0851D61B56D00564440 /* RemoteSession.swift */,
799396931D48C02300086753 /* DropboxFileProvider.swift */,
@@ -480,6 +485,7 @@
799396D41D48C02300086753 /* SMB2Tree.swift in Sources */,
7924B1A21D89DAE000589DB7 /* Options.swift in Sources */,
792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */,
79480FF61E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
79F5745B1DFDB10B00179ABF /* FileObject.swift in Sources */,
79BD63C51E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
7924B1991D89DAE000589DB7 /* Element.swift in Sources */,
@@ -519,6 +525,7 @@
799396D51D48C02300086753 /* SMB2Tree.swift in Sources */,
7924B1A31D89DAE000589DB7 /* Options.swift in Sources */,
792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */,
79480FF71E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
79F5745C1DFDB10B00179ABF /* FileObject.swift in Sources */,
79BD63C61E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
7924B1B01D89F7DE00589DB7 /* FPSStreamTask.swift in Sources */,
@@ -558,6 +565,7 @@
799396D61D48C02300086753 /* SMB2Tree.swift in Sources */,
7924B1A41D89DAE000589DB7 /* Options.swift in Sources */,
792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */,
79480FF81E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
79F5745D1DFDB10B00179ABF /* FileObject.swift in Sources */,
79BD63C71E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
7924B1B11D89F7DF00589DB7 /* FPSStreamTask.swift in Sources */,
@@ -595,7 +603,7 @@
799396601D48B7BF00086753 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.10.3;
BUNDLE_VERSION_STRING = 0.12.0;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -625,7 +633,7 @@
799396611D48B7BF00086753 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.10.3;
BUNDLE_VERSION_STRING = 0.12.0;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2016 Amir Abbas Mousavian
Copyright (c) 2016-17 Amir Abbas Mousavian
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+33 -12
View File
@@ -3,17 +3,21 @@
>This Swift library provide a swifty way to deal with local and remote files and directories in a unified way.
[![Swift Version][swift-image]][swift-url]
[![Platform][platform-image]](#)
[![License][license-image]][license-url]
[![Platform](https://img.shields.io/badge/Platform-iOS%2C%20macOS%2C%20tvOS-lightgray.svg)]()
[![Release versin][release-image]][release-url]
[![CocoaPods version](https://img.shields.io/cocoapods/v/FileProvider.svg)][cocoapods]
[![Carthage compatible][carthage-image]](https://github.com/Carthage/Carthage)
[![Build Status][travis-image]][travis-url]
[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/FileProvider.svg)](https://cocoapods.org/pods/FileProvider)
[![codebeat badge][codebeat-image]][codebeat-url]
[![Codebeat Badge][codebeat-image]][codebeat-url]
[![Cocoapods Docs][docs-image]](docs-url)
[![Cocoapods Downloads][cocoapods-downloads]][cocoapods]
[![Cocoapods Apps][cocoapods-apps]][cocoapods]
<!---
[![codecov](https://codecov.io/gh/amosavian/FileProvider/branch/master/graph/badge.svg)](https://codecov.io/gh/amosavian/FileProvider)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![codecov](https://codecov.io/gh/amosavian/FileProvider/branch/master/graph/badge.svg)](https://codecov.io/gh/amosavian/FileProvider)
--->
This library provides implementaion of WebDav, Dropbox, OneDrive and SMB2 (incomplete) and local files.
@@ -27,13 +31,13 @@ Local and WebDAV providers are fully tested and can be used in production enviro
- [x] **LocalFileProvider** a wrapper around `FileManager` with some additions like searching and reading a portion of file.
- [x] **WebDAVFileProvider** WebDAV protocol is defacto file transmission standard, replaced FTP.
- [x] **DropboxFileProvider** A wrapper around Dropbox Web API.
* For now it has limitation in uploading files up to 150MB.
* For now it has limitation in uploading files up to 150MB.
- [x] **OneDriveFileProvider** A wrapper around OneDrive Web API, works with `onedrive.com` and compatible (business) servers.
* For now it has limitation in uploading files up to 100MB.
- [x] **CloudFileProvider** A wrapper around app's ubiquitous container to iCloud Drive in iOS 8+ API.
- [ ] **SMBFileProvider** SMB2/3 introduced in 2006, which is a file and printer sharing protocol originated from Microsoft Windows and now is replacing AFP protocol on MacOS.
* Data types and some basic functions are implemented but *main interface is not implemented yet!*
* SMB1/CIFS is depericated and very tricky to be implemented
* Data types and some basic functions are implemented but *main interface is not implemented yet!*
* SMB1/CIFS is depericated and very tricky to be implemented
- [ ] **FTPFileProvider** while deprecated in 1990s, it's still in use on some Web hosts.
- [ ] **AmazonS3FileProvider**
- [ ] **GoogleDriveFileProvider**
@@ -112,6 +116,13 @@ let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDo
let documentsProvider = LocalFileProvider(baseURL: documentsURL)
```
Also for using group shared container:
```swift
let documentsProvider = LocalFileProvider(sharedContainerId: "group.yourcompany.appContainer")
// Replace your group identifier with string above
```
You can't change the base url later. and all paths are related to this base url by default.
To initialize an iCloud Container provider use below code, This will automatically manager creating Documents folder in container:
@@ -120,6 +131,7 @@ To initialize an iCloud Container provider use below code, This will automatical
let documentsProvider = CloudFileProvider(containerId: nil)
```
For remote file providers authentication may be necessary:
``` swift
@@ -396,11 +408,20 @@ Distributed under the MIT license. See `LICENSE` for more information.
[https://github.com/amosavian/](https://github.com/amosavian/)
[swift-image]:https://img.shields.io/badge/swift-3.0-orange.svg
[cocoapods]: https://cocoapods.org/pods/FileProvider
[swift-image]: https://img.shields.io/badge/swift-3.0-orange.svg
[swift-url]: https://swift.org/
[license-image]: https://img.shields.io/badge/License-MIT-blue.svg
[platform-image]: https://img.shields.io/cocoapods/p/FileProvider.svg
[license-image]: https://img.shields.io/github/license/amosavian/FileProvider.svg
[license-url]: LICENSE
[codebeat-image]: https://codebeat.co/badges/7b359f48-78eb-4647-ab22-56262a827517
[codebeat-url]: https://codebeat.co/projects/github-com-amosavian-fileprovider
[travis-image]: https://img.shields.io/travis/amosavian/FileProvider/master.svg
[travis-url]: https://travis-ci.org/amosavian/FileProvider
[travis-url]: https://travis-ci.org/amosavian/FileProvider
[release-url]: https://github.com/amosavian/FileProvider/releases
[release-image]: https://img.shields.io/github/release/amosavian/FileProvider.svg
[carthage-image]: https://img.shields.io/badge/Carthage-compatible-4BC51D.svg
[cocoapods-downloads]: https://img.shields.io/cocoapods/dt/FileProvider.svg
[cocoapods-apps]: https://img.shields.io/cocoapods/at/FileProvider.svg
[docs-image]: https://img.shields.io/cocoapods/metrics/doc-percent/FileProvider.svg
[docs-url]: http://cocoadocs.org/docsets/FileProvider/
+483
View File
@@ -0,0 +1,483 @@
//
// CloudFileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2017 Mousavian. Distributed under MIT license.
//
import Foundation
open class CloudFileProvider: LocalFileProvider {
public var type: String {
return "iCloudDrive"
}
/// Actually is readonly, value is true
override open var isCoorinating: Bool {
get {
return true
}
set {
return
}
}
open var containerId: String?
public init? (containerId: String?) {
assert(!Thread.isMainThread, "LocalFileProvider.init(containerId:) is not recommended to be executed on Main Thread.")
guard FileManager.default.ubiquityIdentityToken == nil else {
return nil
}
guard let ubiquityURL = FileManager.default.url(forUbiquityContainerIdentifier: containerId) else {
return nil
}
self.containerId = containerId
let baseURL = ubiquityURL.standardized.appendingPathComponent("Documents/")
super.init(baseURL: baseURL)
self.isCoorinating = true
dispatch_queue = DispatchQueue(label: "FileProvider.\(self.type)", attributes: DispatchQueue.Attributes.concurrent)
operation_queue = OperationQueue()
operation_queue.name = "FileProvider.\(self.type).Operation"
fileManager.url(forUbiquityContainerIdentifier: containerId)
opFileManager.url(forUbiquityContainerIdentifier: containerId)
try? fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true)
}
open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
dispatch_queue.async {
let pathURL = self.url(of: path)
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K BEGINSWITH %@", NSMetadataItemPathKey, pathURL.path)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.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, url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
continue
}
if let file = self.mapFileObject(attributes: attribs) {
contents.append(file)
}
}
completionHandler(contents, nil)
})
query.start()
}
}
/// iCloud Storage size and free space is unavailable, it returns local space
open override func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) {
super.storageProperties(completionHandler: completionHandler)
}
open override func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
dispatch_queue.async {
let pathURL = self.url(of: path)
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemPathKey, pathURL.path)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
defer {
query.stop()
NotificationCenter.default.removeObserver(finishObserver!)
}
query.disableUpdates()
guard let result = (query.results as? [NSMetadataItem])?.first, let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
let error = self.throwError(path, code: CocoaError.fileNoSuchFile)
completionHandler(nil, error)
return
}
if let file = self.mapFileObject(attributes: attribs) {
completionHandler(file, nil)
} else {
let noFileError = self.throwError(path, code: CocoaError.fileNoSuchFile)
completionHandler(nil, noFileError)
}
})
query.start()
}
}
@discardableResult
open override func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let r = super.create(folder: folderName, at: atPath, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
@discardableResult
open override func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let r = super.create(file: fileName, at: atPath, contents: data, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
@discardableResult
open override func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let r = super.moveItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
@discardableResult
open override func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let r = super.copyItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
@discardableResult
open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let r = super.removeItem(path: path, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
@discardableResult
open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
// TODO: Make use of overwrite parameter
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
operation_queue.addOperation {
let tempFolder: URL
if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) {
tempFolder = FileManager.default.temporaryDirectory
} else {
tempFolder = URL(fileURLWithPath: NSTemporaryDirectory())
}
let tmpFile = tempFolder.appendingPathComponent(UUID().uuidString)
do {
try self.opFileManager.copyItem(at: localFile, to: tmpFile)
let toUrl = self.url(of: toPath)
try self.opFileManager.setUbiquitous(true, itemAt: tmpFile, destinationURL: toUrl)
completionHandler?(nil)
DispatchQueue.main.async(execute: {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
} catch let e {
if self.opFileManager.fileExists(atPath: tmpFile.path) {
try? self.opFileManager.removeItem(at: tmpFile)
}
completionHandler?(e)
DispatchQueue.main.async(execute: {
self.delegate?.fileproviderFailed(self, operation: opType)
})
}
}
return CloudOperationHandle(operationType: opType, baseURL: self.baseURL)
}
@discardableResult
open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
do {
try self.opFileManager.startDownloadingUbiquitousItem(at: self.url(of: path))
} catch let e {
completionHandler?(e)
DispatchQueue.main.async(execute: {
self.delegate?.fileproviderFailed(self, operation: opType)
})
return nil
}
let r = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
@discardableResult
open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
let r = super.contents(path: path, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
@discardableResult
open override func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
let r = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
@discardableResult
open override func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let r = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
open override func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
dispatch_queue.async {
let pathURL = self.url(of: path)
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "(%K BEGINSWITH %@) && (%K LIKE %@)", NSMetadataItemPathKey, pathURL.path, NSMetadataItemFSNameKey, query)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
var lastReportedCount = 0
if let foundItemHandler = foundItemHandler {
var updateObserver: NSObjectProtocol?
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryGatheringProgress, object: query, queue: nil, using: { (notification) in
query.disableUpdates()
guard query.resultCount > lastReportedCount else { return }
for index in lastReportedCount..<query.resultCount {
guard let attribs = (query.result(at: index) as? NSMetadataItem)?.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
continue
}
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
continue
}
if let file = self.mapFileObject(attributes: attribs) {
foundItemHandler(file)
}
}
lastReportedCount = query.resultCount
query.enableUpdates()
})
}
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
defer {
query.stop()
NotificationCenter.default.removeObserver(finishObserver!)
}
guard let results = query.results as? [NSMetadataItem] else {
return
}
query.disableUpdates()
var contents = [FileObject]()
for result in results {
guard let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
continue
}
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
continue
}
if let file = self.mapFileObject(attributes: attribs) {
contents.append(file)
}
}
completionHandler(contents, nil)
})
query.start()
}
}
fileprivate var monitors = [URL: (NSMetadataQuery, NSObjectProtocol)]()
//
open override func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
self.unregisterNotifcation(path: path)
let pathURL = self.url(of: path)
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "(%K BEGINSWITH %@)", NSMetadataItemPathKey, pathURL.path)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
let updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: nil, using: { (notification) in
query.disableUpdates()
eventHandler()
query.enableUpdates()
})
query.start()
monitors[pathURL] = (query, updateObserver)
}
open override func unregisterNotifcation(path: String) {
let key = url(of: path)
guard let (query, observer) = monitors[key] else {
return
}
query.disableUpdates()
query.stop()
monitors.removeValue(forKey: key)
NotificationCenter.default.removeObserver(observer)
}
open override func isRegisteredForNotification(path: String) -> Bool {
return monitors[url(of: path)] != nil
}
/// may return 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)?.standardized, let name = attribs[NSMetadataItemFSNameKey] as? String else {
return nil
}
let path = self.relativePathOf(url: url)
let rpath = path.hasPrefix("/") ? path.substring(from: path.index(after: path.startIndex)) : path
let relativeUrl = URL(string: rpath, relativeTo: self.baseURL)
let file = FileObject(url: relativeUrl ?? url, name: name, path: path)
file.size = (attribs[NSMetadataItemFSSizeKey] as? NSNumber)?.int64Value ?? -1
file.creationDate = attribs[NSMetadataItemFSCreationDateKey] as? Date
file.modifiedDate = attribs[NSMetadataItemFSContentChangeDateKey] as? Date
let isFolder = (attribs[NSMetadataItemContentTypeTreeKey] as? [String])?.contains("public.folder") ?? false
let isSymbolic = (attribs[NSMetadataItemContentTypeTreeKey] as? [String])?.contains("public.symlink") ?? false
file.type = isFolder ? .directory : (isSymbolic ? .symbolicLink : .regular)
return file
}
/// Removes local copy of file, but spares cloud copy/
/// - Parameter path: Path of file or directory to be remoed from local
/// - Parameter completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
open func evictItem(path: String, completionHandler: SimpleCompletionHandler) {
operation_queue.addOperation {
do {
try self.opFileManager.evictUbiquitousItem(at: self.url(of: path))
completionHandler?(nil)
} catch let e {
completionHandler?(e)
}
}
}
/// Returns a pulic url with expiration date, can be shared with other people.
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
operation_queue.addOperation {
do {
var expiration: NSDate?
let url = try self.opFileManager.url(forPublishingUbiquitousItemAt: self.url(of: path), expiration: &expiration)
completionHandler(url, nil, expiration as Date?, nil)
} catch let e {
completionHandler(nil, nil, nil, e)
}
}
}
}
open class CloudOperationHandle: OperationHandle {
public let baseURL: URL?
public let operationType: FileOperationType
init (operationType: FileOperationType, baseURL: URL?) {
self.baseURL = baseURL
self.operationType = operationType
}
private var sourceURL: URL? {
guard let source = operationType.source, let baseURL = baseURL else { return nil }
return source.hasPrefix("file://") ? URL(fileURLWithPath: source) : baseURL.appendingPathComponent(source)
}
private var destURL: URL? {
guard let dest = operationType.destination, let baseURL = baseURL else { return nil }
return dest.hasPrefix("file://") ? URL(fileURLWithPath: dest) : baseURL.appendingPathComponent(dest)
}
/// Caution: may put pressure on CPU, may have latency
open var bytesSoFar: Int64 {
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return 0 }
let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0
let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0
guard let size = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 else { return -1 }
if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) {
return Int64(uploaded * (Double(size) / 100))
} else if (uploaded == 0 || uploaded == 100) && (downloaded > 0 && downloaded < 100) {
return Int64(downloaded * (Double(size) / 100))
} else if uploaded == 100 || downloaded == 100 {
return size
}
return 0
}
/// Caution: may put pressure on CPU, may have latency
open var totalBytes: Int64 {
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return -1 }
return item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 ?? -1
}
open var inProgress: Bool {
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return false }
let downloadStatus = item.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String ?? NSMetadataUbiquitousItemDownloadingStatusNotDownloaded
let isUploading = item.value(forAttribute: NSMetadataUbiquitousItemIsUploadingKey) as? Bool ?? false
return downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent || isUploading
}
/// Not usable in local provider
open func cancel() -> Bool {
return false
}
fileprivate static func getMetadataItem(url: URL) -> NSMetadataItem? {
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
var item: NSMetadataItem?
let group = DispatchGroup()
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
defer {
query.stop()
group.leave()
NotificationCenter.default.removeObserver(finishObserver!)
}
if query.resultCount > 0 {
item = query.result(at: 0) as? NSMetadataItem
}
query.disableUpdates()
})
group.enter()
query.start()
_ = group.wait(timeout: DispatchTime.now() + 30)
return item
}
}
+57 -22
View File
@@ -22,11 +22,13 @@ open class DropboxFileProvider: FileProviderBasicRemote {
open let apiURL: URL
open let contentURL: URL
open var dispatch_queue: DispatchQueue {
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue {
willSet {
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
}
}
open weak var delegate: FileProviderDelegate?
open let credential: URLCredential?
open private(set) var cache: URLCache?
@@ -38,12 +40,10 @@ open class DropboxFileProvider: FileProviderBasicRemote {
public var session: URLSession {
if _session == nil {
self.sessionDelegate = SessionDelegate(fileProvider: self, credential: credential)
let queue = OperationQueue()
//queue.underlyingQueue = dispatch_queue
let config = URLSessionConfiguration.default
config.urlCache = cache
config.requestCachePolicy = .returnCacheDataElseLoad
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: queue)
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: self.operation_queue)
}
return _session!
}
@@ -58,9 +58,12 @@ open class DropboxFileProvider: FileProviderBasicRemote {
self.credential = credential
self.apiURL = URL(string: "https://api.dropboxapi.com/2/")!
self.contentURL = URL(string: "https://content.dropboxapi.com/2")!
self.contentURL = URL(string: "https://content.dropboxapi.com/2/")!
dispatch_queue = DispatchQueue(label: "FileProvider.\(DropboxFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent)
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: DispatchQueue.Attributes.concurrent)
operation_queue = OperationQueue()
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
}
deinit {
@@ -79,7 +82,7 @@ open class DropboxFileProvider: FileProviderBasicRemote {
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestDictionary = ["path": correctPath(path)! as NSString]
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderDropboxError?
@@ -87,7 +90,7 @@ open class DropboxFileProvider: FileProviderBasicRemote {
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
dbError = 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 = self.mapToFileObject(json) {
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let file = DropboxFileObject(json: json) {
fileObject = file
}
}
@@ -202,8 +205,8 @@ extension DropboxFileProvider: FileProviderOperations {
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
let requestDictionary = ["path": path]
let requestJson = dictionaryToJSON(requestDictionary as [String : AnyObject]) ?? ""
let requestDictionary: [String: AnyObject] = ["path": path as NSString]
let requestJson = dictionaryToJSON(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 {
@@ -228,18 +231,22 @@ extension DropboxFileProvider: FileProviderOperations {
extension DropboxFileProvider: FileProviderReadWrite {
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
if length == 0 {
return nil
}
let opType = FileOperationType.fetch(path: path)
let url = URL(string: "files/download", relativeTo: contentURL)!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
if length > 0 {
request.setValue("bytes=\(offset)-\(offset + length)", forHTTPHeaderField: "Range")
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
} else if offset > 0 && length < 0 {
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
}
let requestDictionary = ["path": correctPath(path)! as NSString]
request.setValue(dictionaryToJSON(requestDictionary as [String : AnyObject]), forHTTPHeaderField: "Dropbox-API-Arg")
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderDropboxError?
if let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: httpResponse.statusCode) {
@@ -289,13 +296,20 @@ extension DropboxFileProvider: FileProviderReadWrite {
}
extension DropboxFileProvider {
@available(*, deprecated, message: "Use DropboxFileProvider.temporaryLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
self.temporaryLink(to: path) { (url, file, _, error) in
completionHandler(url, file, error)
}
}
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
let url = URL(string: "files/get_temporary_link", 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 = ["path": correctPath(path)! as NSString]
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderDropboxError?
@@ -309,22 +323,24 @@ extension DropboxFileProvider {
link = URL(string: linkStr)
}
if let attribDic = json["metadata"] as? [String: AnyObject] {
fileObject = self.mapToFileObject(attribDic)
fileObject = DropboxFileObject(json: attribDic)
}
}
}
completionHandler(link, fileObject, dbError ?? error)
let expiration: Date? = link != nil ? Date(timeIntervalSinceNow: 4 * 60 * 60) : nil
completionHandler(link, fileObject, expiration, dbError ?? error)
})
task.resume()
}
open func copyItem(path: String, toRemoteURL destURL: URL, completionHandler: @escaping ((_ jobId: String?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
open func copyItem(remoteURL: URL, to toPath: String, completionHandler: @escaping ((_ jobId: String?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
let url = URL(string: "files/save_url", 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 = ["path": correctPath(path)! as NSString, "url" : destURL.absoluteString as NSString]
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "url" : remoteURL.absoluteString as NSString]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderDropboxError?
@@ -332,11 +348,11 @@ extension DropboxFileProvider {
var fileObject: DropboxFileObject?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
dbError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
dbError = 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) {
jobId = json["async_job_id"] as? String
if let attribDic = json["metadata"] as? [String: AnyObject] {
fileObject = self.mapToFileObject(attribDic)
fileObject = DropboxFileObject(json: attribDic)
}
}
}
@@ -344,6 +360,25 @@ extension DropboxFileProvider {
})
task.resume()
}
open func copyItem(reference: String, to toPath: String, completionHandler:SimpleCompletionHandler) {
let url = URL(string: "files/save_url", 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(toPath)! as NSString, "copy_reference" : reference as NSString]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderDropboxError?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
dbError = code != nil ? FileProviderDropboxError(code: code!, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
}
completionHandler?(dbError ?? error)
})
task.resume()
}
}
extension DropboxFileProvider: ExtendedFileProvider {
@@ -393,7 +428,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
}
var request = URLRequest(url: url)
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
var requestDictionary = ["path": path as NSString]
var requestDictionary: [String: AnyObject] = ["path": path as NSString]
requestDictionary["format"] = "jpeg" as NSString
if let dimension = dimension {
requestDictionary["size"] = "w\(Int(dimension.width))h\(Int(dimension.height))" as NSString
@@ -426,7 +461,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestDictionary = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)]
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderDropboxError?
+28 -29
View File
@@ -20,7 +20,25 @@ public struct FileProviderDropboxError: Error, CustomStringConvertible {
public final class DropboxFileObject: FileObject {
internal init(name: String, path: String) {
super.init(absoluteURL: URL(string: path), name: name, path: path)
super.init(url: URL(string: path) ?? URL(string: "/")!, name: name, path: path)
}
internal convenience init? (jsonStr: String) {
guard let json = jsonToDictionary(jsonStr) else { return nil }
self.init(json: json)
}
internal convenience init? (json: [String: AnyObject]) {
guard let name = json["name"] as? String else { return nil }
guard let path = json["path_display"] as? String else { return nil }
self.init(name: name, path: path)
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
self.serverTime = resolve(dateString: json["server_modified"] as? String ?? "")
self.modifiedDate = resolve(dateString: json["client_modified"] as? String ?? "")
self.type = (json[".tag"] as? String) == "folder" ? .directory : .regular
self.isReadOnly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false
self.id = json["id"] as? String
self.rev = json["rev"] as? String
}
open internal(set) var serverTime: Date? {
@@ -79,7 +97,7 @@ internal extension DropboxFileProvider {
let json = jsonToDictionary(jsonStr)
if let entries = json?["entries"] as? [AnyObject] , entries.count > 0 {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = self.mapToFileObject(entry) {
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
files.append(file)
}
}
@@ -104,17 +122,17 @@ internal extension DropboxFileProvider {
self.delegateNotify(.create(path: targetPath), error: error)
return nil
}
var requestDictionary = [String: Any]()
var requestDictionary = [String: AnyObject]()
let url: URL
url = URL(string: "files/upload", relativeTo: contentURL)!
requestDictionary["path"] = correctPath(targetPath) as NSString?
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
requestDictionary["client_modified"] = string(from:modifiedDate)
requestDictionary["client_modified"] = rfc3339utc(of: modifiedDate) as NSString
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.setValue(dictionaryToJSON(requestDictionary as [String : AnyObject]), forHTTPHeaderField: "Dropbox-API-Arg")
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
request.httpBody = data
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
var responseError: FileProviderDropboxError?
@@ -137,17 +155,17 @@ internal extension DropboxFileProvider {
self.delegateNotify(.create(path: targetPath), error: error)
return nil
}
var requestDictionary = [String: Any]()
var requestDictionary = [String: AnyObject]()
let url: URL
url = URL(string: "files/upload", relativeTo: contentURL)!
requestDictionary["path"] = correctPath(targetPath) as NSString?
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
requestDictionary["client_modified"] = string(from:modifiedDate)
requestDictionary["client_modified"] = rfc3339utc(of: modifiedDate) as NSString
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.setValue(dictionaryToJSON(requestDictionary as [String : AnyObject]), forHTTPHeaderField: "Dropbox-API-Arg")
request.setValue(dictionaryToJSON(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) {
@@ -181,7 +199,7 @@ internal extension DropboxFileProvider {
let json = jsonToDictionary(jsonStr)
if let entries = json?["matches"] as? [AnyObject] , entries.count > 0 {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = self.mapToFileObject(entry) {
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
foundItem(file)
}
}
@@ -203,25 +221,6 @@ internal extension DropboxFileProvider {
// codebeat:enable[ARITY]
internal extension DropboxFileProvider {
func mapToFileObject(_ jsonStr: String) -> DropboxFileObject? {
guard let json = jsonToDictionary(jsonStr) else { return nil }
return self.mapToFileObject(json)
}
func mapToFileObject(_ json: [String: AnyObject]) -> DropboxFileObject? {
guard let name = json["name"] as? String else { return nil }
guard let path = json["path_display"] as? String else { return nil }
let fileObject = DropboxFileObject(name: name, path: path)
fileObject.size = (json["size"] as? NSNumber)?.int64Value ?? -1
fileObject.serverTime = resolve(dateString: json["server_modified"] as? String ?? "")
fileObject.modifiedDate = resolve(dateString: json["client_modified"] as? String ?? "")
fileObject.type = (json[".tag"] as? String) == "folder" ? .directory : .regular
fileObject.isReadOnly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false
fileObject.id = json["id"] as? String
fileObject.rev = json["rev"] as? String
return fileObject
}
static let dateFormatter = DateFormatter()
static let decimalFormatter = NumberFormatter()
@@ -241,7 +240,7 @@ internal extension DropboxFileProvider {
let longStr = DropboxFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))
dic["Location"] = "\(latStr), \(longStr)"
}
if let timeTakenStr = json["time_taken"] as? String, let timeTaken = self.resolve(dateString: timeTakenStr) {
if let timeTakenStr = json["time_taken"] as? String, let timeTaken = resolve(dateString: timeTakenStr) {
keys.append("Date taken")
DropboxFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dic["Date taken"] = DropboxFileProvider.dateFormatter.string(from: timeTaken)
+4 -3
View File
@@ -61,10 +61,11 @@ extension LocalFileProvider: ExtendedFileProvider {
}
public func thumbnailOfFile(path: String, dimension: CGSize? = nil, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
let dimension = dimension ?? CGSize(width: 64, height: 64)
(dispatch_queue).async {
var thumbnailImage: ImageClass? = nil
// Check cache
let fileURL = self.absoluteURL(path)
let fileURL = self.url(of: path)
// Create Thumbnail and cache
switch fileURL.pathExtension.lowercased() {
case LocalFileInformationGenerator.videoThumbnailExtensions:
@@ -85,7 +86,7 @@ extension LocalFileProvider: ExtendedFileProvider {
}
if let image = thumbnailImage {
let scaledImage = dimension != nil ? LocalFileProvider.scaleDown(image: image, toSize: dimension!) : image
let scaledImage = LocalFileProvider.scaleDown(image: image, toSize: dimension)
completionHandler(scaledImage, nil)
}
}
@@ -117,7 +118,7 @@ extension LocalFileProvider: ExtendedFileProvider {
var dic = [String: Any]()
var keys = [String]()
if let getterMethod = getter {
(dic, keys) = getterMethod(self.absoluteURL(path))
(dic, keys) = getterMethod(self.url(of: path))
}
completionHandler(dic, keys, nil)
+51 -6
View File
@@ -10,26 +10,32 @@ import Foundation
/// Containts path and attributes of a file or resource.
open class FileObject {
/// A `Dictionary` contains file information, using `URLResourceKey` keys.
open internal(set) var allValues: [String: Any]
internal init(allValues: [String: Any]) {
self.allValues = allValues
}
internal init(absoluteURL: URL? = nil, name: String, path: String) {
internal init(url: URL, name: String, path: String) {
self.allValues = [String: Any]()
self.absoluteURL = absoluteURL
self.url = url
self.name = name
self.path = path
}
/// url to access the resource, not supported by Dropbox provider
open internal(set) var absoluteURL: URL? {
@available(*, deprecated, message: "Use FileObject.url.absoluteURL instead.")
open var absoluteURL: URL? {
return url?.absoluteURL
}
open internal(set) var url: URL? {
get {
return allValues["NSURLAbsoluteURLKey"] as? URL
return allValues["NSURLFileURLKey"] as? URL
}
set {
allValues["NSURLAbsoluteURLKey"] = newValue
allValues["NSURLFileURLKey"] = newValue
}
}
@@ -135,6 +141,37 @@ open class FileObject {
}
}
internal func resolve(dateString: String) -> Date? {
let dateFor: DateFormatter = DateFormatter()
dateFor.locale = Locale(identifier: "en_US")
dateFor.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"
if let rfc3339 = dateFor.date(from: dateString) {
return rfc3339
}
dateFor.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss z"
if let rfc1123 = dateFor.date(from: dateString) {
return rfc1123
}
dateFor.dateFormat = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
if let rfc850 = dateFor.date(from: dateString) {
return rfc850
}
dateFor.dateFormat = "EEE MMM d HH':'mm':'ss yyyy"
if let asctime = dateFor.date(from: dateString) {
return asctime
}
return nil
}
internal func rfc3339utc(of date:Date) -> String {
let fm = DateFormatter()
fm.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
fm.timeZone = TimeZone(identifier:"UTC")
fm.locale = Locale(identifier:"en_US_POSIX")
return fm.string(from:date)
}
/// Sorting FileObject array by given criteria, **not thread-safe**
public struct FileObjectSorting {
@@ -179,13 +216,19 @@ public struct FileObjectSorting {
public static let createdAscending = FileObjectSorting(type: .creationDate, ascending: true)
public static let createdDesceding = FileObjectSorting(type: .creationDate, ascending: false)
/// Initializes a `FileObjectSorting` allows to sort an `Array` of `FileObject`.
///
/// - Parameters:
/// - type: Determines to sort based on which file property.
/// - ascending: `true` of resulting `Array` is ascending
/// - isDirectoriesFirst: Puts directoris on the top of resulting `Array`.
public init (type: SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) {
self.sortType = type
self.ascending = ascending
self.isDirectoriesFirst = isDirectoriesFirst
}
/// Sorts array of FileObjects by criterias set in properties
/// Sorts array of `FileObject`s by criterias set in properties
public func sort(_ files: [FileObject]) -> [FileObject] {
return files.sorted {
if isDirectoriesFirst {
@@ -223,11 +266,13 @@ public struct FileObjectSorting {
}
extension Array where Element: FileObject {
/// Returns a sorted array of `FileObject`s by criterias set in properties.
public func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) -> [Element] {
let sorting = FileObjectSorting(type: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
return sorting.sort(self) as! [Element]
}
/// Sorts array of `FileObject`s by criterias set in properties
public mutating func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) {
self = self.sorted(by: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
}
+410 -36
View File
@@ -18,26 +18,113 @@ public typealias ImageClass = NSImage
public typealias SimpleCompletionHandler = ((_ error: Error?) -> Void)?
public protocol FileProviderBasic: class {
/// 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 }
/// Current active path used in `contentsOfDirectory(path:completionHandler:)` method.
var currentPath: String { get set }
/**
Dispatch queue usually used in query methods.
Set it to a new object to switch between cuncurrent and serial queues.
- **Default:** Cuncurrent `DispatchQueue` object.
*/
var dispatch_queue: DispatchQueue { get set }
/// Operation queue ususlly used in file operation methods.
/// use `maximumOperationTasks` property of provider to manage operation queue.
var operation_queue: OperationQueue { get set }
/// Delegate to update UI after finishing file operations.
var delegate: FileProviderDelegate? { get set }
/**
login credential for provider. Should be set in `init` method.
**Example initialization:**
````
let credential = URLCredential(user: "user", password: "password", persistence: .forSeession)
````
- Note: In OAuth based providers like `DropboxFileProvider` and `OneDriveFileProvider`, password is Token.
use [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) library to fetch clientId and Token of user.
*/
var credential: URLCredential? { get }
/**
Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler.
If the directory contains no entries or an error is occured, this method will return the empty array.
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
- Parameter completionHandler: a block with result of directory entries or error.
`contents`: An array of `FileObject` identifying the the directory entries.
`error`: Error returned by system.
*/
func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void))
/**
Returns a `FileObject` containing the attributes of the item (file, directory, symlink, etc.) at the path in question via asynchronous completion handler.
If the directory contains no entries or an error is occured, this method will return the empty `FileObject`.
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
- Parameter completionHandler: a block with result of directory entries or error.
`attributes`: A `FileObject` containing the attributes of the item.
`error`: Error returned by system.
*/
func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void))
/// Returns total and used space in provider container asynchronously.
func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> 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.
- Parameter path: Relative path of file or directory.
- Returns: An url, can be used to access to file directly.
*/
func url(of path: String?) -> URL
}
public extension FileProviderBasic {
/// The maximum number of queued operations that can execute at the same time.
///
/// The default value of this property is `OperationQueue.defaultMaxConcurrentOperationCount`.
public var maximumOperationTasks: Int {
get {
return operation_queue.maxConcurrentOperationCount
}
set {
operation_queue.maxConcurrentOperationCount = newValue
}
}
}
public protocol FileProviderBasicRemote: FileProviderBasic {
///
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`.
var cache: URLCache? { get }
/// Determine to use `cache` property to cache downloaded file objects. Doesn't have effect on query type methods.
var useCache: Bool { get set }
/// Validating cached data using E-Tag or Revision identifier if possible.
var validatingCache: Bool { get set }
}
@@ -92,24 +179,140 @@ internal extension FileProviderBasicRemote {
public protocol FileProviderOperations: FileProviderBasic {
var fileOperationDelegate : FileOperationDelegate? { get set }
/**
Creates a new directory at the specified path asynchronously.
This will create any necessary intermediate directories.
- Parameters:
- folder: Directory name.
- at: Parent path of new directory.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
*/
@discardableResult
func create(folder: String, at: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Creates an new file with data passed to method asynchronously.
Returns error via completionHandler if file is already exists.
- Parameters:
- file: New file name with extension separated by period.
- at: Parent path of new file.
- data: Data of new files. Pass nil or `Data()` to create empty file.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Moves a file or directory from `path` to designated path asynchronously.
When you want move a file, destination path should also consists of file name.
Either a new name or the old one. If file is already exist, an error will be returned via completionHandler.
- Parameters:
- path: original file or directory path.
- to: destination path of file or directory, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Moves a file or directory from `path` to designated path asynchronously.
When you want move a file, destination path should also consists of file name.
Either a new name or the old one.
- Parameters:
- path: original file or directory path.
- to: destination path of file or directory, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func moveItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Copies a file or directory from `path` to designated path asynchronously.
When want copy a file, destination path should also consists of file name.
Either a new name or the old one. If file is already exist, an error will be returned via completionHandler.
- Parameters:
- path: original file or directory path.
- to: destination path of file or directory, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Copies a file or directory from `path` to designated path asynchronously.
When want copy a file, destination path should also consists of file name.
Either a new name or the old one.
- Parameters:
- path: original file or directory path.
- to: destination path of file or directory, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Removes the file or directory at the specified path.
- Parameters:
- path: file or directory path.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Uploads a file from local file url to designated path asynchronously.
Method will fail if source is not a local url with `file://` scheme.
- Parameters:
- localFile: a file url to file.
- to: destination path of file, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress.
*/
@discardableResult
func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Uploads a file from local file url to designated path asynchronously.
Method will fail if source is not a local url with `file://` scheme.
- Parameters:
- localFile: a file url to file.
- to: destination path of file, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress.
*/
@discardableResult
func copyItem(localFile: URL, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Download a file from `path` to designated local file url asynchronously.
Method will fail if destination is not a local url with `file://` scheme.
- Parameters:
- path: original file or directory path.
- toLocalURL: destination local url of file, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle?
}
@@ -132,20 +335,104 @@ extension FileProviderOperations {
}
public protocol FileProviderReadWrite: FileProviderBasic {
/**
Retreives a `Data` object with the contents of the file asynchronously vis contents argument of completion handler.
If path specifies a directory, or if some other error occurs, data will be nil.
- Parameters:
- path: Path of file.
- completionHandler: a block with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle?
/**
Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler.
If path specifies a directory, or if some other error occurs, data will be nil.
- Parameters:
- path: Path of file.
- offset: First byte index which should be read. **Starts from 0.**
- length: Bytes count of data. Pass `-1` to read until the end of file.
- completionHandler: a block with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle?
/**
Write the contents of the `Data` to a location asynchronously.
It will return error if file is already exists.
Not attomically by default, unless the provider enforces it.
- Parameters:
- path: Path of target file.
- contents: Data to be written into file.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Write the contents of the `Data` to a location asynchronously.
It will return error if file is already exists.
- Parameters:
- path: Path of target file.
- contents: Data to be written into file.
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Write the contents of the `Data` to a location asynchronously.
Not attomically by default, unless the provider enforces it.
- Parameters:
- path: Path of target file.
- contents: Data to be written into file.
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Write the contents of the `Data` to a location asynchronously.
- Parameters:
- path: Path of target file.
- contents: Data to be written into file.
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
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))
}
@@ -172,20 +459,46 @@ extension FileProviderReadWrite {
}
public protocol FileProviderMonitor: FileProviderBasic {
/**
Starts monitoring a path and its subpaths, including files and folders, for any change,
including copy, move/rename, content changes, etc.
To avoid thread congestion, `evetHandler` will be triggered with 0.2 seconds interval,
and has a 0.25 second delay, to ensure it's called after updates.
- Note: this functionality is available only in `LocalFileProvider` and `CloudFileProvider`.
- Note: `eventHandler` is not called on main thread, for updating UI. dispatch routine to main thread.
- Important: `eventHandler` may be called if file is changed in recursive subpaths of registered path.
This may cause negative impact on performance if a root path is being monitored.
- Parameters:
- path: path of directory.
- eventHandler: Block executed after change, on a secondary thread.
*/
func registerNotifcation(path: String, eventHandler: @escaping (() -> Void))
/// Stops monitoring the path.
///
/// - Parameter path: path of directory.
func unregisterNotifcation(path: String)
/// Investigate either the path is registered for change notification or not.
///
/// - Parameter path: path of directory.
/// - Returns: Directory is being monitored or not.
func isRegisteredForNotification(path: String) -> Bool
}
public protocol FileProvider: FileProviderBasic, FileProviderOperations, FileProviderReadWrite, NSCopying {
}
fileprivate let pathTrimSet = CharacterSet(charactersIn: " /")
internal let pathTrimSet = CharacterSet(charactersIn: " /")
extension FileProviderBasic {
public var type: String {
return Self.type
}
/// path without heading and trailing slash
public var bareCurrentPath: String {
return currentPath.trimmingCharacters(in: pathTrimSet)
}
@@ -194,6 +507,7 @@ extension FileProviderBasic {
return path.trimmingCharacters(in: pathTrimSet).addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
}
@available(*, deprecated, message: "Use FileProvider.url(of:).absoluteURL instead.")
public func absoluteURL(_ path: String? = nil) -> URL {
return url(of: path).absoluteURL
}
@@ -217,6 +531,10 @@ extension FileProviderBasic {
}
public func relativePathOf(url: URL) -> String {
if url.baseURL == self.baseURL {
return url.relativePath.removingPercentEncoding!
}
guard let baseURL = self.baseURL else { return url.absoluteString }
return url.standardizedFileURL.absoluteString.replacingOccurrences(of: baseURL.absoluteString, with: "/").removingPercentEncoding!
}
@@ -230,6 +548,8 @@ extension FileProviderBasic {
return p
}
/// Returns a file name supposed to be unique with adding numbers to end of file.
/// - Important: It's a synchronous method. Don't use it on matin thread.
public func fileByUniqueName(_ filePath: String) -> String {
let fileUrl = URL(fileURLWithPath: filePath)
let dirPath = fileUrl.deletingLastPathComponent().path
@@ -251,7 +571,7 @@ extension FileProviderBasic {
}
var i = number ?? 2
let similiar = contents.map {
$0.absoluteURL?.lastPathComponent ?? $0.name
$0.url?.lastPathComponent ?? $0.name
}.filter {
$0.hasPrefix(result)
}
@@ -267,7 +587,7 @@ extension FileProviderBasic {
}
internal func throwError(_ path: String, code: FoundationErrorEnum) -> NSError {
let fileURL = self.absoluteURL(path)
let fileURL = self.url(of: path)
let domain: String
switch code {
case is URLError:
@@ -278,47 +598,67 @@ extension FileProviderBasic {
return NSError(domain: domain, code: code.rawValue, userInfo: [NSURLErrorFailingURLErrorKey: fileURL, NSURLErrorFailingURLStringErrorKey: fileURL.absoluteString])
}
internal func NotImplemented() {
assert(false, "method not implemented")
internal func NotImplemented(_ fn: String = #function, file: StaticString = #file) {
assert(false, "\(fn) method is not yet implemented. \(file)")
}
internal func resolve(dateString: String) -> Date? {
let dateFor: DateFormatter = DateFormatter()
dateFor.locale = Locale(identifier: "en_US")
dateFor.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss zzz"
if let rfc1123 = dateFor.date(from: dateString) {
return rfc1123
}
dateFor.dateFormat = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
if let rfc850 = dateFor.date(from: dateString) {
return rfc850
}
dateFor.dateFormat = "EEE MMM d HH':'mm':'ss yyyy"
if let asctime = dateFor.date(from: dateString) {
return asctime
}
dateFor.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssz"
if let isotime = dateFor.date(from: dateString) {
return isotime
}
return nil
}
public func string(from date:Date) -> String {
let fm = DateFormatter()
fm.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
fm.timeZone = TimeZone(identifier:"UTC")
fm.locale = Locale(identifier:"en_US_POSIX")
return fm.string(from:date)
}
}
public protocol ExtendedFileProvider: FileProviderBasic {
/// Returuns true if thumbnail preview is supported by provider and file type accordingly.
///
/// - Parameter path: path of file.
/// - Returns: A `Bool` idicates path can have thumbnail.
func thumbnailOfFileSupported(path: String) -> Bool
/// Returns true if provider supports fetching properties of file like dimensions, duration, etc.
/// Usually media or document files support these meta-infotmations.
///
/// - Parameter path: path of file.
/// - Returns: A `Bool` idicates path can have properties.
func propertiesOfFileSupported(path: String) -> Bool
/**
Generates ans returns a thumbnail preview of document asynchronously. The defualt dimension of returned image is different
regarding provider type, usually 64x64 pixels.
- Parameters:
- path: path of file.
- completionHandler: a block with result of preview image or error.
`image`: `NSImage`/`UIImage` object contains preview.
`error`: Error returned by system.
*/
func thumbnailOfFile(path: String, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void))
/**
Generates ans returns a thumbnail preview of document asynchronously. The defualt dimension of returned image is different
regarding provider type, usually 64x64 pixels. Default value used when `dimenstion` is `nil`.
- Note: `LocalFileInformationGenerator` variables can be set to change default behavior of
thumbnail and properties generator of `LocalFileProvider`.
- Parameters:
- path: path of file.
- dimension: width and height of result preview image.
- completionHandler: a block with result of preview image or error.
`image`: `NSImage`/`UIImage` object contains preview.
`error`: Error returned by system.
*/
func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void))
/**
Fetching properties of file like dimensions, duration, etc. It's variant depending on file type.
Images, videos and audio files meta-information will be returned.
- Note: `LocalFileInformationGenerator` variables can be set to change default behavior of
thumbnail and properties generator of `LocalFileProvider`.
- Parameters:
- path: path of file.
- completionHandler: a block 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.
*/
func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void))
}
@@ -445,13 +785,21 @@ extension ExtendedFileProvider {
}
}
/// Operation type description of file operation, included files path in associated values.
public enum FileOperationType: CustomStringConvertible {
/// Creating a file or directory in path.
case create (path: String)
/// Copying a file or directory from source to destination.
case copy (source: String, destination: String)
/// Moving a file or directory from source to destination.
case move (source: String, destination: String)
/// Modifying data of a file o in path by writing new data.
case modify (path: String)
/// Deleting file or directory in path.
case remove (path: String)
/// Creating a symbolic link or alias to target.
case link (link: String, target: String)
/// Fetching data in file located in path.
case fetch (path: String)
public var description: String {
@@ -466,16 +814,24 @@ public enum FileOperationType: CustomStringConvertible {
}
}
/// present participle of action, like 'Copying`.
public var actionDescription: String {
return description.trimmingCharacters(in: CharacterSet(charactersIn: "e")) + "ing"
}
/// Path of subjecting file.
public var source: String? {
guard let reflect = Mirror(reflecting: self).children.first?.value else { return nil }
let mirror = Mirror(reflecting: reflect)
return reflect as? String ?? mirror.children.first?.value as? String
}
/// Path of subjecting file.
public var path: String? {
return source
}
/// Path of destination file.
public var destination: String? {
guard let reflect = Mirror(reflecting: self).children.first?.value else { return nil }
let mirror = Mirror(reflecting: reflect)
@@ -492,11 +848,22 @@ public enum FileOperationType: CustomStringConvertible {
public protocol OperationHandle {
/// Operation supposed to be done on files. Contains file paths as associated value.
var operationType: FileOperationType { get }
/// Bytes written/read by operation so far.
var bytesSoFar: Int64 { get }
/// Total bytes of operation.
var totalBytes: Int64 { get }
/// Operation is progress or not, Returns false if operation is done or not initiated yet.
var inProgress: Bool { get }
/// Progress of operation, usually equals with `bytesSoFar/totalBytes`. or NaN if not available.
var progress: Float { get }
/// Cancels operation while in progress, or cancels data/download/upload url session task.
func cancel() -> Bool
}
@@ -509,8 +876,15 @@ public extension OperationHandle {
}
public protocol FileProviderDelegate: class {
/// fileproviderSucceed(_:operation:) gives delegate a notification when an operation finished with success.
/// This method is called in main thread to avoids UI bugs.
func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperationType)
/// fileproviderSucceed(_:operation:) gives delegate a notification when an operation finished with failure.
/// This method is called in main thread to avoids UI bugs.
func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperationType)
/// fileproviderSucceed(_:operation:) gives delegate a notification when an operation progess.
/// Supported by some providers, especially remote ones.
/// This method is called in main thread to avoids UI bugs.
func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperationType, progress: Float)
}
+334 -113
View File
@@ -14,31 +14,66 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
open fileprivate(set) var baseURL: URL?
open var currentPath: String
open var dispatch_queue: DispatchQueue
open var operation_queue: DispatchQueue
open var operation_queue: OperationQueue
open weak var delegate: FileProviderDelegate?
open fileprivate(set) var credential: URLCredential?
open internal(set) var credential: URLCredential?
open private(set) var fileManager = FileManager()
open private(set) var opFileManager = FileManager()
fileprivate var fileProviderManagerDelegate: LocalFileProviderManagerDelegate? = nil
/// Forces file operations to use NSFileCoordinating, should be set true if:
/// - Files are on ubiquity(iCloud) container
/// - Multiple processes are accessing same file, recommended always in macOS and when using app extensions in iOS/tvOS (shared container)
open var isCoorinating: Bool
/// default values are `directory: .documentDirectory, domainMask: .userDomainMask`
public convenience init (directory: FileManager.SearchPathDirectory = .documentDirectory, domainMask: FileManager.SearchPathDomainMask = .userDomainMask) {
self.init(baseURL: FileManager.default.urls(for: directory, in: domainMask).first!)
}
public convenience init ? (sharedContainerId: String, directory: FileManager.SearchPathDirectory = .userDirectory) {
guard let baseURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: sharedContainerId) else {
return nil
}
var finalBaseURL = baseURL
switch directory {
case .documentDirectory:
finalBaseURL = baseURL.appendingPathComponent("Documents")
case .libraryDirectory:
finalBaseURL = baseURL.appendingPathComponent("Library")
case .cachesDirectory:
finalBaseURL = baseURL.appendingPathComponent("Library/Caches")
case .applicationSupportDirectory:
finalBaseURL = baseURL.appendingPathComponent("Library/Application%20support")
default:
break
}
self.init(baseURL: finalBaseURL)
try? fileManager.createDirectory(at: finalBaseURL, withIntermediateDirectories: true)
}
public init (baseURL: URL) {
self.baseURL = baseURL
self.isPathRelative = true
self.currentPath = ""
self.credential = nil
self.isCoorinating = false
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: DispatchQueue.Attributes.concurrent)
operation_queue = OperationQueue()
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
dispatch_queue = DispatchQueue(label: "FileProvider.\(LocalFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent)
operation_queue = DispatchQueue(label: "FileProvider.\(LocalFileProvider.type).Operation", attributes: [])
fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self)
opFileManager.delegate = fileProviderManagerDelegate
}
@available(*, deprecated, message: "Overriding this method has no effect anymore.")
open class func defaultBaseURL() -> URL {
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
}
@@ -46,13 +81,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
dispatch_queue.async {
do {
let contents = try self.fileManager.contentsOfDirectory(at: self.absoluteURL(path), includingPropertiesForKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .isHiddenKey, .volumeIsReadOnlyKey], options: .skipsSubdirectoryDescendants)
let contents = try self.fileManager.contentsOfDirectory(at: self.url(of: path), includingPropertiesForKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .isHiddenKey, .volumeIsReadOnlyKey], options: .skipsSubdirectoryDescendants)
let filesAttributes = contents.flatMap({ (fileURL) -> LocalFileObject? in
let path = self.relativePathOf(url: fileURL)
return LocalFileObject(fileWithPath: path, relativeTo: self.baseURL)
})
completionHandler(filesAttributes, nil)
} catch let e as NSError {
} catch let e {
completionHandler([], e)
}
}
@@ -76,109 +111,204 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
@discardableResult
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
operation_queue.async {
let url = self.url(of: atPath).appendingPathComponent(folderName)
let operationHandler: (URL) -> Void = { url in
do {
try self.opFileManager.createDirectory(at: self.absoluteURL(atPath).appendingPathComponent(folderName), withIntermediateDirectories: true, attributes: [:])
try self.opFileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: [:])
completionHandler?(nil)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
} catch let e as NSError {
}
} catch let e {
completionHandler?(e)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
})
}
}
}
if isCoorinating {
let intent = NSFileAccessIntent.writingIntent(with: url, options: .forReplacing)
self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
completionHandler?(error)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
})
} else {
operation_queue.addOperation {
operationHandler(url)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
}
@discardableResult
open func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(fileName))
operation_queue.async {
let fileURL = self.absoluteURL(atPath).appendingPathComponent(fileName)
let success = self.opFileManager.createFile(atPath: fileURL.path, contents: data, attributes: nil)
let url = self.url(of: atPath).appendingPathComponent(fileName, isDirectory: false)
let operationHandler: (URL) -> Void = { url in
let success = self.opFileManager.createFile(atPath: url.path, contents: data, attributes: nil)
if success {
completionHandler?(nil)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
}
} else {
completionHandler?(self.throwError(atPath, code: URLError.cannotCreateFile as FoundationErrorEnum))
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
})
}
}
}
if isCoorinating {
let intent = NSFileAccessIntent.writingIntent(with:url, options: .forReplacing)
self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
completionHandler?(error)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
})
} else {
operation_queue.addOperation {
operationHandler(url)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
}
@discardableResult
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.move(source: path, destination: toPath)
operation_queue.async {
if !overwrite && self.fileManager.fileExists(atPath: self.absoluteURL(toPath).path) {
let sourceUrl = self.url(of: path)
let destUrl = self.url(of: toPath)
let sourceIntent = NSFileAccessIntent.writingIntent(with: sourceUrl, options: .forDeleting)
let destIntent = NSFileAccessIntent.writingIntent(with: destUrl, options: .forReplacing)
let operationHandler: (URL, URL) -> Void = { sourceUrl, destUrl in
if !overwrite && self.fileManager.fileExists(atPath: destUrl.path) {
completionHandler?(self.throwError(toPath, code: URLError.cannotMoveFile as FoundationErrorEnum))
return
}
do {
try self.opFileManager.moveItem(at: self.absoluteURL(path), to: self.absoluteURL(toPath))
try self.opFileManager.moveItem(at: sourceUrl, to: destUrl)
completionHandler?(nil)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
} catch let e as NSError {
}
} catch let e {
completionHandler?(e)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
})
}
}
}
if isCoorinating {
coordinated(intents: [sourceIntent, destIntent], completionHandler: operationHandler) { (error) in
completionHandler?(error)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
}
} else {
operation_queue.addOperation {
operationHandler(sourceUrl, destUrl)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
}
@discardableResult
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: toPath)
operation_queue.async {
if !overwrite && self.fileManager.fileExists(atPath: self.absoluteURL(toPath).path) {
completionHandler?(self.throwError(toPath, code: URLError.cannotWriteToFile as FoundationErrorEnum))
let sourceUrl = self.url(of: path)
let destUrl = self.url(of: toPath)
let sourceIntent = NSFileAccessIntent.readingIntent(with: sourceUrl, options: .withoutChanges)
let destIntent = NSFileAccessIntent.writingIntent(with: destUrl, options: .forDeleting)
let operationHandler: (URL, URL) -> Void = { sourceUrl, destUrl in
if !overwrite && self.fileManager.fileExists(atPath: destUrl.path) {
completionHandler?(self.throwError(toPath, code: URLError.cannotMoveFile as FoundationErrorEnum))
return
}
do {
try self.opFileManager.copyItem(at: self.absoluteURL(path), to: self.absoluteURL(toPath))
try self.opFileManager.copyItem(at: sourceUrl, to: destUrl)
completionHandler?(nil)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
} catch let e as NSError {
}
} catch let e {
completionHandler?(e)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
})
}
}
}
if isCoorinating {
coordinated(intents: [sourceIntent, destIntent], moving: true, completionHandler: operationHandler) { (error) in
completionHandler?(error)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
}
} else {
operation_queue.addOperation {
operationHandler(sourceUrl, destUrl)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
}
@discardableResult
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.remove(path: path)
operation_queue.async {
let url = self.url(of: path)
let operationHandler: (URL) -> Void = { url in
do {
try self.opFileManager.removeItem(at: self.absoluteURL(path))
let successfulSecurityScopedResourceAccess = url.startAccessingSecurityScopedResource()
try self.opFileManager.removeItem(at: url)
if successfulSecurityScopedResourceAccess {
url.stopAccessingSecurityScopedResource()
}
completionHandler?(nil)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
} catch let e as NSError {
}
} catch let e {
completionHandler?(e)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
})
}
}
}
if isCoorinating {
let intent = NSFileAccessIntent.writingIntent(with:url, options: .forReplacing)
self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
completionHandler?(error)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
})
} else {
operation_queue.addOperation {
operationHandler(url)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
}
@@ -186,18 +316,18 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
// TODO: Make use of overwrite parameter
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
operation_queue.async {
operation_queue.addOperation {
do {
try self.opFileManager.copyItem(at: localFile, to: self.absoluteURL(toPath))
try self.opFileManager.copyItem(at: localFile, to: self.url(of: toPath))
completionHandler?(nil)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
} catch let e as NSError {
}
} catch let e {
completionHandler?(e)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
})
}
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
@@ -206,18 +336,18 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
@discardableResult
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
operation_queue.async {
operation_queue.addOperation {
do {
try self.opFileManager.copyItem(at: self.absoluteURL(path), to: toLocalURL)
try self.opFileManager.copyItem(at: self.url(of: path), to: toLocalURL)
completionHandler?(nil)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
} catch let e as NSError {
}
} catch let e {
completionHandler?(e)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
})
}
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
@@ -225,43 +355,98 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
@discardableResult
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
dispatch_queue.async {
let data = self.fileManager.contents(atPath: self.absoluteURL(path).path)
completionHandler(data, nil)
let opType = FileOperationType.fetch(path: path)
let url = self.url(of: path)
let operationHandler: (URL) -> Void = { url in
do {
let data = try Data(contentsOf: url)
completionHandler(data, nil)
} catch let e {
completionHandler(nil, e)
}
}
return nil
if isCoorinating {
let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges)
coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
completionHandler(nil, error)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
})
} else {
dispatch_queue.async {
operationHandler(url)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
}
@discardableResult
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
if length < 0 {
if length == 0 {
return nil
}
if offset == 0 && length < 0 {
return self.contents(path: path, completionHandler: completionHandler)
}
let opType = FileOperationType.fetch(path: path)
dispatch_queue.async {
let aPath = self.absoluteURL(path).path
guard self.fileManager.fileExists(atPath: aPath) && !self.absoluteURL(path).fileIsDirectory else {
completionHandler(nil, self.throwError(path, code: URLError.cannotOpenFile as FoundationErrorEnum))
return
}
guard let handle = FileHandle(forReadingAtPath: aPath) else {
let url = self.url(of: path)
let operationHandler: (URL) -> Void = { url in
guard self.fileManager.fileExists(atPath: url.path) && !url.fileIsDirectory else {
completionHandler(nil, self.throwError(path, code: URLError.fileDoesNotExist as FoundationErrorEnum))
return
}
guard let handle = FileHandle(forReadingAtPath: url.path) else {
completionHandler(nil, self.throwError(path, code: URLError.cannotOpenFile as FoundationErrorEnum))
return
}
defer {
handle.closeFile()
}
handle.seek(toFileOffset: UInt64(offset))
let data = handle.readData(ofLength: length)
completionHandler(data, nil)
handle.seek(toFileOffset: UInt64(offset))
guard Int64(handle.offsetInFile) == offset else {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadUnknown as FoundationErrorEnum))
return
}
let data = handle.readData(ofLength: length)
guard length > 0 && data.count == length else {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
return
}
completionHandler(data, nil)
}
if isCoorinating {
let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges)
coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
completionHandler(nil, error)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
})
} else {
dispatch_queue.async {
operationHandler(url)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
}
@discardableResult
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.modify(path: path)
let url = self.url(of: path)
var options: Data.WritingOptions = []
if atomically {
options.insert(.atomic)
@@ -269,18 +454,42 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
if overwrite {
options.insert(.withoutOverwriting)
}
operation_queue.async {
try? data.write(to: self.absoluteURL(path), options: atomically ? [.atomic] : [])
DispatchQueue.main.async(execute: {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
let operationHandler: (URL) -> Void = { url in
do {
try data.write(to: url, options: atomically ? [.atomic] : [])
completionHandler?(nil)
DispatchQueue.main.async{
self.delegate?.fileproviderSucceed(self, operation: opType)
}
} catch let e {
completionHandler?(e)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
}
}
if isCoorinating {
let intent = NSFileAccessIntent.writingIntent(with: url, options: .forReplacing)
coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
completionHandler?(error)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
})
} else {
operation_queue.addOperation {
operationHandler(url)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
}
open func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
dispatch_queue.async {
let iterator = self.fileManager.enumerator(at: self.absoluteURL(path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in
let iterator = self.fileManager.enumerator(at: self.url(of: path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in
completionHandler([], e)
return true
}
@@ -302,12 +511,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
open func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
self.unregisterNotifcation(path: path)
let absurl = self.absoluteURL(path)
let isdir = (try? absurl.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false) ?? false
let dirurl = self.url(of: path)
let isdir = (try? dirurl.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false) ?? false
if !isdir {
return
}
let monitor = LocalFolderMonitor(url: absurl) {
let monitor = LocalFolderMonitor(url: dirurl) {
eventHandler()
}
monitor.start()
@@ -341,49 +550,61 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
public extension LocalFileProvider {
public func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) {
operation_queue.async {
operation_queue.addOperation {
do {
try self.opFileManager.createSymbolicLink(at: self.absoluteURL(path), withDestinationURL: self.absoluteURL(destPath))
try self.opFileManager.createSymbolicLink(at: self.url(of: path), withDestinationURL: self.url(of: destPath))
completionHandler?(nil)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: .link(link: path, target: destPath))
})
} catch let e as NSError {
}
} catch let e {
completionHandler?(e)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: .link(link: path, target: destPath))
})
}
}
}
}
public func destination(ofSymbolicLink path: String, completionHandler: @escaping (_ url: URL?, _ error: Error?) -> Void) {
dispatch_queue.async {
do {
let destPath = try self.opFileManager.destinationOfSymbolicLink(atPath: self.url(of: path).path)
let destUrl = URL(fileURLWithPath: destPath)
completionHandler(destUrl, nil)
} catch let e{
completionHandler(nil, e)
}
}
}
}
class CloudFileProvider: LocalFileProvider {
// FIXME: convert static var type to class var in next Swift version
open var type: String { return "iCloudDrive" }
public init? (containerId: String?) {
assert(!Thread.isMainThread, "LocalFileProvider.init(containerId:) is not recommended to be executed on Main Thread.")
guard FileManager.default.ubiquityIdentityToken == nil else {
return nil
internal extension LocalFileProvider {
func coordinated(intents: [NSFileAccessIntent], completionHandler: @escaping (_ url: URL) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
let coordinator = NSFileCoordinator(filePresenter: nil)
coordinator.coordinate(with: intents, queue: operation_queue) { (error) in
if let error = error {
errorHandler?(error)
return
}
completionHandler(intents[0].url)
}
guard let ubiquityURL = FileManager.default.url(forUbiquityContainerIdentifier: containerId) else {
return nil
}
super.init(baseURL: ubiquityURL.appendingPathComponent("Documents"))
dispatch_queue = DispatchQueue(label: "FileProvider.\(self.type)", attributes: DispatchQueue.Attributes.concurrent)
operation_queue = DispatchQueue(label: "FileProvider.\(self.type).Operation", attributes: [])
fileManager.url(forUbiquityContainerIdentifier: containerId)
opFileManager.url(forUbiquityContainerIdentifier: containerId)
fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self)
opFileManager.delegate = fileProviderManagerDelegate
}
open override static func defaultBaseURL() -> URL {
return FileManager.default.url(forUbiquityContainerIdentifier: nil) ?? super.defaultBaseURL()
func coordinated(intents: [NSFileAccessIntent], moving: Bool = false, completionHandler: @escaping (_ sourceUrl: URL, _ destURL: URL) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
let coordinator = NSFileCoordinator(filePresenter: nil)
coordinator.coordinate(with: intents, queue: operation_queue) { (error) in
if let error = error {
errorHandler?(error)
return
}
if moving {
coordinator.item(at: intents[0].url, willMoveTo: intents[1].url)
}
completionHandler(intents[0].url, intents[1].url)
if moving {
coordinator.item(at: intents[0].url, didMoveTo: intents[1].url)
}
}
}
}
+30 -16
View File
@@ -9,32 +9,38 @@
import Foundation
public final class LocalFileObject: FileObject {
internal init(absoluteURL: URL, name: String, path: String) {
super.init(absoluteURL: absoluteURL, name: name, path: path)
internal override init(url: URL, name: String, path: String) {
super.init(url: url, name: name, path: path)
}
public convenience init? (fileWithPath path: String, relativeTo relativeURL: URL?) {
let fileURL: URL
if let relativeURL = relativeURL {
fileURL = relativeURL.appendingPathComponent(path)
} else {
fileURL = URL(fileURLWithPath: path)
var fileURL: URL?
var rpath = path.replacingOccurrences(of: relativeURL?.absoluteString ?? "", with: "", options: .anchored)
if path.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
}
do {
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey])
self.init(absoluteURL: fileURL, name: values.name ?? fileURL.lastPathComponent, path: path)
for (key, value) in values.allValues {
self.allValues[key.rawValue] = value
if rpath.isEmpty {
fileURL = 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)
}
} catch {
}
if let fileURL = fileURL {
self.init(fileWithURL: fileURL)
} else {
return nil
}
}
public convenience init?(fileWithURL fileURL: URL) {
do {
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey])
self.init(absoluteURL: fileURL, name: values.name ?? fileURL.lastPathComponent, path: fileURL.path)
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey, .generationIdentifierKey])
let path = fileURL.relativePath.hasPrefix("/") ? fileURL.relativePath : "/" + fileURL.relativePath
self.init(url: fileURL, name: values.name ?? fileURL.lastPathComponent, path: path)
for (key, value) in values.allValues {
self.allValues[key.rawValue] = value
}
@@ -51,6 +57,14 @@ public final class LocalFileObject: FileObject {
allValues[URLResourceKey.fileAllocatedSizeKey.rawValue] = Int(exactly: newValue) ?? Int.max
}
}
open var rev: String? {
get {
let data = allValues[URLResourceKey.generationIdentifierKey.rawValue] as? Data
return data?.map { String(format: "%02hhx", $0) }.joined()
}
}
}
internal class LocalFolderMonitor {
@@ -190,7 +204,7 @@ open class LocalOperationHandle: OperationHandle {
public let operationType: FileOperationType
init (operationType: FileOperationType, baseURL: URL?) {
self.baseURL = baseURL ?? LocalFileProvider.defaultBaseURL()
self.baseURL = baseURL ?? URL(fileURLWithPath: "/")
self.operationType = operationType
}
+13 -5
View File
@@ -24,11 +24,13 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
}
open var currentPath: String
open var dispatch_queue: DispatchQueue {
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue {
willSet {
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
}
}
open weak var delegate: FileProviderDelegate?
open let credential: URLCredential?
open private(set) var cache: URLCache?
@@ -51,7 +53,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
}
public init? (credential: URLCredential?, serverURL: URL? = nil, drive: String = "root", cache: URLCache? = nil) {
self.baseURL = serverURL ?? URL(string: "https://api.onedrive.com")
self.baseURL = (serverURL ?? URL(string: "https://api.onedrive.com/")!).appendingPathComponent("")
self.drive = drive
self.isPathRelative = true
self.currentPath = ""
@@ -59,7 +61,9 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
self.validatingCache = true
self.cache = cache
self.credential = credential
dispatch_queue = DispatchQueue(label: "FileProvider.\(OneDriveFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent)
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: DispatchQueue.Attributes.concurrent)
operation_queue = OperationQueue()
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
}
deinit {
@@ -83,7 +87,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
dbError = 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 = self.mapToFileObject(json) {
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) {
fileObject = file
}
}
@@ -218,13 +222,17 @@ extension OneDriveFileProvider: FileProviderOperations {
extension OneDriveFileProvider: FileProviderReadWrite {
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
if length == 0 {
return nil
}
let opType = FileOperationType.fetch(path: path)
let url = URL(string: escaped(path: path) + ":/content", relativeTo: driveURL)!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
if length > 0 {
request.setValue("bytes=\(offset)-\(offset + length)", forHTTPHeaderField: "Range")
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
} else if offset > 0 && length < 0 {
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
}
+27 -27
View File
@@ -19,10 +19,32 @@ public struct FileProviderOneDriveError: Error, CustomStringConvertible {
}
public final class OneDriveFileObject: FileObject {
internal init(name: String, path: String) {
super.init(absoluteURL: URL(string: path), name: name, path: path)
internal init(baseURL: URL?, name: String, path: String) {
var rpath = path
if path.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
}
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: path)!
super.init(url: url, name: name, path: path)
}
internal convenience init? (baseURL: URL?, drive: String, jsonStr: String) {
guard let json = jsonToDictionary(jsonStr) 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)
self.init(baseURL: baseURL, name: name, path: lPath)
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
self.modifiedDate = resolve(dateString: json["lastModifiedDateTime"] as? String ?? "")
self.creationDate = resolve(dateString: json["createdDateTime"] as? String ?? "")
self.type = (json["folder"] as? String) != nil ? .directory : .regular
self.id = json["id"] as? String
self.entryTag = json["eTag"] as? String
}
open internal(set) var id: String? {
get {
@@ -74,7 +96,7 @@ internal extension OneDriveFileProvider {
let json = jsonToDictionary(jsonStr)
if let entries = json?["value"] as? [AnyObject] , entries.count > 0 {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = self.mapToFileObject(entry) {
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: entry) {
files.append(file)
}
}
@@ -168,7 +190,7 @@ internal extension OneDriveFileProvider {
let json = jsonToDictionary(jsonStr)
if let entries = json?["value"] as? [AnyObject] , entries.count > 0 {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = self.mapToFileObject(entry) {
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: entry) {
foundItem(file)
}
}
@@ -190,28 +212,6 @@ internal extension OneDriveFileProvider {
// codebeat:enable[ARITY]
internal extension OneDriveFileProvider {
func mapToFileObject(_ jsonStr: String) -> OneDriveFileObject? {
guard let json = jsonToDictionary(jsonStr) else { return nil }
return self.mapToFileObject(json)
}
func mapToFileObject(_ json: [String: AnyObject]) -> OneDriveFileObject? {
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)
let fileObject = OneDriveFileObject(name: name, path: lPath)
if let webURL = json["webUrl"] as? String, let absolluteURL = URL(string: webURL) {
fileObject.absoluteURL = absolluteURL
}
fileObject.size = (json["size"] as? NSNumber)?.int64Value ?? -1
fileObject.modifiedDate = resolve(dateString: json["lastModifiedDateTime"] as? String ?? "")
fileObject.creationDate = resolve(dateString: json["createdDateTime"] as? String ?? "")
fileObject.type = (json["folder"] as? String) != nil ? .directory : .regular
fileObject.id = json["id"] as? String
fileObject.entryTag = json["eTag"] as? String
return fileObject
}
static let dateFormatter = DateFormatter()
static let decimalFormatter = NumberFormatter()
@@ -252,7 +252,7 @@ internal extension OneDriveFileProvider {
keys.append("Duration")
dic["Duration"] = OneDriveFileProvider.formatshort(interval: TimeInterval(duration) / 1000)
}
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = self.resolve(dateString: timeTakenStr) {
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = resolve(dateString: timeTakenStr) {
keys.append("Date taken")
OneDriveFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dic["Date taken"] = OneDriveFileProvider.dateFormatter.string(from: timeTaken)
+7 -3
View File
@@ -9,11 +9,13 @@
import Foundation
class SMBFileProvider: FileProvider, FileProviderMonitor {
open static var type: String = "Samba"
open var isPathRelative: Bool = true
open var baseURL: URL?
open var currentPath: String = ""
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue
open weak var delegate: FileProviderDelegate?
open let credential: URLCredential?
@@ -23,9 +25,11 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
guard baseURL.uw_scheme.lowercased() == "smb" else {
return nil
}
self.baseURL = baseURL
dispatch_queue = DispatchQueue(label: "FileProvider.\(SMBFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent)
//let url = baseURL.uw_absoluteString
self.baseURL = baseURL.appendingPathComponent("")
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: DispatchQueue.Attributes.concurrent)
operation_queue = OperationQueue()
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
self.credential = credential
}
+151 -124
View File
@@ -8,30 +8,6 @@
import Foundation
public final class WebDavFileObject: FileObject {
internal init(absoluteURL: URL, name: String, path: String) {
super.init(absoluteURL: absoluteURL, name: name, path: path)
}
open internal(set) var contentType: String {
get {
return allValues["NSURLContentTypeKey"] as? String ?? ""
}
set {
allValues["NSURLContentTypeKey"] = newValue
}
}
open internal(set) var entryTag: String? {
get {
return allValues["NSURLEntryTagKey"] as? String
}
set {
allValues["NSURLEntryTagKey"] = newValue
}
}
}
/// Because this class uses NSURLSession, it's necessary to disable App Transport Security
/// in case of using this class with unencrypted HTTP connection.
@@ -40,11 +16,14 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
open let isPathRelative: Bool
open let baseURL: URL?
open var currentPath: String
public var dispatch_queue: DispatchQueue {
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue {
willSet {
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
}
}
public weak var delegate: FileProviderDelegate?
open let credential: URLCredential?
open private(set) var cache: URLCache?
@@ -70,14 +49,16 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
if !["http", "https"].contains(baseURL.uw_scheme.lowercased()) {
return nil
}
self.baseURL = baseURL
self.baseURL = baseURL.appendingPathComponent("")
self.isPathRelative = true
self.currentPath = ""
self.useCache = false
self.validatingCache = true
self.cache = cache
self.credential = credential
dispatch_queue = DispatchQueue(label: "FileProvider.\(WebDAVFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent)
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: DispatchQueue.Attributes.concurrent)
operation_queue = OperationQueue()
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
}
deinit {
@@ -86,7 +67,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
let opType = FileOperationType.fetch(path: path)
let url = absoluteURL(path)
let url = self.url(of: path).appendingPathComponent("")
var request = URLRequest(url: url)
request.httpMethod = "PROPFIND"
request.setValue("1", forHTTPHeaderField: "Depth")
@@ -100,12 +81,12 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
}
var fileObjects = [WebDavFileObject]()
if let data = data {
let xresponse = self.parseXMLResponse(data)
for attr in xresponse {
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
for attr in xresponse where attr.href != url {
if attr.href.path == url.path {
continue
}
fileObjects.append(self.mapToFileObject(attr))
fileObjects.append(WebDavFileObject(attr))
}
}
completionHandler(fileObjects, responseError ?? error)
@@ -113,7 +94,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
}
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
let url = absoluteURL(path)
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PROPFIND"
request.setValue("1", forHTTPHeaderField: "Depth")
@@ -126,9 +107,9 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
responseError = FileProviderWebDavError(code: rCode, url: url)
}
if let data = data {
let xresponse = self.parseXMLResponse(data)
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
if let attr = xresponse.first {
completionHandler(self.mapToFileObject(attr), responseError ?? error)
completionHandler(WebDavFileObject(attr), responseError ?? error)
return
}
}
@@ -153,7 +134,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
var totalSize: Int64 = -1
var usedSize: Int64 = 0
if let data = data {
let xresponse = self.parseXMLResponse(data)
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
if let attr = xresponse.first {
totalSize = Int64(attr.prop["quota-available-bytes"] ?? "") ?? -1
usedSize = Int64(attr.prop["quota-used-bytes"] ?? "") ?? 0
@@ -173,7 +154,7 @@ extension WebDAVFileProvider: FileProviderOperations {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = absoluteURL((atPath as NSString).appendingPathComponent(folderName) + "/")
let url = self.url(of: atPath).appendingPathComponent(folderName, isDirectory: true)
var request = URLRequest(url: url)
request.httpMethod = "MKCOL"
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
@@ -195,7 +176,7 @@ extension WebDAVFileProvider: FileProviderOperations {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = absoluteURL(path).appendingPathComponent(fileName)
let url = self.url(of: path).appendingPathComponent(fileName)
var request = URLRequest(url: url)
request.httpMethod = "PUT"
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
@@ -239,10 +220,10 @@ extension WebDAVFileProvider: FileProviderOperations {
}
func doOperation(operation opType: FileOperationType, overwrite: Bool? = nil, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let sourceURL = absoluteURL(opType.source!)
let sourceURL = self.url(of:opType.source!)
var request = URLRequest(url: sourceURL)
if let dest = opType.destination {
request.setValue(absoluteURL(dest).absoluteString, forHTTPHeaderField: "Destination")
request.setValue(url(of:dest).absoluteString, forHTTPHeaderField: "Destination")
}
switch opType {
case .copy:
@@ -265,7 +246,7 @@ extension WebDAVFileProvider: FileProviderOperations {
responseError = FileProviderWebDavError(code: code, url: sourceURL)
}
if code == .multiStatus, let data = data {
let xresponses = self.parseXMLResponse(data)
let xresponses = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
for xresponse in xresponses where (xresponse.status ?? 0) >= 300 {
completionHandler?(FileProviderWebDavError(code: code, url: sourceURL))
}
@@ -289,7 +270,7 @@ extension WebDAVFileProvider: FileProviderOperations {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = absoluteURL(toPath)
let url = self.url(of:toPath)
var request = URLRequest(url: url)
request.httpMethod = "PUT"
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
@@ -311,7 +292,7 @@ extension WebDAVFileProvider: FileProviderOperations {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = absoluteURL(path)
let url = self.url(of:path)
let request = URLRequest(url: url)
let task = session.downloadTask(with: request, completionHandler: { (sourceFileURL, response, error) in
var responseError: FileProviderWebDavError?
@@ -338,12 +319,16 @@ extension WebDAVFileProvider: FileProviderOperations {
extension WebDAVFileProvider: FileProviderReadWrite {
@discardableResult
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
if length == 0 {
return nil
}
let opType = FileOperationType.fetch(path: path)
let url = absoluteURL(path)
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "GET"
if length > 0 {
request.setValue("bytes=\(offset)-\(offset + length)", forHTTPHeaderField: "Range")
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
} else if offset > 0 && length < 0 {
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
}
@@ -365,7 +350,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
return nil
}
// FIXME: lock destination before writing process
let url = atomically ? absoluteURL(path).appendingPathExtension("tmp") : absoluteURL(path)
let url = atomically ? self.url(of: path).appendingPathExtension("tmp") : self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PUT"
if !overwrite {
@@ -374,7 +359,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, url: self.absoluteURL(path))
responseError = FileProviderWebDavError(code: rCode, url: self.url(of: path))
}
defer {
self.delegateNotify(opType, error: responseError ?? error)
@@ -393,7 +378,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
}
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
let url = absoluteURL(path)
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PROPFIND"
//request.setValue("1", forHTTPHeaderField: "Depth")
@@ -406,14 +391,14 @@ extension WebDAVFileProvider: FileProviderReadWrite {
responseError = FileProviderWebDavError(code: rCode, url: url)
}
if let data = data {
let xresponse = self.parseXMLResponse(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 = self.mapToFileObject(attr)
let fileObject = WebDavFileObject(attr)
fileObjects.append(fileObject)
foundItemHandler?(fileObject)
}
@@ -431,9 +416,10 @@ extension WebDAVFileProvider: FileProviderReadWrite {
* A messy approach is listing a directory with an interval period and compare
* with previous results
*/
NotImplemented()
}
fileprivate func unregisterNotifcation(path: String) {
NotImplemented()
}
// TODO: implements methods for lock mechanism
}
@@ -453,37 +439,34 @@ extension WebDAVFileProvider: FileProvider {
// MARK: WEBDAV XML response implementation
internal extension WebDAVFileProvider {
struct DavResponse {
let href: URL
let hrefString: String
let status: Int?
let prop: [String: String]
fileprivate func delegateNotify(_ operation: FileOperationType, error: Error?) {
DispatchQueue.main.async(execute: {
if error == nil {
self.delegate?.fileproviderSucceed(self, operation: operation)
} else {
self.delegate?.fileproviderFailed(self, operation: operation)
}
})
}
}
struct DavResponse {
let href: URL
let hrefString: String
let status: Int?
let prop: [String: String]
fileprivate func parseXMLResponse(_ response: Data) -> [DavResponse] {
var result = [DavResponse]()
do {
let xml = try AEXMLDocument(xml: response)
var rootnode = xml.root
var responsetag = "response"
for node in rootnode.all ?? [] where node.name.lowercased().hasSuffix("multistatus") {
rootnode = node
init? (_ node: AEXMLElement, baseURL: URL?) {
func removeSlash(_ str: String) -> String {
if str.hasPrefix("/") {
return str.substring(from: str.index(after: str.startIndex))
} else {
return str
}
for node in rootnode.children where node.name.lowercased().hasSuffix("response") {
responsetag = node.name
break
}
for responseNode in rootnode[responsetag].all ?? [] {
if let davResponse = mapNodeToDavResponse(responseNode) {
result.append(davResponse)
}
}
} catch _ {
}
return result
}
fileprivate func mapNodeToDavResponse(_ node: AEXMLElement) -> DavResponse? {
// find node names with namespace
var hreftag = "href"
var statustag = "status"
var propstattag = "propstat"
@@ -498,63 +481,107 @@ internal extension WebDAVFileProvider {
propstattag = node.name
}
}
let href = node[hreftag].value
if let href = href, let hrefURL = URL(string: href) {
var status: Int?
let statusDesc = (node[statustag].string).components(separatedBy: " ")
if statusDesc.count > 2 {
status = Int(statusDesc[1])
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 hrefURL = URL(string: removeSlash(relativePath), relativeTo: baseURL) ?? baseURL
guard let href = hrefURL?.standardized else { return nil }
// reading status and properties
var status: Int?
let statusDesc = (node[statustag].string).components(separatedBy: " ")
if statusDesc.count > 2 {
status = Int(statusDesc[1])
}
var propDic = [String: String]()
let propStatNode = node[propstattag]
for node in propStatNode.children where node.name.lowercased().hasSuffix("status"){
statustag = node.name
break
}
let statusDesc2 = (propStatNode[statustag].string).components(separatedBy: " ")
if statusDesc2.count > 2 {
status = Int(statusDesc2[1])
}
var proptag = "prop"
for tnode in propStatNode.children where tnode.name.lowercased().hasSuffix("prop") {
proptag = tnode.name
break
}
for propItemNode in propStatNode[proptag].children {
propDic[propItemNode.name.components(separatedBy: ":").last!.lowercased()] = propItemNode.value
if propItemNode.name.hasSuffix("resourcetype") && propItemNode.xml.contains("collection") {
propDic["getcontenttype"] = "httpd/unix-directory"
}
var propDic = [String: String]()
let propStatNode = node[propstattag]
for node in propStatNode.children where node.name.lowercased().hasSuffix("status"){
statustag = node.name
}
self.href = href
self.hrefString = hrefString
self.status = status
self.prop = propDic
}
static func parse(xmlResponse: Data, baseURL: URL?) -> [DavResponse] {
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
}
for node in rootnode.children where node.name.lowercased().hasSuffix("response") {
responsetag = node.name
break
}
let statusDesc2 = (propStatNode[statustag].string).components(separatedBy: " ")
if statusDesc2.count > 2 {
status = Int(statusDesc2[1])
}
var proptag = "prop"
for tnode in propStatNode.children where tnode.name.lowercased().hasSuffix("prop") {
proptag = tnode.name
break
}
for propItemNode in propStatNode[proptag].children {
propDic[propItemNode.name.components(separatedBy: ":").last!.lowercased()] = propItemNode.value
if propItemNode.name.hasSuffix("resourcetype") && propItemNode.xml.contains("collection") {
propDic["getcontenttype"] = "httpd/unix-directory"
for responseNode in rootnode[responsetag].all ?? [] {
if let davResponse = DavResponse(responseNode, baseURL: baseURL) {
result.append(davResponse)
}
}
return DavResponse(href: hrefURL, hrefString: href, status: status, prop: propDic)
} catch _ {
}
return nil
return result
}
fileprivate func mapToFileObject(_ davResponse: DavResponse) -> WebDavFileObject {
var href = davResponse.href
if href.baseURL == nil {
href = absoluteURL(href.path)
}
}
public final class WebDavFileObject: FileObject {
internal init(_ davResponse: DavResponse) {
let href = davResponse.href
let name = davResponse.prop["displayname"] ?? (davResponse.hrefString.removingPercentEncoding! as NSString).lastPathComponent
let fileObject = WebDavFileObject(absoluteURL: href, name: name, path: href.path)
fileObject.size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown
fileObject.creationDate = self.resolve(dateString: davResponse.prop["creationdate"] ?? "")
fileObject.modifiedDate = self.resolve(dateString: davResponse.prop["getlastmodified"] ?? "")
fileObject.contentType = davResponse.prop["getcontenttype"] ?? "octet/stream"
fileObject.type = fileObject.contentType == "httpd/unix-directory" ? .directory : .regular
fileObject.entryTag = davResponse.prop["getetag"]
return fileObject
let relativePath = href.relativePath
let path = relativePath.hasPrefix("/") ? relativePath : ("/" + relativePath)
super.init(url: href, name: name, path: path)
self.size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown
self.creationDate = resolve(dateString: davResponse.prop["creationdate"] ?? "")
self.modifiedDate = resolve(dateString: davResponse.prop["getlastmodified"] ?? "")
self.contentType = davResponse.prop["getcontenttype"] ?? "octet/stream"
self.isHidden = (Int(davResponse.prop["ishidden"] ?? "0") ?? 0) > 0
self.type = self.contentType == "httpd/unix-directory" ? .directory : .regular
self.entryTag = davResponse.prop["getetag"]
}
fileprivate func delegateNotify(_ operation: FileOperationType, error: Error?) {
DispatchQueue.main.async(execute: {
if error == nil {
self.delegate?.fileproviderSucceed(self, operation: operation)
} else {
self.delegate?.fileproviderFailed(self, operation: operation)
}
})
/// MIME type of the file
open internal(set) var contentType: String {
get {
return allValues["NSURLContentTypeKey"] as? String ?? ""
}
set {
allValues["NSURLContentTypeKey"] = newValue
}
}
/// HTTP E-Tag, can be used to mark changed files
open internal(set) var entryTag: String? {
get {
return allValues["NSURLEntryTagKey"] as? String
}
set {
allValues["NSURLEntryTagKey"] = newValue
}
}
}