Compare commits

..

10 Commits

Author SHA1 Message Date
Amir Abbas 72520973e9 Changed FileObject.allValues key type to URLResourceKey
- LocalFileProvider.storageProperties method now uses url resource values
2017-02-08 07:30:41 +03:30
Amir Abbas 63016285af Added UndoManager support to Local provider
- Refactored Local provider operations into one function
2017-02-08 04:54:18 +03:30
Amir Abbas 3245c9df03 Updated travis.yml 2017-02-07 21:59:17 +03:30
Amir Abbas ee1fa89747 Updated travis.yml 2017-02-07 12:07:59 +03:30
Amir Abbas 4a6b25deac Updated travis.yml 2017-02-07 09:48:12 +03:30
Amir Abbas 2423cbd0f6 Updated travis.yml 2017-02-07 02:55:22 +03:30
Amir Abbas 480099ca8a Updated travis.yml 2017-02-06 19:41:57 +03:30
Amir Abbas 755bf6bf84 Updated travis.yaml 2017-02-06 19:38:14 +03:30
Amir Abbas 525825ff5d Fixed Cloud provider listing (RunLoop) bug
- Added ubiquity scope to Cloud provider
- Renamed `temporaryLink` to `publicLink`
- Added deprecated renaming guide
2017-02-06 18:03:58 +03:30
Amir Abbas Mousavian a35384dc31 Refactoring: reducing complexity of properties methods 2017-02-03 23:15:23 +03:30
15 changed files with 483 additions and 484 deletions
+21 -8
View File
@@ -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
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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;
+4 -4
View File
@@ -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
+86 -24
View File
@@ -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 apps 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 apps 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 apps 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
}
+7 -6
View File
@@ -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)
+6 -6
View File
@@ -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
}
}
}
+82 -103
View File
@@ -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
View File
@@ -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 ?? ""
+48 -3
View File
@@ -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
View File
@@ -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)
}
}
}
+25 -7
View File
@@ -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
+4 -1
View File
@@ -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
}
+25 -46
View File
@@ -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)
}
+8 -5
View File
@@ -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
}
}
}