Fixed iCloud download/upload progress report, Fix #93
- Fixed FileObject comparison - Fixed image GPS metadata population in ExtendedLocalFileProvider - More items for image metadata in ExtendedLocalFileProvider
This commit is contained in:
@@ -583,16 +583,76 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
return file
|
||||
}
|
||||
|
||||
lazy fileprivate var observer: KVOObserver = KVOObserver()
|
||||
|
||||
fileprivate func monitorFile(path: String, operation: FileOperationType, progress: Progress?) {
|
||||
let pathURL = self.url(of: path).standardizedFileURL
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "%K LIKE[CD] %@", NSMetadataItemPathKey, pathURL.path)
|
||||
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataUbiquitousItemPercentDownloadedKey, NSMetadataUbiquitousItemPercentUploadedKey, NSMetadataUbiquitousItemDownloadingStatusKey, NSMetadataItemFSSizeKey]
|
||||
query.predicate = NSPredicate(format: "(%K LIKE[CD] %@)", NSMetadataItemPathKey, pathURL.path)
|
||||
query.valueListAttributes = [NSMetadataUbiquitousItemPercentDownloadedKey,
|
||||
NSMetadataUbiquitousItemPercentUploadedKey,
|
||||
NSMetadataUbiquitousItemDownloadingStatusKey,
|
||||
NSMetadataItemFSSizeKey]
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
var context = QueryProgressWrapper(provider: self, progress: progress, operation: operation)
|
||||
query.addObserver(self.observer, forKeyPath: "results", options: [.initial, .new, .old], context: &context)
|
||||
|
||||
var isDownloadingOperation = false
|
||||
var isUploadingOperation = false
|
||||
|
||||
switch operation {
|
||||
case .fetch:
|
||||
isDownloadingOperation = true
|
||||
case .modify, .create:
|
||||
isUploadingOperation = true
|
||||
case .copy(_, destination: let dest) where dest.hasPrefix("file://"), .move(_, destination: let dest) where dest.hasPrefix("file://"):
|
||||
isDownloadingOperation = true
|
||||
case .copy(source: let source, _) where source.hasPrefix("file://"), .move(source: let source, _) where source.hasPrefix("file://"):
|
||||
isDownloadingOperation = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
var observer: NSObjectProtocol?
|
||||
observer = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: .main) { [weak self] (notification) in
|
||||
guard let items = notification.userInfo?[NSMetadataQueryUpdateChangedItemsKey] as? NSArray,
|
||||
let item = items.firstObject as? NSMetadataItem else {
|
||||
return
|
||||
}
|
||||
|
||||
func terminateAndRemoveObserver() {
|
||||
query.stop()
|
||||
observer.flatMap(NotificationCenter.default.removeObserver)
|
||||
observer = nil
|
||||
}
|
||||
|
||||
func updateProgress(_ percent: NSNumber) {
|
||||
let fraction = percent.doubleValue / 100
|
||||
self?.delegateNotify(operation, progress: fraction)
|
||||
if let progress = progress {
|
||||
if progress.totalUnitCount < 1, let size = item.value(forAttribute: NSMetadataItemFSSizeKey) as? NSNumber {
|
||||
progress.totalUnitCount = size.int64Value
|
||||
}
|
||||
progress.completedUnitCount = progress.totalUnitCount > 0 ? Int64(Double(progress.totalUnitCount) * fraction) : 0
|
||||
}
|
||||
if percent.doubleValue == 100.0 {
|
||||
terminateAndRemoveObserver()
|
||||
}
|
||||
}
|
||||
|
||||
for attrName in item.attributes {
|
||||
switch attrName {
|
||||
case NSMetadataUbiquitousItemDownloadingStatusKey:
|
||||
if let value = item.value(forAttribute: attrName) as? String, value == NSMetadataUbiquitousItemDownloadingStatusDownloaded {
|
||||
terminateAndRemoveObserver()
|
||||
}
|
||||
case NSMetadataUbiquitousItemPercentDownloadedKey:
|
||||
guard isDownloadingOperation, let percent = item.value(forAttribute: attrName) as? NSNumber else { break }
|
||||
updateProgress(percent)
|
||||
case NSMetadataUbiquitousItemPercentUploadedKey:
|
||||
guard isUploadingOperation, let percent = item.value(forAttribute: attrName) as? NSNumber else { break }
|
||||
updateProgress(percent)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
query.start()
|
||||
@@ -691,57 +751,6 @@ public enum UbiquitousScope: RawRepresentable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct QueryProgressWrapper {
|
||||
weak var provider: CloudFileProvider?
|
||||
weak var progress: Progress?
|
||||
let operation: FileOperationType
|
||||
}
|
||||
|
||||
fileprivate class KVOObserver: NSObject {
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
guard let query = object as? NSMetadataQuery else {
|
||||
return
|
||||
}
|
||||
guard let wrapper = context?.load(as: QueryProgressWrapper.self) else {
|
||||
query.stop()
|
||||
query.removeObserver(self, forKeyPath: "results")
|
||||
return
|
||||
}
|
||||
let provider = wrapper.provider
|
||||
let progress = wrapper.progress
|
||||
let operation = wrapper.operation
|
||||
|
||||
guard let results = change?[.newKey], let item = (results as? [NSMetadataItem])?.first else {
|
||||
return
|
||||
}
|
||||
|
||||
query.disableUpdates()
|
||||
var size = progress?.totalUnitCount ?? -1
|
||||
if size < 0, let size_d = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 {
|
||||
size = size_d
|
||||
progress?.totalUnitCount = size
|
||||
}
|
||||
let downloadStatus = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? String ?? ""
|
||||
let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0
|
||||
let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0
|
||||
if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) {
|
||||
progress?.completedUnitCount = Int64(uploaded / 100 * Double(size))
|
||||
provider?.delegateNotify(operation, progress: uploaded / 100)
|
||||
} else if (uploaded == 0 || uploaded == 100) && downloadStatus != NSMetadataUbiquitousItemDownloadingStatusCurrent {
|
||||
progress?.completedUnitCount = Int64(downloaded / 100 * Double(size))
|
||||
provider?.delegateNotify(operation, progress: downloaded / 100)
|
||||
} else if uploaded == 100 || downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent {
|
||||
progress?.completedUnitCount = size
|
||||
query.stop()
|
||||
query.removeObserver(self, forKeyPath: "results")
|
||||
provider?.delegateNotify(operation)
|
||||
}
|
||||
|
||||
query.enableUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func getMetadataItem(url: URL) -> NSMetadataItem? {
|
||||
let query = NSMetadataQuery()
|
||||
|
||||
@@ -282,26 +282,31 @@ public struct LocalFileInformationGenerator {
|
||||
let imageDict = cfImageDict as NSDictionary
|
||||
let tiffDict = imageDict[kCGImagePropertyTIFFDictionary as String] as? NSDictionary ?? [:]
|
||||
let exifDict = imageDict[kCGImagePropertyExifDictionary as String] as? NSDictionary ?? [:]
|
||||
let gpsDict = imageDict[kCGImagePropertyGPSDictionary 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 maker", 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)
|
||||
add(key: "Copyright", value: tiffDict[kCGImagePropertyTIFFCopyright as String] as? String)
|
||||
add(key: "Date taken", value: tiffDict[kCGImagePropertyTIFFDateTime as String] as? String)
|
||||
|
||||
if let latitude = tiffDict[kCGImagePropertyGPSLatitude as String] as? NSNumber, let longitude = tiffDict[kCGImagePropertyGPSLongitude as String] as? NSNumber {
|
||||
add(key: "Location", value: "\(latitude), \(longitude)")
|
||||
if let latitude = gpsDict[kCGImagePropertyGPSLatitude as String] as? NSNumber,
|
||||
let longitude = gpsDict[kCGImagePropertyGPSLongitude as String] as? NSNumber {
|
||||
let altitudeDesc = (gpsDict[kCGImagePropertyGPSAltitude as String] as? NSNumber).map({ " at \($0.format(precision: 0))m" }) ?? ""
|
||||
add(key: "Location", value: "\(latitude.format()), \(longitude.format())\(altitudeDesc)")
|
||||
}
|
||||
add(key: "Altitude", value: tiffDict[kCGImagePropertyGPSAltitude as String] as? NSNumber)
|
||||
add(key: "Area", value: tiffDict[kCGImagePropertyGPSAreaInformation as String])
|
||||
add(key: "Area", value: gpsDict[kCGImagePropertyGPSAreaInformation as String])
|
||||
|
||||
add(key: "Color space", value: imageDict[kCGImagePropertyColorModel as String])
|
||||
add(key: "Color depth", value: (imageDict[kCGImagePropertyDepth as String] as? NSNumber).map({ "\($0) bits" }))
|
||||
add(key: "Color profile", value: imageDict[kCGImagePropertyProfileName as String])
|
||||
add(key: "Focal length", value: exifDict[kCGImagePropertyExifFocalLength as String])
|
||||
add(key: "White banance", value: exifDict[kCGImagePropertyExifWhiteBalance as String])
|
||||
add(key: "F number", value: exifDict[kCGImagePropertyExifFNumber as String])
|
||||
add(key: "Exposure program", value: exifDict[kCGImagePropertyExifExposureProgram as String])
|
||||
|
||||
@@ -325,7 +330,7 @@ public struct LocalFileInformationGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
func makeDescription(_ key: String?) -> String? {
|
||||
func makeKeyDescription(_ key: String?) -> String? {
|
||||
guard let key = key else {
|
||||
return nil
|
||||
}
|
||||
@@ -336,6 +341,18 @@ public struct LocalFileInformationGenerator {
|
||||
return newKey.capitalized
|
||||
}
|
||||
|
||||
func parseLocationData(_ value: String) -> (latitude: Double, longitude: Double, height: Double?)? {
|
||||
let scanner = Scanner.init(string: value)
|
||||
var latitude: Double = 0.0, longitude: Double = 0.0, height: Double = 0
|
||||
|
||||
if scanner.scanDouble(&latitude), scanner.scanDouble(&longitude) {
|
||||
scanner.scanDouble(&height)
|
||||
return (latitude, longitude, height)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
guard fileURL.fileExists else {
|
||||
return (dic, keys)
|
||||
}
|
||||
@@ -347,10 +364,20 @@ public struct LocalFileInformationGenerator {
|
||||
#else
|
||||
let commonKey = item.commonKey
|
||||
#endif
|
||||
if let description = makeDescription(commonKey) {
|
||||
if let value = item.stringValue {
|
||||
keys.append(description)
|
||||
dic[description] = value
|
||||
if let key = makeKeyDescription(commonKey) {
|
||||
if commonKey == "location", let value = item.stringValue, let loc = parseLocationData(value) {
|
||||
keys.append(key)
|
||||
let heightStr: String = (loc.height as NSNumber?).map({ ", \($0.format(precision: 0))m" }) ?? ""
|
||||
dic[key] = "\((loc.latitude as NSNumber).format())°, \((loc.longitude as NSNumber).format())°\(heightStr)"
|
||||
} else if let value = item.dateValue {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
} else if let value = item.numberValue {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
} else if let value = item.stringValue {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,6 +441,15 @@ internal extension String {
|
||||
}
|
||||
}
|
||||
|
||||
internal extension NSNumber {
|
||||
internal func format(precision: Int = 2, style: NumberFormatter.Style = .decimal) -> String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.maximumFractionDigits = precision
|
||||
formatter.numberStyle = style
|
||||
return formatter.string(from: self)!
|
||||
}
|
||||
}
|
||||
|
||||
internal extension TimeInterval {
|
||||
internal var formatshort: String {
|
||||
var result = "0:00"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
/// Containts path, url and attributes of a file or resource.
|
||||
open class FileObject: Equatable {
|
||||
open class FileObject: NSObject {
|
||||
/// A `Dictionary` contains file information, using `URLResourceKey` keys.
|
||||
open internal(set) var allValues: [URLResourceKey: Any]
|
||||
|
||||
@@ -19,6 +19,7 @@ open class FileObject: Equatable {
|
||||
|
||||
internal init(url: URL?, name: String, path: String) {
|
||||
self.allValues = [URLResourceKey: Any]()
|
||||
super.init()
|
||||
if let url = url {
|
||||
self.url = url
|
||||
}
|
||||
@@ -147,6 +148,18 @@ open class FileObject: Equatable {
|
||||
open var isSymLink: Bool {
|
||||
return self.type == .symbolicLink
|
||||
}
|
||||
}
|
||||
|
||||
extension FileObject {
|
||||
open override var hashValue: Int {
|
||||
let hashURL = self.url.hashValue
|
||||
let hashSize = self.size.hashValue
|
||||
return (hashURL << 7) &+ hashURL &+ hashSize
|
||||
}
|
||||
|
||||
open override var hash: Int {
|
||||
return self.hashValue
|
||||
}
|
||||
|
||||
/// Check `FileObject` equality
|
||||
public static func ==(lhs: FileObject, rhs: FileObject) -> Bool {
|
||||
@@ -159,7 +172,7 @@ open class FileObject: Equatable {
|
||||
}
|
||||
#else
|
||||
if type(of: lhs) != type(of: rhs) {
|
||||
return false
|
||||
return false
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -169,6 +182,13 @@ open class FileObject: Equatable {
|
||||
return rhs.path == lhs.path && rhs.size == lhs.size && rhs.modifiedDate == lhs.modifiedDate
|
||||
}
|
||||
|
||||
open override func isEqual(_ object: Any?) -> Bool {
|
||||
guard let object = object as? FileObject else { return false }
|
||||
return self == object
|
||||
}
|
||||
}
|
||||
|
||||
extension FileObject {
|
||||
internal func mapPredicate() -> [String: Any] {
|
||||
let mapDict: [URLResourceKey: String] = [.fileURLKey: "url", .nameKey: "name", .pathKey: "path",
|
||||
.fileSizeKey: "fileSize", .creationDateKey: "creationDate",
|
||||
@@ -225,7 +245,7 @@ open class FileObject: Equatable {
|
||||
}
|
||||
|
||||
/// Containts attributes of a provider.
|
||||
open class VolumeObject {
|
||||
open class VolumeObject: NSObject {
|
||||
/// A `Dictionary` contains volume information, using `URLResourceKey` keys.
|
||||
open internal(set) var allValues: [URLResourceKey: Any]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user