Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72520973e9 | |||
| 63016285af | |||
| 3245c9df03 | |||
| ee1fa89747 | |||
| 4a6b25deac | |||
| 2423cbd0f6 | |||
| 480099ca8a | |||
| 755bf6bf84 | |||
| 525825ff5d | |||
| a35384dc31 |
+21
-8
@@ -3,30 +3,43 @@ language: objective-c
|
||||
osx_image: xcode8.2
|
||||
|
||||
xcode_project: FileProvider.xcodeproj
|
||||
|
||||
env:
|
||||
global:
|
||||
- PROJECTNAME="FileProvider"
|
||||
- FRAMEWORK_NAME="FileProvider.framework"
|
||||
matrix:
|
||||
- SHCEME="FileProvider OSX" SDK="macosx" ACTION="build"
|
||||
- SHCEME="FileProvider iOS" SDK="iphonesimulator" ACTION="build"
|
||||
- SHCEME="FileProvider tvOS" SDK="appletvsimulator" ACTION="build"
|
||||
# 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
|
||||
- brew outdated carthage || brew upgrade carthage
|
||||
|
||||
script:
|
||||
- set pipefail
|
||||
- xcodebuild -version
|
||||
- xcodebuild -project $PROJECT.xcodeproj -scheme "$SCHEME" -sdk $SDK $ACTION ONLY_ACTIVE_ARCH=NO | xcpretty
|
||||
# - pod lib lint --quick
|
||||
- pod lib lint --quick
|
||||
# - xcodebuild -project $PROJECTNAME.xcodeproj -scheme "$SCHEME" -sdk $SDK $ACTION ONLY_ACTIVE_ARCH=NO
|
||||
- xcodebuild -project $PROJECTNAME.xcodeproj -scheme "FileProvider OSX" -sdk macosx build | xcpretty
|
||||
- xcodebuild -project $PROJECTNAME.xcodeproj -scheme "FileProvider iOS" -sdk iphonesimulator build ONLY_ACTIVE_ARCH=NO | xcpretty
|
||||
- xcodebuild -project $PROJECTNAME.xcodeproj -scheme "FileProvider tvOS" -sdk appletvsimulator build ONLY_ACTIVE_ARCH=NO | xcpretty
|
||||
|
||||
# after_success:
|
||||
# - bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
before_deploy:
|
||||
- carthage build --no-skip-current
|
||||
- brew update
|
||||
- brew outdated carthage || brew upgrade carthage
|
||||
- carthage version
|
||||
- carthage archive $PROJECTNAME
|
||||
|
||||
deploy:
|
||||
file: $PROJECTNAME.framework.zip
|
||||
provider: releases
|
||||
file: $FRAMEWORK_NAME.zip
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: amosavian/FileProvider
|
||||
tags: true
|
||||
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
|
||||
#
|
||||
|
||||
s.name = "FileProvider"
|
||||
s.version = "0.12.1"
|
||||
s.version = "0.12.3"
|
||||
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.
|
||||
|
||||
@@ -603,7 +603,7 @@
|
||||
799396601D48B7BF00086753 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.12.1;
|
||||
BUNDLE_VERSION_STRING = 0.12.3;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
@@ -633,7 +633,7 @@
|
||||
799396611D48B7BF00086753 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.12.1;
|
||||
BUNDLE_VERSION_STRING = 0.12.3;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
|
||||
@@ -31,15 +31,15 @@ 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] **CloudFileProvider** A wrapper around app's ubiquitous container to iCloud Drive in iOS 8+ API.
|
||||
- [x] **WebDAVFileProvider** WebDAV protocol is defacto file transmission standard, replaced FTP.
|
||||
- [x] **DropboxFileProvider** A wrapper around Dropbox REST API.
|
||||
- [x] **DropboxFileProvider** A wrapper around Dropbox Web API.
|
||||
* For now it has limitation in uploading files up to 150MB.
|
||||
- [x] **OneDriveFileProvider** A wrapper around OneDrive REST API, works with `onedrive.com` and compatible (business) servers.
|
||||
* For now it has limitation in uploading files up to 100MB.
|
||||
- [ ] **GoogleFileProvider** A wrapper around Goodle Drive REST API.
|
||||
- [ ] **AmazonS3FileProvider** Amazon storage backend. Used by many sites.
|
||||
- [ ] **SMBFileProvider** SMB2/3 introduced in 2006, which is a file and printer sharing protocol originated from Microsoft Windows and now is replacing AFP protocol on macOS.
|
||||
* Data types and some basic functions are implemented but *main interface is not implemented yet!*
|
||||
* SMB1/CIFS is depericated and very tricky to be implemented
|
||||
* Data types and some basic functions are implemented but *main interface is not implemented yet!*.
|
||||
* SMB1/CIFS is deprecated and very tricky to be implemented.
|
||||
- [ ] **FTPFileProvider** while deprecated in 1990s, it's still in use on some Web hosts.
|
||||
|
||||
## Requirements
|
||||
@@ -69,7 +69,7 @@ github "amosavian/FileProvider"
|
||||
Or to use in Swift Package Manager add this line in `Dependencies`:
|
||||
|
||||
```swift
|
||||
.Package(url: "https://github.com/amosavian/FileProvider.git", majorVersion: 0, minorVersion: 8)
|
||||
.Package(url: "https://github.com/amosavian/FileProvider.git", majorVersion: 0, minorVersion: 12)
|
||||
```
|
||||
|
||||
### Manually
|
||||
|
||||
@@ -11,7 +11,8 @@ import Foundation
|
||||
open class CloudFileProvider: LocalFileProvider {
|
||||
open override class var type: String { return "iCloudDrive" }
|
||||
|
||||
/// Actually is readonly, value is true
|
||||
/// Forces file operations to use `NSFileCoordinating`,
|
||||
/// Actually this is readonly, and value is always true.
|
||||
override open var isCoorinating: Bool {
|
||||
get {
|
||||
return true
|
||||
@@ -24,6 +25,9 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
/// The fully-qualified container identifier for an iCloud container directory.
|
||||
open fileprivate(set) var containerId: String?
|
||||
|
||||
/// Scope of container, indicates user can manipulate data/files or not.
|
||||
open fileprivate(set) var scope: UbiquitousScope
|
||||
|
||||
/**
|
||||
Initializes the provider for the iCloud container associated with the specified identifier and
|
||||
establishes access to that container.
|
||||
@@ -31,10 +35,11 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
- Important: Do not call this method from your app’s main thread. Because this method might take a nontrivial amount of time to set up iCloud and return the requested URL, you should always call it from a secondary thread.
|
||||
|
||||
- Parameter containerId: The fully-qualified container identifier for an iCloud container directory. The string you specify must not contain wildcards and must be of the form `<TEAMID>.<CONTAINER>`, where `<TEAMID>` is your development team ID and `<CONTAINER>` is the bundle identifier of the container you want to access.\
|
||||
The container identifiers for your app must be declared in the `com.apple.developer.ubiquity-container-identifiers` array of the `.entitlements` property list file in your Xcode project.\
|
||||
If you specify nil for this parameter, this method uses the first container listed in the `com.apple.developer.ubiquity-container-identifiers` entitlement array.
|
||||
The container identifiers for your app must be declared in the `com.apple.developer.ubiquity-container-identifiers` array of the `.entitlements` property list file in your Xcode project.\
|
||||
If you specify nil for this parameter, this method uses the first container listed in the `com.apple.developer.ubiquity-container-identifiers` entitlement array.
|
||||
- Parameter scope: Use `.documents` (default) to put documents that the user is allowed to access inside a Documents subdirectory. Otherwise use `.data` to store user-related data files that your app needs to share but that are not files you want the user to manipulate directly.
|
||||
*/
|
||||
public init? (containerId: String?) {
|
||||
public init? (containerId: String?, scope: UbiquitousScope = .documents) {
|
||||
assert(!Thread.isMainThread, "LocalFileProvider.init(containerId:) is not recommended to be executed on Main Thread.")
|
||||
guard FileManager.default.ubiquityIdentityToken == nil else {
|
||||
return nil
|
||||
@@ -43,7 +48,14 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
return nil
|
||||
}
|
||||
self.containerId = containerId
|
||||
let baseURL = ubiquityURL.standardized.appendingPathComponent("Documents/")
|
||||
self.scope = scope
|
||||
let baseURL: URL
|
||||
if scope == .documents {
|
||||
baseURL = ubiquityURL.standardized.appendingPathComponent("Documents/")
|
||||
} else {
|
||||
baseURL = ubiquityURL.standardized
|
||||
}
|
||||
|
||||
super.init(baseURL: baseURL)
|
||||
self.isCoorinating = true
|
||||
|
||||
@@ -57,12 +69,14 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
try? fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
// FIXME: create runloop for dispatch_queue, start query on it
|
||||
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]
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
@@ -91,9 +105,14 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
query.stop()
|
||||
completionHandler(contents, nil)
|
||||
})
|
||||
query.start()
|
||||
DispatchQueue.main.async {
|
||||
if !query.start() {
|
||||
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +126,7 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
let pathURL = self.url(of: path)
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemPathKey, pathURL.path)
|
||||
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
@@ -130,7 +149,11 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
completionHandler(nil, noFileError)
|
||||
}
|
||||
})
|
||||
query.start()
|
||||
DispatchQueue.main.async {
|
||||
if !query.start() {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +262,7 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
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]
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
|
||||
var lastReportedCount = 0
|
||||
|
||||
@@ -302,18 +325,22 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
completionHandler(contents, nil)
|
||||
})
|
||||
|
||||
query.start()
|
||||
DispatchQueue.main.async {
|
||||
if !query.start() {
|
||||
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var monitors = [URL: (NSMetadataQuery, NSObjectProtocol)]()
|
||||
//
|
||||
fileprivate var monitors = [String: (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]
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
|
||||
let updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: nil, using: { (notification) in
|
||||
|
||||
@@ -324,24 +351,25 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
query.enableUpdates()
|
||||
})
|
||||
|
||||
query.start()
|
||||
|
||||
monitors[pathURL] = (query, updateObserver)
|
||||
DispatchQueue.main.async {
|
||||
if query.start() {
|
||||
self.monitors[path] = (query, updateObserver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open override func unregisterNotifcation(path: String) {
|
||||
let key = url(of: path)
|
||||
guard let (query, observer) = monitors[key] else {
|
||||
guard let (query, observer) = monitors[path] else {
|
||||
return
|
||||
}
|
||||
query.disableUpdates()
|
||||
query.stop()
|
||||
monitors.removeValue(forKey: key)
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
monitors.removeValue(forKey: path)
|
||||
}
|
||||
|
||||
open override func isRegisteredForNotification(path: String) -> Bool {
|
||||
return monitors[url(of: path)] != nil
|
||||
return monitors[path] != nil
|
||||
}
|
||||
|
||||
open override func copy(with zone: NSZone? = nil) -> Any {
|
||||
@@ -401,6 +429,38 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public enum UbiquitousScope: RawRepresentable {
|
||||
/// Search all files not in the Documents directories of the app’s iCloud container directories.
|
||||
/// Use this scope to store user-related data files that your app needs to share
|
||||
/// but that are not files you want the user to manipulate directly.
|
||||
case data
|
||||
/// Search all files in the Documents directories of the app’s iCloud container directories.
|
||||
/// Put documents that the user is allowed to access inside a Documents subdirectory.
|
||||
case documents
|
||||
|
||||
public typealias RawValue = String
|
||||
|
||||
public init? (rawValue: String) {
|
||||
switch rawValue {
|
||||
case NSMetadataQueryUbiquitousDataScope:
|
||||
self = .data
|
||||
case NSMetadataQueryUbiquitousDocumentsScope:
|
||||
self = .documents
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .data:
|
||||
return NSMetadataQueryUbiquitousDataScope
|
||||
case .documents:
|
||||
return NSMetadataQueryUbiquitousDocumentsScope
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class CloudOperationHandle: OperationHandle {
|
||||
public let baseURL: URL?
|
||||
public let operationType: FileOperationType
|
||||
@@ -458,11 +518,12 @@ open class CloudOperationHandle: OperationHandle {
|
||||
fileprivate static func getMetadataItem(url: URL) -> NSMetadataItem? {
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path)
|
||||
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
|
||||
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]
|
||||
|
||||
var item: NSMetadataItem?
|
||||
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
@@ -479,8 +540,9 @@ open class CloudOperationHandle: OperationHandle {
|
||||
|
||||
})
|
||||
|
||||
group.enter()
|
||||
query.start()
|
||||
DispatchQueue.main.async {
|
||||
query.start()
|
||||
}
|
||||
_ = group.wait(timeout: DispatchTime.now() + 30)
|
||||
return item
|
||||
}
|
||||
|
||||
@@ -133,8 +133,6 @@ open class DropboxFileProvider: FileProviderBasicRemote {
|
||||
}
|
||||
|
||||
extension DropboxFileProvider: FileProviderOperations {
|
||||
|
||||
|
||||
public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
|
||||
return doOperation(.create(path: path), completionHandler: completionHandler)
|
||||
@@ -244,7 +242,10 @@ 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 {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -305,12 +306,12 @@ extension DropboxFileProvider: FileProviderReadWrite {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
// TODO: Implement /copy_reference, /get_account & /get_current_account
|
||||
// TODO: Implement /get_account & /get_current_account
|
||||
}
|
||||
|
||||
extension DropboxFileProvider {
|
||||
/// *DEPRECATED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead.
|
||||
@available(*, deprecated, message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
|
||||
@available(*, deprecated, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
|
||||
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
|
||||
self.publicLink(to: path) { (url, file, _, error) in
|
||||
completionHandler(url, file, error)
|
||||
@@ -318,7 +319,7 @@ extension DropboxFileProvider {
|
||||
}
|
||||
|
||||
/// *DEPRECATED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead.
|
||||
@available(*, deprecated, message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
|
||||
@available(*, deprecated, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
|
||||
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
self.publicLink(to: path) { (url, file, expiration, error) in
|
||||
completionHandler(url, file, expiration, error)
|
||||
|
||||
@@ -43,28 +43,28 @@ public final class DropboxFileObject: FileObject {
|
||||
|
||||
open internal(set) var serverTime: Date? {
|
||||
get {
|
||||
return allValues["NSURLServerDateKey"] as? Date
|
||||
return allValues[.serverDate] as? Date
|
||||
}
|
||||
set {
|
||||
allValues["NSURLServerDateKey"] = newValue
|
||||
allValues[.serverDate] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
open internal(set) var id: String? {
|
||||
get {
|
||||
return allValues["NSURLDocumentIdentifyKey"] as? String
|
||||
return allValues[.documentIdentifierKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues["NSURLDocumentIdentifyKey"] = newValue
|
||||
allValues[.documentIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
open internal(set) var rev: String? {
|
||||
get {
|
||||
return allValues[URLResourceKey.generationIdentifierKey.rawValue] as? String
|
||||
return allValues[.generationIdentifierKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.generationIdentifierKey.rawValue] = newValue
|
||||
allValues[.generationIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,6 @@ extension LocalFileProvider: ExtendedFileProvider {
|
||||
|
||||
completionHandler(dic, keys, nil)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +188,16 @@ public struct LocalFileInformationGenerator {
|
||||
}
|
||||
|
||||
static public var imageProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func simplify(_ top:Int64, _ bottom:Int64) -> (newTop:Int, newBottom:Int) {
|
||||
var x = top
|
||||
var y = bottom
|
||||
@@ -203,79 +212,57 @@ public struct LocalFileInformationGenerator {
|
||||
return(Int(newTopVal), Int(newBottomVal))
|
||||
}
|
||||
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
guard let cgDataRef = CGImageSourceCreateWithURL(fileURL as CFURL, nil), let cfImageDict = CGImageSourceCopyPropertiesAtIndex(cgDataRef, 0, nil) else {
|
||||
return (dic, keys)
|
||||
}
|
||||
let imageDict = cfImageDict as NSDictionary
|
||||
let tiffDict = imageDict[kCGImagePropertyTIFFDictionary as String] as? [String : AnyObject] ?? [:]
|
||||
let exifDict = imageDict[kCGImagePropertyExifDictionary as String] as? [String : AnyObject] ?? [:]
|
||||
if let pixelWidth: AnyObject = imageDict.object(forKey: kCGImagePropertyPixelWidth) as? NSNumber, let pixelHeight: AnyObject = imageDict.object(forKey: kCGImagePropertyPixelHeight) as? NSNumber {
|
||||
keys.append("Dimensions")
|
||||
dic["Dimensions"] = "\(pixelWidth)x\(pixelHeight)"
|
||||
}
|
||||
if let dpi = imageDict[kCGImagePropertyDPIWidth as String] {
|
||||
keys.append("DPI")
|
||||
dic["DPI"] = dpi
|
||||
}
|
||||
if let devicemake = tiffDict[kCGImagePropertyTIFFMake as String] {
|
||||
keys.append("Device make")
|
||||
dic["Device make"] = devicemake
|
||||
}
|
||||
if let devicemodel = tiffDict[kCGImagePropertyTIFFModel as String] {
|
||||
keys.append("Device model")
|
||||
dic["Device model"] = devicemodel
|
||||
}
|
||||
if let lensmodel = exifDict[kCGImagePropertyExifLensModel as String] {
|
||||
keys.append("Lens model")
|
||||
dic["Lens model"] = lensmodel
|
||||
}
|
||||
if let artist = tiffDict[kCGImagePropertyTIFFArtist as String] as? String , !artist.isEmpty {
|
||||
keys.append("Artist")
|
||||
dic["Artist"] = artist
|
||||
let tiffDict = imageDict[kCGImagePropertyTIFFDictionary as String] as? NSDictionary ?? [:]
|
||||
let exifDict = imageDict[kCGImagePropertyExifDictionary as String] as? NSDictionary ?? [:]
|
||||
if let pixelWidth = imageDict.object(forKey: kCGImagePropertyPixelWidth) as? NSNumber, let pixelHeight = imageDict.object(forKey: kCGImagePropertyPixelHeight) as? NSNumber {
|
||||
add(key: "Dimensions", value: "\(pixelWidth)x\(pixelHeight)")
|
||||
}
|
||||
|
||||
add(key: "DPI", value: imageDict[kCGImagePropertyDPIWidth as String])
|
||||
add(key: "Device make", value: tiffDict[kCGImagePropertyTIFFMake as String])
|
||||
add(key: "Device model", value: tiffDict[kCGImagePropertyTIFFModel as String])
|
||||
add(key: "Lens model", value: exifDict[kCGImagePropertyExifLensModel as String])
|
||||
add(key: "Artist", value: tiffDict[kCGImagePropertyTIFFArtist as String] as? String)
|
||||
if let cr = tiffDict[kCGImagePropertyTIFFCopyright as String] as? String , !cr.isEmpty {
|
||||
keys.append("Copyright")
|
||||
dic["Copyright"] = cr
|
||||
add(key: "Copyright", value: cr)
|
||||
|
||||
}
|
||||
if let date = tiffDict[kCGImagePropertyTIFFDateTime as String] as? String , !date.isEmpty {
|
||||
keys.append("Date taken")
|
||||
dic["Date taken"] = date
|
||||
add(key: "Date taken", value: date)
|
||||
}
|
||||
if let latitude = tiffDict[kCGImagePropertyGPSLatitude as String]?.doubleValue, let longitude = tiffDict[kCGImagePropertyGPSLongitude as String]?.doubleValue {
|
||||
keys.append("Location")
|
||||
dic["Location"] = "\(latitude), \(longitude)"
|
||||
if let latitude = tiffDict[kCGImagePropertyGPSLatitude as String] as? NSNumber, let longitude = tiffDict[kCGImagePropertyGPSLongitude as String] as? NSNumber {
|
||||
add(key: "Location", value: "\(latitude), \(longitude)")
|
||||
}
|
||||
if let colorspace = imageDict[kCGImagePropertyColorModel as String] {
|
||||
keys.append("Color space")
|
||||
dic["Color space"] = colorspace
|
||||
}
|
||||
if let focallen = exifDict[kCGImagePropertyExifFocalLength as String] {
|
||||
keys.append("Focal length")
|
||||
dic["Focal length"] = focallen
|
||||
}
|
||||
if let fnum = exifDict[kCGImagePropertyExifFNumber as String] {
|
||||
keys.append("F number")
|
||||
dic["F number"] = fnum
|
||||
}
|
||||
if let expprog = exifDict[kCGImagePropertyExifExposureProgram as String] {
|
||||
keys.append("Exposure program")
|
||||
dic["Exposure program"] = expprog
|
||||
}
|
||||
if let exp = exifDict[kCGImagePropertyExifExposureTime as String]?.doubleValue {
|
||||
let expfrac = simplify(Int64(exp * 10_000_000_000_000), 10_000_000_000_000)
|
||||
keys.append("Exposure time")
|
||||
dic["Exposure time"] = "\(expfrac.newTop)/\(expfrac.newBottom)"
|
||||
add(key: "Color space", value: imageDict[kCGImagePropertyColorModel as String])
|
||||
add(key: "Focal length", value: exifDict[kCGImagePropertyExifFocalLength as String])
|
||||
add(key: "F number", value: exifDict[kCGImagePropertyExifFNumber as String])
|
||||
add(key: "Exposure program", value: exifDict[kCGImagePropertyExifExposureProgram as String])
|
||||
|
||||
if let exp = exifDict[kCGImagePropertyExifExposureTime as String] as? NSNumber {
|
||||
let expfrac = simplify(Int64(exp.doubleValue * 10_000_000_000_000), 10_000_000_000_000)
|
||||
add(key: "Exposure time", value: "\(expfrac.newTop)/\(expfrac.newBottom)")
|
||||
}
|
||||
if let iso = exifDict[kCGImagePropertyExifISOSpeedRatings as String] as? NSArray , iso.count > 0 {
|
||||
keys.append("ISO speed")
|
||||
dic["ISO speed"] = iso[0]
|
||||
add(key: "ISO speed", value: iso[0])
|
||||
}
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
static var audioProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func makeDescription(_ key: String?) -> String? {
|
||||
guard let key = key else {
|
||||
return nil
|
||||
@@ -287,8 +274,6 @@ public struct LocalFileInformationGenerator {
|
||||
return newKey.capitalized
|
||||
}
|
||||
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
if FileManager.default.fileExists(atPath: fileURL.path) {
|
||||
let playerItem = AVPlayerItem(url: fileURL)
|
||||
let metadataList = playerItem.asset.commonMetadata
|
||||
@@ -301,12 +286,8 @@ public struct LocalFileInformationGenerator {
|
||||
}
|
||||
}
|
||||
if let ap = try? AVAudioPlayer(contentsOf: fileURL) {
|
||||
keys.append("Duration")
|
||||
dic["Duration"] = LocalFileProvider.formatshort(interval: ap.duration)
|
||||
if let bitRate = ap.settings[AVSampleRateKey] as? Int {
|
||||
keys.append("Bitrate")
|
||||
dic["Bitrate"] = bitRate
|
||||
}
|
||||
add(key: "Duration", value: LocalFileProvider.formatshort(interval: ap.duration))
|
||||
add(key: "Bitrate", value: ap.settings[AVSampleRateKey] as? Int)
|
||||
}
|
||||
}
|
||||
return (dic, keys)
|
||||
@@ -315,6 +296,14 @@ public struct LocalFileInformationGenerator {
|
||||
static public var videoProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if let audioprops = LocalFileInformationGenerator.audioProperties?(fileURL) {
|
||||
dic = audioprops.prop
|
||||
keys = audioprops.keys
|
||||
@@ -329,17 +318,14 @@ public struct LocalFileInformationGenerator {
|
||||
var bitrate: Float = 0
|
||||
let width = Int(videoTracks[0].naturalSize.width)
|
||||
let height = Int(videoTracks[0].naturalSize.height)
|
||||
keys.append("Dimensions")
|
||||
dic["Dimensions"] = "\(width)x\(height)"
|
||||
add(key: "Dimensions", value: "\(width)x\(height)")
|
||||
var duration: Int64 = 0
|
||||
for track in videoTracks {
|
||||
duration += track.timeRange.duration.timescale > 0 ? track.timeRange.duration.value / Int64(track.timeRange.duration.timescale) : 0
|
||||
bitrate += track.estimatedDataRate
|
||||
}
|
||||
keys.append("Duration")
|
||||
dic["Duration"] = LocalFileProvider.formatshort(interval: TimeInterval(duration))
|
||||
keys.append("Video Bitrate")
|
||||
dic["Video Bitrate"] = "\(Int(ceil(bitrate / 1000))) kbps"
|
||||
add(key: "Duration", value: LocalFileProvider.formatshort(interval: TimeInterval(duration)))
|
||||
add(key: "Video Bitrate", value: "\(Int(ceil(bitrate / 1000))) kbps")
|
||||
}
|
||||
let audioTracks = asset.tracks(withMediaType: AVMediaTypeAudio)
|
||||
// dic["Audio channels"] = audioTracks.count
|
||||
@@ -347,12 +333,21 @@ public struct LocalFileInformationGenerator {
|
||||
for track in audioTracks {
|
||||
bitrate += track.estimatedDataRate
|
||||
}
|
||||
keys.append("Audio Bitrate")
|
||||
dic["Audio Bitrate"] = "\(Int(ceil(bitrate / 1000))) kbps"
|
||||
add(key: "Audio Bitrate", value: "\(Int(ceil(bitrate / 1000))) kbps")
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
static public var pdfProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func getKey(_ key: String, from dict: CGPDFDictionaryRef) -> String? {
|
||||
var cfValue: CGPDFStringRef? = nil
|
||||
if (CGPDFDictionaryGetString(dict, key, &cfValue)), let value = CGPDFStringCopyTextString(cfValue!) {
|
||||
@@ -378,56 +373,40 @@ public struct LocalFileInformationGenerator {
|
||||
return nil
|
||||
}
|
||||
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
if let data = try? Data(contentsOf: fileURL), let provider = CGDataProvider(data: data as CFData), let reference = CGPDFDocument(provider), let dict = reference.info {
|
||||
if let title = getKey("Title", from: dict), !title.isEmpty {
|
||||
keys.append("Title")
|
||||
dic["Title"] = title
|
||||
add(key: "Title", value: title)
|
||||
}
|
||||
if let author = getKey("Author", from: dict), !author.isEmpty {
|
||||
keys.append("Author")
|
||||
dic["Author"] = author
|
||||
add(key: "Author", value: author)
|
||||
}
|
||||
if let subject = getKey("Subject", from: dict), !subject.isEmpty {
|
||||
keys.append("Subject")
|
||||
dic["Subject"] = subject
|
||||
add(key: "Subject", value: subject)
|
||||
}
|
||||
var majorVersion: Int32 = 0
|
||||
var minorVersion: Int32 = 0
|
||||
reference.getVersion(majorVersion: &majorVersion, minorVersion: &minorVersion)
|
||||
if majorVersion > 0 {
|
||||
keys.append("Version")
|
||||
dic["Version"] = String(majorVersion) + "." + String(minorVersion)
|
||||
}
|
||||
if reference.numberOfPages > 0 {
|
||||
keys.append("Pages")
|
||||
dic["Pages"] = reference.numberOfPages
|
||||
add(key: "Version", value: String(majorVersion) + "." + String(minorVersion))
|
||||
}
|
||||
add(key: "Pages", value: reference.numberOfPages)
|
||||
|
||||
if reference.numberOfPages > 0, let pageRef = reference.page(at: 1) {
|
||||
let size = pageRef.getBoxRect(CGPDFBox.mediaBox).size
|
||||
keys.append("Resolution")
|
||||
dic["Resolution"] = "\(Int(size.width))x\(Int(size.height))"
|
||||
add(key: "Resolution", value: "\(Int(size.width))x\(Int(size.height))")
|
||||
}
|
||||
if let creator = getKey("Creator", from: dict), !creator.isEmpty {
|
||||
keys.append("Content creator")
|
||||
dic["Content creator"] = creator
|
||||
add(key: "Content creator", value: creator)
|
||||
}
|
||||
if let creationDateString = getKey("CreationDate", from: dict), let creationDate = convertDate(creationDateString) {
|
||||
keys.append("Creation date")
|
||||
dic["Creation date"] = creationDate
|
||||
if let creationDateString = getKey("CreationDate", from: dict) {
|
||||
add(key: "Creation date", value: convertDate(creationDateString))
|
||||
}
|
||||
if let modifiedDateString = getKey("ModDate", from: dict), let modDate = convertDate(modifiedDateString) {
|
||||
keys.append("Modified date")
|
||||
dic["Modified date"] = modDate
|
||||
if let modifiedDateString = getKey("ModDate", from: dict) {
|
||||
add(key: "Modified date", value: convertDate(modifiedDateString))
|
||||
}
|
||||
keys.append("Security")
|
||||
dic["Security"] = reference.isEncrypted ? "Present" : "None"
|
||||
keys.append("Allows printing")
|
||||
dic["Allows printing"] = reference.allowsPrinting ? "Yes" : "No"
|
||||
keys.append("Allows copying")
|
||||
dic["Allows copying"] = reference.allowsCopying ? "Yes" : "No"
|
||||
add(key: "Security", value: reference.isEncrypted ? "Present" : "None")
|
||||
add(key: "Allows printing", value: reference.allowsPrinting ? "Yes" : "No")
|
||||
add(key: "Allows copying", value: reference.allowsCopying ? "Yes" : "No")
|
||||
}
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
+31
-24
@@ -11,21 +11,21 @@ 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]
|
||||
open internal(set) var allValues: [URLResourceKey: Any]
|
||||
|
||||
internal init(allValues: [String: Any]) {
|
||||
internal init(allValues: [URLResourceKey: Any]) {
|
||||
self.allValues = allValues
|
||||
}
|
||||
|
||||
internal init(url: URL, name: String, path: String) {
|
||||
self.allValues = [String: Any]()
|
||||
self.allValues = [URLResourceKey: Any]()
|
||||
self.url = url
|
||||
self.name = name
|
||||
self.path = path
|
||||
}
|
||||
|
||||
/// url to access the resource, not supported by Dropbox provider
|
||||
@available(*, deprecated, message: "Use url.absoluteURL instead.")
|
||||
@available(*, deprecated, renamed: "url", message: "Use url.absoluteURL instead.")
|
||||
open var absoluteURL: URL? {
|
||||
return url?.absoluteURL
|
||||
}
|
||||
@@ -34,75 +34,75 @@ open class FileObject {
|
||||
/// not supported by Dropbox provider.
|
||||
open internal(set) var url: URL? {
|
||||
get {
|
||||
return allValues["NSURLFileURLKey"] as? URL
|
||||
return allValues[.fileURL] as? URL
|
||||
}
|
||||
set {
|
||||
allValues["NSURLFileURLKey"] = newValue
|
||||
allValues[.fileURL] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Name of the file, usually equals with the last path component
|
||||
open internal(set) var name: String {
|
||||
get {
|
||||
return allValues[URLResourceKey.nameKey.rawValue] as! String
|
||||
return allValues[.nameKey] as! String
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.nameKey.rawValue] = newValue
|
||||
allValues[.nameKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Relative path of file object
|
||||
open internal(set) var path: String {
|
||||
get {
|
||||
return allValues[URLResourceKey.pathKey.rawValue] as! String
|
||||
return allValues[.pathKey] as! String
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.pathKey.rawValue] = newValue
|
||||
allValues[.pathKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Size of file on disk, return -1 for directories.
|
||||
open internal(set) var size: Int64 {
|
||||
get {
|
||||
return allValues[URLResourceKey.fileSizeKey.rawValue] as? Int64 ?? -1
|
||||
return allValues[.fileSizeKey] as? Int64 ?? -1
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.fileSizeKey.rawValue] = newValue
|
||||
allValues[.fileSizeKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The time contents of file has been created, returns nil if not set
|
||||
open internal(set) var creationDate: Date? {
|
||||
get {
|
||||
return allValues[URLResourceKey.creationDateKey.rawValue] as? Date
|
||||
return allValues[.creationDateKey] as? Date
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.creationDateKey.rawValue] = newValue
|
||||
allValues[.creationDateKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The time contents of file has been modified, returns nil if not set
|
||||
open internal(set) var modifiedDate: Date? {
|
||||
get {
|
||||
return allValues[URLResourceKey.contentModificationDateKey.rawValue] as? Date
|
||||
return allValues[.contentModificationDateKey] as? Date
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.contentModificationDateKey.rawValue] = newValue
|
||||
allValues[.contentModificationDateKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// return resource type of file, usually directory, regular or symLink
|
||||
open internal(set) var type: URLFileResourceType? {
|
||||
get {
|
||||
return allValues[URLResourceKey.fileResourceTypeKey.rawValue] as? URLFileResourceType
|
||||
return allValues[.fileResourceTypeKey] as? URLFileResourceType
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.fileResourceTypeKey.rawValue] = newValue
|
||||
allValues[.fileResourceTypeKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// **DEPRECATED:** Use `type` property instead.
|
||||
@available(*, deprecated, message: "Use type property instead.")
|
||||
/// **OBSOLETED:** Use `type` property instead.
|
||||
@available(*, obsoleted: 1.0, renamed: "type", message: "Use type property instead.")
|
||||
open var fileType: URLFileResourceType? {
|
||||
return self.type
|
||||
}
|
||||
@@ -111,20 +111,20 @@ open class FileObject {
|
||||
/// Setting this value on a file begining with dot has no effect
|
||||
open internal(set) var isHidden: Bool {
|
||||
get {
|
||||
return allValues[URLResourceKey.isHiddenKey.rawValue] as? Bool ?? false
|
||||
return allValues[.isHiddenKey] as? Bool ?? false
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.isHiddenKey.rawValue] = newValue
|
||||
allValues[.isHiddenKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// File can not be written
|
||||
open internal(set) var isReadOnly: Bool {
|
||||
get {
|
||||
return !(allValues[URLResourceKey.isWritableKey.rawValue] as? Bool ?? true)
|
||||
return !(allValues[.isWritableKey] as? Bool ?? true)
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.isWritableKey.rawValue] = !newValue
|
||||
allValues[.isWritableKey] = !newValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +296,13 @@ extension URLFileResourceType {
|
||||
}
|
||||
}
|
||||
|
||||
internal extension URLResourceKey {
|
||||
static let fileURL = URLResourceKey(rawValue: "NSURLFileURLKey")
|
||||
static let serverDate = URLResourceKey(rawValue: "NSURLServerDateKey")
|
||||
static let entryTag = URLResourceKey(rawValue: "NSURLEntryTagKey")
|
||||
static let mimeType = URLResourceKey(rawValue: "NSURLMIMETypeIdentifierKey")
|
||||
}
|
||||
|
||||
internal extension URL {
|
||||
var uw_scheme: String {
|
||||
return self.scheme ?? ""
|
||||
|
||||
@@ -492,6 +492,42 @@ public protocol FileProviderMonitor: FileProviderBasic {
|
||||
func isRegisteredForNotification(path: String) -> Bool
|
||||
}
|
||||
|
||||
public protocol FileProvideUndoable: FileProviderOperations {
|
||||
var undoManager: UndoManager? { get set }
|
||||
|
||||
func canUndo(handle: OperationHandle) -> Bool
|
||||
func canUndo(operation: FileOperationType) -> Bool
|
||||
}
|
||||
|
||||
public extension FileProvideUndoable {
|
||||
public func canUndo(operation: FileOperationType) -> Bool {
|
||||
return undoOperation(for: operation) != nil
|
||||
}
|
||||
|
||||
public func canUndo(handle: OperationHandle) -> Bool {
|
||||
return canUndo(operation: handle.operationType)
|
||||
}
|
||||
|
||||
internal func undoOperation(for operation: FileOperationType) -> FileOperationType? {
|
||||
switch operation {
|
||||
case .create(path: let path):
|
||||
return .remove(path: path)
|
||||
case .modify(path: _):
|
||||
return nil
|
||||
case .copy(source: _, destination: let dest):
|
||||
return .remove(path: dest)
|
||||
case .move(source: let source, destination: let dest):
|
||||
return .move(source: dest, destination: source)
|
||||
case .link(link: let link, target: _):
|
||||
return .remove(path: link)
|
||||
case .remove(path: _):
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public protocol FileProvider: FileProviderBasic, FileProviderOperations, FileProviderReadWrite, NSCopying {
|
||||
}
|
||||
|
||||
@@ -510,8 +546,8 @@ extension FileProviderBasic {
|
||||
return path.trimmingCharacters(in: pathTrimSet).addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
|
||||
}
|
||||
|
||||
/// **DEPRECATED:** Use `url(of:).absoluteURL` instead.
|
||||
@available(*, deprecated, message: "Use url(of:).absoluteURL instead.")
|
||||
/// **OBSOLETED:** Use `url(of:).absoluteURL` instead.
|
||||
@available(*, obsoleted: 1.0, renamed: "url(of:)", message: "Use url(of:).absoluteURL instead.")
|
||||
public func absoluteURL(_ path: String? = nil) -> URL {
|
||||
return url(of: path).absoluteURL
|
||||
}
|
||||
@@ -534,11 +570,20 @@ extension FileProviderBasic {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Returns the relative path of url, wothout percent encoding. Even if url is absolute or
|
||||
/// retrieved from another provider, it will try to resolve the url against `baseURL` of
|
||||
/// current provider. It's highly recomended to use this method for displaying purposes.
|
||||
///
|
||||
/// - Parameter url: Absolute url to file or directory.
|
||||
/// - Returns: A `String` contains relative path of url against base url.
|
||||
public func relativePathOf(url: URL) -> String {
|
||||
if url.baseURL == self.baseURL {
|
||||
// check if url derieved from current base url
|
||||
if url.relativeString.isEmpty, url.baseURL == self.baseURL {
|
||||
return url.relativePath.removingPercentEncoding!
|
||||
}
|
||||
|
||||
// resolve url string against baseurl
|
||||
guard let baseURL = self.baseURL?.standardizedFileURL else { return url.absoluteString }
|
||||
return url.standardizedFileURL.absoluteString.replacingOccurrences(of: baseURL.absoluteString, with: "/").removingPercentEncoding!
|
||||
}
|
||||
|
||||
+133
-244
@@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndoable {
|
||||
open class var type: String { return "Local" }
|
||||
open var isPathRelative: Bool
|
||||
open fileprivate(set) var baseURL: URL?
|
||||
@@ -22,6 +22,8 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
open private(set) var opFileManager = FileManager()
|
||||
fileprivate var fileProviderManagerDelegate: LocalFileProviderManagerDelegate? = nil
|
||||
|
||||
open var undoManager: UndoManager? = nil
|
||||
|
||||
/**
|
||||
Forces file operations to use `NSFileCoordinating`, should be set `true` if:
|
||||
- Files are on ubiquity (iCloud) container.
|
||||
@@ -112,7 +114,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
do {
|
||||
let contents = try self.fileManager.contentsOfDirectory(at: self.url(of: path), includingPropertiesForKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .isHiddenKey, .volumeIsReadOnlyKey], options: .skipsSubdirectoryDescendants)
|
||||
let contents = try self.fileManager.contentsOfDirectory(at: self.url(of: path), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants)
|
||||
let filesAttributes = contents.flatMap({ (fileURL) -> LocalFileObject? in
|
||||
let path = self.relativePathOf(url: fileURL)
|
||||
return LocalFileObject(fileWithPath: path, relativeTo: self.baseURL)
|
||||
@@ -125,9 +127,9 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) {
|
||||
let dict = (try? FileManager.default.attributesOfFileSystem(forPath: baseURL?.path ?? "/"))
|
||||
let totalSize = (dict?[.systemSize] as? NSNumber)?.int64Value ?? -1;
|
||||
let freeSize = (dict?[.systemFreeSize] as? NSNumber)?.int64Value ?? 0;
|
||||
let values = try? baseURL?.resourceValues(forKeys: [.volumeTotalCapacityKey, .volumeAvailableCapacityKey])
|
||||
let totalSize = Int64(values??.volumeTotalCapacity ?? -1)
|
||||
let freeSize = Int64(values??.volumeAvailableCapacity ?? 0)
|
||||
completionHandler(totalSize, totalSize - freeSize)
|
||||
}
|
||||
|
||||
@@ -142,245 +144,165 @@ 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) + "/")
|
||||
let url = self.url(of: atPath).appendingPathComponent(folderName)
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
do {
|
||||
try self.opFileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: [:])
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.writingIntent(with: url, options: .forReplacing)
|
||||
self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(fileName))
|
||||
let url = self.url(of: atPath).appendingPathComponent(fileName, isDirectory: false)
|
||||
let fileName = fileName.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
let path = (atPath as NSString).appendingPathComponent(fileName)
|
||||
let opType = FileOperationType.create(path: path)
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
let success = self.opFileManager.createFile(atPath: url.path, contents: data, attributes: nil)
|
||||
if success {
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} else {
|
||||
completionHandler?(self.throwError(atPath, code: URLError.cannotCreateFile as FoundationErrorEnum))
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.writingIntent(with:url, options: .forReplacing)
|
||||
self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
return self.doOperation(opType, data: data, atomically: true, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.move(source: path, destination: toPath)
|
||||
let sourceUrl = self.url(of: path)
|
||||
let destUrl = self.url(of: toPath)
|
||||
|
||||
let sourceIntent = NSFileAccessIntent.writingIntent(with: sourceUrl, options: .forDeleting)
|
||||
let destIntent = NSFileAccessIntent.writingIntent(with: destUrl, options: .forReplacing)
|
||||
|
||||
let operationHandler: (URL, URL) -> Void = { sourceUrl, destUrl in
|
||||
if !overwrite && self.fileManager.fileExists(atPath: destUrl.path) {
|
||||
completionHandler?(self.throwError(toPath, code: URLError.cannotMoveFile as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
do {
|
||||
try self.opFileManager.moveItem(at: sourceUrl, to: destUrl)
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
|
||||
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
|
||||
completionHandler?(self.throwError(toPath, code: URLError.cannotMoveFile as FoundationErrorEnum))
|
||||
return nil
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
coordinated(intents: [sourceIntent, destIntent], completionHandler: operationHandler) { (error) in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(sourceUrl, destUrl)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toPath)
|
||||
let sourceUrl = self.url(of: path)
|
||||
let destUrl = self.url(of: toPath)
|
||||
|
||||
let sourceIntent = NSFileAccessIntent.readingIntent(with: sourceUrl, options: .withoutChanges)
|
||||
let destIntent = NSFileAccessIntent.writingIntent(with: destUrl, options: .forDeleting)
|
||||
|
||||
let operationHandler: (URL, URL) -> Void = { sourceUrl, destUrl in
|
||||
if !overwrite && self.fileManager.fileExists(atPath: destUrl.path) {
|
||||
completionHandler?(self.throwError(toPath, code: URLError.cannotMoveFile as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
do {
|
||||
try self.opFileManager.copyItem(at: sourceUrl, to: destUrl)
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
|
||||
completionHandler?(self.throwError(toPath, code: URLError.cannotWriteToFile as FoundationErrorEnum))
|
||||
return nil
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
coordinated(intents: [sourceIntent, destIntent], moving: true, completionHandler: operationHandler) { (error) in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(sourceUrl, destUrl)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.remove(path: path)
|
||||
let url = self.url(of: path)
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
do {
|
||||
let successfulSecurityScopedResourceAccess = url.startAccessingSecurityScopedResource()
|
||||
try self.opFileManager.removeItem(at: url)
|
||||
if successfulSecurityScopedResourceAccess {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.writingIntent(with:url, options: .forReplacing)
|
||||
self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
// TODO: Make use of overwrite parameter
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
operation_queue.addOperation {
|
||||
do {
|
||||
try self.opFileManager.copyItem(at: localFile, to: self.url(of: toPath))
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
}
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
|
||||
operation_queue.addOperation {
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
dynamic func doSimpleOperation(_ box: UndoBox) {
|
||||
_ = self.doOperation(box.operation) { (_) in
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let sourcePath = opType.source else { return nil }
|
||||
let destPath = opType.destination
|
||||
let source: URL, dest: URL?
|
||||
|
||||
if sourcePath.hasPrefix("file://") {
|
||||
source = URL(string: sourcePath)!
|
||||
} else {
|
||||
source = self.url(of: sourcePath)
|
||||
}
|
||||
|
||||
if let destPath = destPath {
|
||||
if destPath.hasPrefix("file://") {
|
||||
dest = URL(string: destPath)!
|
||||
} else {
|
||||
dest = self.url(of: destPath)
|
||||
}
|
||||
} else {
|
||||
dest = nil
|
||||
}
|
||||
|
||||
let operationHandler: (URL, URL?) -> Void = { source, dest in
|
||||
let successfulSecurityScopedResourceAccess = source.startAccessingSecurityScopedResource()
|
||||
do {
|
||||
try self.opFileManager.copyItem(at: self.url(of: path), to: toLocalURL)
|
||||
switch opType {
|
||||
case .create:
|
||||
if sourcePath.hasSuffix("/") {
|
||||
try self.opFileManager.createDirectory(at: source, withIntermediateDirectories: true, attributes: [:])
|
||||
} else {
|
||||
try data?.write(to: source, options: Data.WritingOptions.atomic)
|
||||
}
|
||||
case .modify:
|
||||
try data?.write(to: source, options: atomically ? [.atomic] : [])
|
||||
case .copy:
|
||||
guard let dest = dest else { return }
|
||||
try self.opFileManager.copyItem(at: source, to: dest)
|
||||
case .move:
|
||||
guard let dest = dest else { return }
|
||||
try self.opFileManager.moveItem(at: source, to: dest)
|
||||
case.remove:
|
||||
try self.opFileManager.removeItem(at: source)
|
||||
default:
|
||||
return
|
||||
}
|
||||
if successfulSecurityScopedResourceAccess {
|
||||
source.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
|
||||
if let undoOp = self.undoOperation(for: opType) {
|
||||
let undoBox = UndoBox(provider: self, operation: undoOp)
|
||||
self.undoManager?.registerUndo(withTarget: self, selector: #selector(LocalFileProvider.doSimpleOperation(_:)), object: undoBox)
|
||||
}
|
||||
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} catch let e {
|
||||
if successfulSecurityScopedResourceAccess {
|
||||
source.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
var intents = [NSFileAccessIntent]()
|
||||
|
||||
switch opType {
|
||||
case .create, .remove, .modify:
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forReplacing))
|
||||
case .copy:
|
||||
guard let dest = dest else { return nil }
|
||||
intents.append(NSFileAccessIntent.readingIntent(with: source, options: .withoutChanges))
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: dest, options: .forReplacing))
|
||||
case .move:
|
||||
guard let dest = dest else { return nil }
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forDeleting))
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: dest, options: .forReplacing))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
self.coordinated(intents: intents, completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(source, dest)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@@ -417,7 +339,10 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
|
||||
@discardableResult
|
||||
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
if length == 0 {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -430,11 +355,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
|
||||
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))
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileNoSuchFile as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
guard let handle = FileHandle(forReadingAtPath: url.path) else {
|
||||
completionHandler(nil, self.throwError(path, code: URLError.cannotOpenFile as FoundationErrorEnum))
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadNoPermission as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -477,45 +402,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
@discardableResult
|
||||
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.modify(path: path)
|
||||
let url = self.url(of: path)
|
||||
var options: Data.WritingOptions = []
|
||||
if atomically {
|
||||
options.insert(.atomic)
|
||||
}
|
||||
if overwrite {
|
||||
options.insert(.withoutOverwriting)
|
||||
}
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
do {
|
||||
try data.write(to: url, options: atomically ? [.atomic] : [])
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async{
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.writingIntent(with: url, options: .forReplacing)
|
||||
coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
return self.doOperation(opType, data: data, atomically: atomically, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
@@ -638,19 +525,21 @@ internal extension LocalFileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func coordinated(intents: [NSFileAccessIntent], moving: Bool = false, completionHandler: @escaping (_ sourceUrl: URL, _ destURL: URL) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
|
||||
func coordinated(intents: [NSFileAccessIntent], moving: Bool = false, completionHandler: @escaping (_ sourceUrl: URL, _ destURL: URL?) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
|
||||
let coordinator = NSFileCoordinator(filePresenter: nil)
|
||||
coordinator.coordinate(with: intents, queue: operation_queue) { (error) in
|
||||
if let error = error {
|
||||
errorHandler?(error)
|
||||
return
|
||||
}
|
||||
if moving {
|
||||
coordinator.item(at: intents[0].url, willMoveTo: intents[1].url)
|
||||
let newSource: URL = intents[0].url
|
||||
let newDest: URL? = intents.count > 1 ? intents[1].url : nil
|
||||
if moving, let newDest = newDest {
|
||||
coordinator.item(at: newSource, willMoveTo: newDest)
|
||||
}
|
||||
completionHandler(intents[0].url, intents[1].url)
|
||||
if moving {
|
||||
coordinator.item(at: intents[0].url, didMoveTo: intents[1].url)
|
||||
completionHandler(newSource, newDest)
|
||||
if moving, let newDest = newDest {
|
||||
coordinator.item(at: newSource, didMoveTo: newDest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,12 +37,12 @@ public final class LocalFileObject: FileObject {
|
||||
|
||||
public convenience init?(fileWithURL fileURL: URL) {
|
||||
do {
|
||||
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey, .generationIdentifierKey])
|
||||
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey, .generationIdentifierKey, .documentIdentifierKey])
|
||||
let path = fileURL.relativePath.hasPrefix("/") ? fileURL.relativePath : "/" + fileURL.relativePath
|
||||
|
||||
self.init(url: fileURL, name: values.name ?? fileURL.lastPathComponent, path: path)
|
||||
for (key, value) in values.allValues {
|
||||
self.allValues[key.rawValue] = value
|
||||
self.allValues[key] = value
|
||||
}
|
||||
} catch {
|
||||
return nil
|
||||
@@ -51,23 +51,31 @@ public final class LocalFileObject: FileObject {
|
||||
|
||||
open internal(set) var allocatedSize: Int64 {
|
||||
get {
|
||||
return allValues[URLResourceKey.fileAllocatedSizeKey.rawValue] as? Int64 ?? 0
|
||||
return allValues[.fileAllocatedSizeKey] as? Int64 ?? 0
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.fileAllocatedSizeKey.rawValue] = Int(exactly: newValue) ?? Int.max
|
||||
allValues[.fileAllocatedSizeKey] = Int(exactly: newValue) ?? Int.max
|
||||
}
|
||||
}
|
||||
|
||||
open internal(set) var id: Int? {
|
||||
get {
|
||||
return allValues[.documentIdentifierKey] as? Int
|
||||
}
|
||||
set {
|
||||
allValues[.documentIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
open var rev: String? {
|
||||
get {
|
||||
let data = allValues[URLResourceKey.generationIdentifierKey.rawValue] as? Data
|
||||
let data = allValues[.generationIdentifierKey] as? Data
|
||||
return data?.map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class LocalFolderMonitor {
|
||||
internal final class LocalFolderMonitor {
|
||||
fileprivate let source: DispatchSourceFileSystemObject
|
||||
fileprivate let descriptor: CInt
|
||||
fileprivate let qq: DispatchQueue = DispatchQueue.global(qos: .default)
|
||||
@@ -297,6 +305,16 @@ open class LocalOperationHandle: OperationHandle {
|
||||
}
|
||||
}
|
||||
|
||||
class UndoBox: NSObject {
|
||||
weak var provider: FileProvideUndoable?
|
||||
let operation: FileOperationType
|
||||
|
||||
init(provider: FileProvideUndoable, operation: FileOperationType) {
|
||||
self.provider = provider
|
||||
self.operation = operation
|
||||
}
|
||||
}
|
||||
|
||||
internal extension URL {
|
||||
var fileIsDirectory: Bool {
|
||||
return (try? self.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
|
||||
|
||||
@@ -240,7 +240,10 @@ 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 {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -48,28 +48,28 @@ public final class OneDriveFileObject: FileObject {
|
||||
|
||||
open internal(set) var id: String? {
|
||||
get {
|
||||
return allValues["NSURLDocumentIdentifyKey"] as? String
|
||||
return allValues[.documentIdentifierKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues["NSURLDocumentIdentifyKey"] = newValue
|
||||
allValues[.documentIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
open internal(set) var contentType: String {
|
||||
get {
|
||||
return allValues["NSURLContentTypeKey"] as? String ?? ""
|
||||
return allValues[.mimeType] as? String ?? ""
|
||||
}
|
||||
set {
|
||||
allValues["NSURLContentTypeKey"] = newValue
|
||||
allValues[.mimeType] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
open internal(set) var entryTag: String? {
|
||||
get {
|
||||
return allValues["NSURLEntryTagKey"] as? String
|
||||
return allValues[.entryTag] as? String
|
||||
}
|
||||
set {
|
||||
allValues["NSURLEntryTagKey"] = newValue
|
||||
allValues[.entryTag] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,72 +235,51 @@ internal extension OneDriveFileProvider {
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if let parent = json["image"] as? [String: Any] ?? json["video"] as? [String: Any], let height = parent["height"] as? UInt64, let width = parent["width"] as? UInt64 {
|
||||
keys.append("Dimensions")
|
||||
dic["Dimensions"] = "\(width)x\(height)"
|
||||
add(key: "Dimensions", value: "\(width)x\(height)")
|
||||
}
|
||||
if let location = json["location"] as? [String: Any], let latitude = location["latitude"] as? Double, let longitude = location["longitude"] as? Double {
|
||||
|
||||
OneDriveFileProvider.decimalFormatter.numberStyle = .decimal
|
||||
OneDriveFileProvider.decimalFormatter.maximumFractionDigits = 5
|
||||
keys.append("Location")
|
||||
let latStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: latitude))
|
||||
let longStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))
|
||||
dic["Location"] = "\(latStr), \(longStr)"
|
||||
add(key: "Location", value: "\(latStr), \(longStr)")
|
||||
}
|
||||
if let parent = json["image"] as? [String: Any] ?? json["video"] as? [String: Any], let duration = parent["duration"] as? UInt64 {
|
||||
keys.append("Duration")
|
||||
dic["Duration"] = OneDriveFileProvider.formatshort(interval: TimeInterval(duration) / 1000)
|
||||
add(key: "Duration", value: OneDriveFileProvider.formatshort(interval: TimeInterval(duration) / 1000))
|
||||
}
|
||||
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = resolve(dateString: timeTakenStr) {
|
||||
keys.append("Date taken")
|
||||
OneDriveFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
dic["Date taken"] = OneDriveFileProvider.dateFormatter.string(from: timeTaken)
|
||||
add(key: "Date taken", value: OneDriveFileProvider.dateFormatter.string(from: timeTaken))
|
||||
}
|
||||
|
||||
if let photo = json["photo"] as? [String: Any] {
|
||||
if let devicemake = photo["cameraMake"] as? String {
|
||||
keys.append("Device make")
|
||||
dic["Device make"] = devicemake
|
||||
}
|
||||
if let devicemodel = photo["cameraModel"] as? String {
|
||||
keys.append("Device model")
|
||||
dic["Device model"] = devicemodel
|
||||
}
|
||||
if let focallen = photo["focalLength"] as? Double {
|
||||
keys.append("Focal length")
|
||||
dic["Focal length"] = focallen
|
||||
}
|
||||
if let fnum = photo["fNumber"] as? Double {
|
||||
keys.append("F number")
|
||||
dic["F number"] = fnum
|
||||
}
|
||||
add(key: "Device make", value: photo["cameraMake"] as? String)
|
||||
add(key: "Device model", value: photo["cameraModel"] as? String)
|
||||
add(key: "focalLength", value: photo["focalLength"] as? Double)
|
||||
add(key: "fNumber", value: photo["fNumber"] as? Double)
|
||||
if let expNom = photo["exposureNumerator"] as? Double, let expDen = photo["exposureDenominator"] as? Double {
|
||||
keys.append("Exposure time")
|
||||
dic["Exposure time"] = "\(Int(expNom))/\(Int(expDen))"
|
||||
add(key: "Exposure time", value: "\(Int(expNom))/\(Int(expDen))")
|
||||
}
|
||||
if let iso = photo["iso"] as? Int64 {
|
||||
keys.append("ISO speed")
|
||||
dic["ISO speed"] = iso
|
||||
}
|
||||
|
||||
add(key: "ISO speed", value: photo["iso"] as? Int64)
|
||||
}
|
||||
|
||||
if let audio = json["audio"] as? [String: Any] {
|
||||
for (key, value) in audio {
|
||||
if key == "bitrate" || key == "isVariableBitrate" { continue }
|
||||
let casedKey = spaceCamelCase(key)
|
||||
keys.append(casedKey)
|
||||
dic[casedKey] = value
|
||||
add(key: casedKey, value: value)
|
||||
}
|
||||
}
|
||||
|
||||
if let video = json["video"] as? [String: Any] {
|
||||
if let bitRate = video["bitrate"] as? Int {
|
||||
keys.append("Bitrate")
|
||||
dic["Bitrate"] = bitRate
|
||||
}
|
||||
}
|
||||
add(key: "Bitrate", value: (json["video"] as? NSDictionary)?["bitrate"] as? Int)
|
||||
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
@@ -331,7 +331,10 @@ 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 {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -579,20 +582,20 @@ public final class WebDavFileObject: FileObject {
|
||||
/// MIME type of the file
|
||||
open internal(set) var contentType: String {
|
||||
get {
|
||||
return allValues["NSURLContentTypeKey"] as? String ?? ""
|
||||
return allValues[.mimeType] as? String ?? ""
|
||||
}
|
||||
set {
|
||||
allValues["NSURLContentTypeKey"] = newValue
|
||||
allValues[.mimeType] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP E-Tag, can be used to mark changed files
|
||||
open internal(set) var entryTag: String? {
|
||||
get {
|
||||
return allValues["NSURLEntryTagKey"] as? String
|
||||
return allValues[.entryTag] as? String
|
||||
}
|
||||
set {
|
||||
allValues["NSURLEntryTagKey"] = newValue
|
||||
allValues[.entryTag] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user