From e5e5faa4e839a37d4963d1a440bb551eb490835f Mon Sep 17 00:00:00 2001 From: Amir Abbas Date: Thu, 3 May 2018 19:39:18 +0430 Subject: [PATCH] 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 --- Sources/CloudFileProvider.swift | 123 ++++++++++-------- Sources/ExtendedLocalFileProvider.swift | 49 +++++-- Sources/Extensions/FoundationExtensions.swift | 9 ++ Sources/FileObject.swift | 26 +++- 4 files changed, 136 insertions(+), 71 deletions(-) diff --git a/Sources/CloudFileProvider.swift b/Sources/CloudFileProvider.swift index 10f7f4c..20db163 100644 --- a/Sources/CloudFileProvider.swift +++ b/Sources/CloudFileProvider.swift @@ -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() diff --git a/Sources/ExtendedLocalFileProvider.swift b/Sources/ExtendedLocalFileProvider.swift index ec36f64..fd2d643 100644 --- a/Sources/ExtendedLocalFileProvider.swift +++ b/Sources/ExtendedLocalFileProvider.swift @@ -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 } } } diff --git a/Sources/Extensions/FoundationExtensions.swift b/Sources/Extensions/FoundationExtensions.swift index ea55d90..5c54c01 100755 --- a/Sources/Extensions/FoundationExtensions.swift +++ b/Sources/Extensions/FoundationExtensions.swift @@ -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" diff --git a/Sources/FileObject.swift b/Sources/FileObject.swift index c0e644c..ef2f847 100644 --- a/Sources/FileObject.swift +++ b/Sources/FileObject.swift @@ -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]