Skip to content

Commit

Permalink
Fixed iCloud download/upload progress report, Fix #93
Browse files Browse the repository at this point in the history
- Fixed FileObject comparison
- Fixed image GPS metadata population in ExtendedLocalFileProvider
- More items for image metadata in ExtendedLocalFileProvider
  • Loading branch information
amosavian committed May 3, 2018
1 parent f2cd571 commit e5e5faa
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 71 deletions.
123 changes: 66 additions & 57 deletions Sources/CloudFileProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
49 changes: 38 additions & 11 deletions Sources/ExtendedLocalFileProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand All @@ -325,7 +330,7 @@ public struct LocalFileInformationGenerator {
}
}

func makeDescription(_ key: String?) -> String? {
func makeKeyDescription(_ key: String?) -> String? {
guard let key = key else {
return nil
}
Expand All @@ -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)
}
Expand All @@ -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
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions Sources/Extensions/FoundationExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
26 changes: 23 additions & 3 deletions Sources/FileObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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
}
Expand Down Expand Up @@ -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 {
Expand All @@ -159,7 +172,7 @@ open class FileObject: Equatable {
}
#else
if type(of: lhs) != type(of: rhs) {
return false
return false
}
#endif

Expand All @@ -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",
Expand Down Expand Up @@ -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]

Expand Down

0 comments on commit e5e5faa

Please sign in to comment.