New background task modes
This commit is contained in:
@@ -19,22 +19,91 @@ protocol DatabaseDelegate: class {
|
||||
|
||||
func database(didLoadImageForCap cap: Int)
|
||||
|
||||
func database(completedBackgroundWorkItem title: String, subtitle: String)
|
||||
|
||||
func database(needsUserConfirmation title: String, body: String, shouldProceed: @escaping (Bool) -> Void)
|
||||
|
||||
func database(didFailBackgroundWork title: String, subtitle: String)
|
||||
|
||||
func databaseHasNewClassifier()
|
||||
|
||||
func databaseDidFinishBackgroundWork()
|
||||
|
||||
func databaseNeedsFullRefresh()
|
||||
}
|
||||
|
||||
private enum BackgroundWorkTaskType: Int, CustomStringConvertible, Comparable {
|
||||
|
||||
case downloadCapNames = 9
|
||||
case downloadCounts = 8
|
||||
case downloadClassifier = 7
|
||||
case uploadingCaps = 6
|
||||
case uploadingImages = 5
|
||||
case downloadMainImages = 4
|
||||
case creatingThumbnails = 3
|
||||
case creatingColors = 2
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .downloadCapNames:
|
||||
return "Downloading names"
|
||||
case .downloadCounts:
|
||||
return "Downloading counts"
|
||||
case .downloadClassifier:
|
||||
return "Downloading classifier"
|
||||
case .uploadingCaps:
|
||||
return "Uploading caps"
|
||||
case .uploadingImages:
|
||||
return "Uploading images"
|
||||
case .downloadMainImages:
|
||||
return "Downloading images"
|
||||
case .creatingThumbnails:
|
||||
return "Creating thumbnails"
|
||||
case .creatingColors:
|
||||
return "Creating colors"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var maximumNumberOfSimultaneousItems: Int {
|
||||
switch self {
|
||||
case .downloadMainImages:
|
||||
return 50
|
||||
case .creatingThumbnails:
|
||||
return 10
|
||||
case .creatingColors:
|
||||
return 10
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
var nextType: BackgroundWorkTaskType? {
|
||||
BackgroundWorkTaskType(rawValue: rawValue - 1)
|
||||
}
|
||||
|
||||
static func < (lhs: BackgroundWorkTaskType, rhs: BackgroundWorkTaskType) -> Bool {
|
||||
lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
final class Database {
|
||||
|
||||
// MARK: Variables
|
||||
|
||||
let db: Connection
|
||||
|
||||
let upload: Upload
|
||||
private let upload: Upload
|
||||
|
||||
let download: Download
|
||||
private let download: Download
|
||||
|
||||
let storage: Storage
|
||||
|
||||
weak var delegate: DatabaseDelegate?
|
||||
|
||||
init?(url: URL, server: URL) {
|
||||
init?(url: URL, server: URL, storageFolder: URL) {
|
||||
guard let db = try? Connection(url.path) else {
|
||||
return nil
|
||||
}
|
||||
@@ -54,6 +123,7 @@ final class Database {
|
||||
self.db = db
|
||||
self.upload = upload
|
||||
self.download = download
|
||||
self.storage = Storage(in: storageFolder)
|
||||
log("Database loaded with \(capCount) caps")
|
||||
}
|
||||
|
||||
@@ -64,6 +134,11 @@ final class Database {
|
||||
(try? db.prepare(Cap.table))?.map(Cap.init) ?? []
|
||||
}
|
||||
|
||||
/// The ids of all caps
|
||||
var capIds: Set<Int> {
|
||||
Set(caps.map { $0.id })
|
||||
}
|
||||
|
||||
/// A dictionary of all caps, indexed by their ids
|
||||
var capDict: [Int : Cap] {
|
||||
caps.reduce(into: [:]) { $0[$1.id] = $1 }
|
||||
@@ -90,61 +165,68 @@ final class Database {
|
||||
(try? db.prepare(Cap.table).reduce(0) { $0 + $1[Cap.columnCount] }) ?? 0
|
||||
}
|
||||
|
||||
/// The caps without a downloaded image
|
||||
var capsWithoutImages: [Cap] {
|
||||
caps.filter({ !app.storage.hasImage(for: $0.id) })
|
||||
}
|
||||
|
||||
/// The number of caps without a downloaded image
|
||||
var capCountWithoutImages: Int {
|
||||
capsWithoutImages.count
|
||||
}
|
||||
|
||||
/// The caps without a downloaded image
|
||||
var capsWithoutThumbnails: [Cap] {
|
||||
caps.filter({ !app.storage.hasThumbnail(for: $0.id) })
|
||||
}
|
||||
|
||||
/// The number of caps without a downloaded image
|
||||
var capCountWithoutThumbnails: Int {
|
||||
capsWithoutThumbnails.count
|
||||
}
|
||||
|
||||
var pendingImageUploads: [(cap: Int, version: Int)] {
|
||||
var nextPendingCapUpload: Cap? {
|
||||
do {
|
||||
return try db.prepare(upload.table).map { row in
|
||||
(cap: row[upload.rowCapId], version: row[upload.rowCapVersion])
|
||||
guard let row = try db.pluck(Cap.table.filter(Cap.columnUploaded == false).order(Cap.columnId.asc)) else {
|
||||
return nil
|
||||
}
|
||||
return Cap(row: row)
|
||||
} catch {
|
||||
log("Failed to get pending image uploads")
|
||||
return []
|
||||
log("Failed to get next pending cap upload")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicate if there are any unfinished uploads
|
||||
var hasPendingImageUploads: Bool {
|
||||
((try? db.scalar(upload.table.count)) ?? 0) > 0
|
||||
}
|
||||
|
||||
var pendingCapUploads: [Cap] {
|
||||
do {
|
||||
return try db.prepare(Cap.table.filter(Cap.columnUploaded == false).order(Cap.columnId.asc)).map(Cap.init)
|
||||
} catch {
|
||||
log("Failed to get pending cap uploads")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
var hasPendingCapUploads: Bool {
|
||||
var pendingCapUploadCount: Int {
|
||||
do {
|
||||
let query = Cap.table.filter(Cap.columnUploaded == false).count
|
||||
return try db.scalar(query) > 0
|
||||
return try db.scalar(query)
|
||||
} catch {
|
||||
log("Failed to get pending cap upload count")
|
||||
return false
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
var nextPendingImageUpload: (id: Int, version: Int)? {
|
||||
do {
|
||||
guard let row = try db.pluck(upload.table) else {
|
||||
return nil
|
||||
}
|
||||
return (id: row[upload.rowCapId], version: row[upload.rowCapVersion])
|
||||
} catch {
|
||||
log("Failed to get pending image uploads")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var capsWithImages: Set<Int> {
|
||||
capIds.filter { storage.hasImage(for: $0) }
|
||||
}
|
||||
|
||||
var capsWithThumbnails: Set<Int> {
|
||||
capIds.filter { storage.hasThumbnail(for: $0) }
|
||||
}
|
||||
|
||||
var pendingImageUploadCount: Int {
|
||||
((try? db.scalar(upload.table.count)) ?? 0)
|
||||
}
|
||||
|
||||
/// The number of caps without a thumbnail on disk
|
||||
var pendingCapForThumbnailCreation: Int {
|
||||
caps.reduce(0) { $0 + (storage.hasThumbnail(for: $1.id) ? 0 : 1) }
|
||||
}
|
||||
|
||||
var pendingCapsForColorCreation: Int {
|
||||
do {
|
||||
return try capCount - db.scalar(Colors.table.count)
|
||||
} catch {
|
||||
log("Failed to get count of caps without color: \(error)")
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
var classifierVersion: Int {
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: Classifier.userDefaultsKey)
|
||||
@@ -184,28 +266,12 @@ final class Database {
|
||||
log("Cap not inserted")
|
||||
return false
|
||||
}
|
||||
guard app.storage.save(image: image, for: cap.id) else {
|
||||
guard storage.save(image: image, for: cap.id) else {
|
||||
log("Cap image not saved")
|
||||
return false
|
||||
}
|
||||
guard !isInOfflineMode else {
|
||||
log("Offline mode: Not uploading cap")
|
||||
return true
|
||||
}
|
||||
upload.upload(name: name, for: cap.id) { success in
|
||||
guard success else {
|
||||
return
|
||||
}
|
||||
self.update(uploaded: true, for: cap.id)
|
||||
self.upload.uploadImage(for: cap.id, version: 0) { actualVersion in
|
||||
guard let actualVersion = actualVersion else {
|
||||
self.log("Failed to upload first image for cap \(cap.id)")
|
||||
return
|
||||
}
|
||||
self.log("Uploaded first image for cap \(cap.id)")
|
||||
self.update(count: actualVersion + 1, for: cap.id)
|
||||
}
|
||||
}
|
||||
addPendingUpload(for: cap.id, version: 0)
|
||||
startBackgroundWork()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -236,7 +302,7 @@ final class Database {
|
||||
log("Failed to get count for cap \(cap)")
|
||||
return false
|
||||
}
|
||||
guard app.storage.save(image: image, for: cap, version: version) else {
|
||||
guard storage.save(image: image, for: cap, version: version) else {
|
||||
log("Failed to save image \(version) for cap \(cap) to disk")
|
||||
return false
|
||||
}
|
||||
@@ -248,22 +314,7 @@ final class Database {
|
||||
log("Failed to add cap \(cap) version \(version) to upload queue")
|
||||
return false
|
||||
}
|
||||
guard !isInOfflineMode else {
|
||||
log("Offline mode: Not uploading cap image")
|
||||
return true
|
||||
}
|
||||
upload.uploadImage(for: cap, version: version) { actualVersion in
|
||||
guard let actualVersion = actualVersion else {
|
||||
self.log("Failed to upload image \(version) for cap \(cap)")
|
||||
return
|
||||
}
|
||||
guard self.removePendingUpload(of: cap, version: version) else {
|
||||
self.log("Failed to remove version \(version) for cap \(cap) from upload queue")
|
||||
return
|
||||
}
|
||||
self.log("Uploaded version \(actualVersion) for cap \(cap)")
|
||||
self.update(count: actualVersion + 1, for: cap)
|
||||
}
|
||||
startBackgroundWork()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -310,7 +361,7 @@ final class Database {
|
||||
guard update("name", for: cap, setter: Cap.columnName <- name, Cap.columnUploaded <- false) else {
|
||||
return false
|
||||
}
|
||||
uploadRemainingData()
|
||||
startBackgroundWork()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -343,8 +394,12 @@ final class Database {
|
||||
|
||||
// MARK: Uploads
|
||||
|
||||
@discardableResult
|
||||
private func addPendingUpload(for cap: Int, version: Int) -> Bool {
|
||||
do {
|
||||
guard try db.scalar(upload.existsQuery(for: cap, version: version)) == 0 else {
|
||||
return true
|
||||
}
|
||||
try db.run(upload.insertQuery(for: cap, version: version))
|
||||
return true
|
||||
} catch {
|
||||
@@ -353,6 +408,7 @@ final class Database {
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func removePendingUpload(for cap: Int, version: Int) -> Bool {
|
||||
do {
|
||||
try db.run(upload.deleteQuery(for: cap, version: version))
|
||||
@@ -420,32 +476,18 @@ final class Database {
|
||||
|
||||
@discardableResult
|
||||
func downloadImage(for cap: Int, version: Int = 0, completion: @escaping (_ image: UIImage?) -> Void) -> Bool {
|
||||
return download.image(for: cap, version: version) { image in
|
||||
if version == 0 && image != nil {
|
||||
let url = storage.localImageUrl(for: cap, version: version)
|
||||
return download.image(for: cap, version: version, to: url) { success in
|
||||
if version == 0 && success {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.database(didLoadImageForCap: cap)
|
||||
}
|
||||
}
|
||||
let image = self.storage.image(for: cap, version: version)
|
||||
completion(image)
|
||||
}
|
||||
}
|
||||
|
||||
func downloadCapNames(completion: @escaping (_ success: Bool) -> Void) {
|
||||
log("Downloading cap names")
|
||||
download.names { names in
|
||||
guard let names = names else {
|
||||
DispatchQueue.main.async {
|
||||
completion(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
self.update(names: names)
|
||||
DispatchQueue.main.async {
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func update(names: [String]) {
|
||||
let notify = capCount > 0
|
||||
log("Downloaded cap names (initialDownload: \(!notify))")
|
||||
@@ -475,46 +517,418 @@ final class Database {
|
||||
}
|
||||
}
|
||||
|
||||
func downloadMainCapImages(progress: @escaping (_ current: Int, _ total: Int) -> Void) {
|
||||
let caps = capsWithoutImages.map { $0.id }
|
||||
|
||||
var downloaded = 0
|
||||
let total = caps.count
|
||||
|
||||
func update() {
|
||||
DispatchQueue.main.async {
|
||||
progress(downloaded, total)
|
||||
}
|
||||
var isDoingWorkInBackgound: Bool {
|
||||
backgroundTaskStatus != nil
|
||||
}
|
||||
|
||||
private var didUpdateBackgroundItems = false
|
||||
private var backgroundTaskStatus: BackgroundWorkTaskType? = nil
|
||||
private var expectedBackgroundWorkStatus: BackgroundWorkTaskType? = nil
|
||||
|
||||
private var nextBackgroundWorkStatus: BackgroundWorkTaskType? {
|
||||
guard let oldType = backgroundTaskStatus else {
|
||||
return expectedBackgroundWorkStatus
|
||||
}
|
||||
update()
|
||||
|
||||
guard total > 0 else {
|
||||
log("No images to download")
|
||||
guard let type = expectedBackgroundWorkStatus else {
|
||||
return backgroundTaskStatus?.nextType
|
||||
}
|
||||
guard oldType > type else {
|
||||
return type
|
||||
}
|
||||
return oldType.nextType
|
||||
}
|
||||
|
||||
private func setNextBackgroundWorkStatus() -> BackgroundWorkTaskType? {
|
||||
backgroundTaskStatus = nextBackgroundWorkStatus
|
||||
expectedBackgroundWorkStatus = nil
|
||||
return backgroundTaskStatus
|
||||
}
|
||||
|
||||
private let context = CIContext(options: [.workingColorSpace: kCFNull!])
|
||||
|
||||
|
||||
func startInitialDownload() {
|
||||
startBackgroundWork(startingWith: .downloadCapNames)
|
||||
}
|
||||
|
||||
func scheduleClassifierDownload() {
|
||||
startBackgroundWork(startingWith: .downloadClassifier)
|
||||
}
|
||||
|
||||
func startBackgroundWork() {
|
||||
startBackgroundWork(startingWith: .uploadingCaps)
|
||||
}
|
||||
|
||||
private func startBackgroundWork(startingWith type: BackgroundWorkTaskType) {
|
||||
guard !isDoingWorkInBackgound else {
|
||||
if expectedBackgroundWorkStatus?.rawValue ?? 0 < type.rawValue {
|
||||
log("Background work scheduled: \(type)")
|
||||
expectedBackgroundWorkStatus = type
|
||||
}
|
||||
return
|
||||
}
|
||||
log("Starting to download \(total) images")
|
||||
DispatchQueue.global(qos: .utility).async {
|
||||
self.performAllBackgroundWorkItems(allItemsStartingAt: type)
|
||||
}
|
||||
}
|
||||
|
||||
private func performAllBackgroundWorkItems(allItemsStartingAt type: BackgroundWorkTaskType) {
|
||||
didUpdateBackgroundItems = false
|
||||
expectedBackgroundWorkStatus = type
|
||||
log("Starting background task")
|
||||
while let type = setNextBackgroundWorkStatus() {
|
||||
log("Handling background task: \(type)")
|
||||
guard performAllItems(for: type) else {
|
||||
// If an error occurs, stop the background tasks
|
||||
backgroundTaskStatus = nil
|
||||
expectedBackgroundWorkStatus = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
log("Background work completed")
|
||||
delegate?.databaseDidFinishBackgroundWork()
|
||||
}
|
||||
|
||||
private func performAllItems(for type: BackgroundWorkTaskType) -> Bool {
|
||||
switch type {
|
||||
case .downloadCapNames:
|
||||
return downloadCapNames()
|
||||
case .downloadCounts:
|
||||
return downloadImageCounts()
|
||||
case .downloadClassifier:
|
||||
return downloadClassifier()
|
||||
case .uploadingCaps:
|
||||
return uploadCaps()
|
||||
case .uploadingImages:
|
||||
return uploadImages()
|
||||
case .downloadMainImages:
|
||||
return downloadMainImages()
|
||||
case .creatingThumbnails:
|
||||
return createThumbnails()
|
||||
case .creatingColors:
|
||||
return createColors()
|
||||
}
|
||||
}
|
||||
|
||||
private func downloadCapNames() -> Bool {
|
||||
log("Downloading cap names")
|
||||
let result = DispatchGroup.singleTask { callback in
|
||||
download.names { names in
|
||||
guard let names = names else {
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
self.update(names: names)
|
||||
callback(true)
|
||||
}
|
||||
}
|
||||
log("Completed download of cap names")
|
||||
return result
|
||||
}
|
||||
|
||||
private func downloadImageCounts() -> Bool {
|
||||
log("Downloading cap image counts")
|
||||
let result = DispatchGroup.singleTask { callback in
|
||||
download.imageCounts { counts in
|
||||
guard let counts = counts else {
|
||||
self.log("Failed to download server image counts")
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
let newCaps = self.didDownload(imageCounts: counts)
|
||||
|
||||
guard newCaps.count > 0 else {
|
||||
callback(true)
|
||||
return
|
||||
}
|
||||
self.log("Found \(newCaps.count) new caps on the server.")
|
||||
self.downloadInfo(for: newCaps) { success in
|
||||
callback(success)
|
||||
}
|
||||
}
|
||||
}
|
||||
guard result else {
|
||||
log("Failed download of cap image counts")
|
||||
return false
|
||||
}
|
||||
log("Completed download of cap image counts")
|
||||
return true
|
||||
}
|
||||
|
||||
private func downloadClassifier() -> Bool {
|
||||
log("Downloading classifier (if needed)")
|
||||
let result = DispatchGroup.singleTask { callback in
|
||||
download.classifierVersion { version in
|
||||
guard let version = version else {
|
||||
self.log("Failed to download server model version")
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
let ownVersion = self.classifierVersion
|
||||
guard ownVersion < version else {
|
||||
self.log("Not updating classifier: Own version \(ownVersion), server version \(version)")
|
||||
callback(true)
|
||||
return
|
||||
}
|
||||
let title = "Download classifier"
|
||||
let detail = ownVersion == 0 ?
|
||||
"A classifier to match caps is available for download (version \(version)). Would you like to download it now?" :
|
||||
"Version \(version) of the classifier is available for download (You have version \(ownVersion)). Would you like to download it now?"
|
||||
self.delegate!.database(needsUserConfirmation: title, body: detail) { proceed in
|
||||
guard proceed else {
|
||||
self.log("User skipped classifier download")
|
||||
callback(true)
|
||||
return
|
||||
}
|
||||
self.download.classifier { progress, received, total in
|
||||
let t = ByteCountFormatter.string(fromByteCount: total, countStyle: .file)
|
||||
let r = ByteCountFormatter.string(fromByteCount: received, countStyle: .file)
|
||||
let title = String(format: "%.0f", progress * 100) + " % (\(r) / \(t))"
|
||||
self.delegate?.database(completedBackgroundWorkItem: "Downloading classifier", subtitle: title)
|
||||
} completion: { url in
|
||||
guard let url = url else {
|
||||
self.log("Failed to download classifier")
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
let compiledUrl: URL
|
||||
do {
|
||||
compiledUrl = try MLModel.compileModel(at: url)
|
||||
} catch {
|
||||
self.log("Failed to compile downloaded classifier: \(error)")
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
|
||||
guard self.storage.save(recognitionModelAt: compiledUrl) else {
|
||||
self.log("Failed to save compiled classifier")
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
callback(true)
|
||||
self.classifierVersion = version
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log("Downloaded classifier (if new version existed)")
|
||||
return result
|
||||
}
|
||||
|
||||
private func uploadCaps() -> Bool {
|
||||
var completed = 0
|
||||
while let cap = nextPendingCapUpload {
|
||||
guard upload.upload(cap) else {
|
||||
delegate?.database(didFailBackgroundWork: "Upload failed",
|
||||
subtitle: "Cap \(cap.id) not uploaded")
|
||||
return false
|
||||
}
|
||||
update(uploaded: true, for: cap.id)
|
||||
completed += 1
|
||||
let total = completed + pendingCapUploadCount
|
||||
delegate?.database(completedBackgroundWorkItem: "Uploading caps", subtitle: "\(completed + 1) of \(total)")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func uploadImages() -> Bool {
|
||||
var completed = 0
|
||||
while let (id, version) = nextPendingImageUpload {
|
||||
guard let cap = self.cap(for: id) else {
|
||||
log("No cap \(id) to upload image \(version)")
|
||||
removePendingUpload(for: id, version: version)
|
||||
continue
|
||||
}
|
||||
guard let url = storage.existingImageUrl(for: cap.id, version: version) else {
|
||||
log("No image \(version) of cap \(id) to upload")
|
||||
removePendingUpload(for: id, version: version)
|
||||
continue
|
||||
}
|
||||
guard let count = upload.upload(imageAt: url, of: cap.id) else {
|
||||
delegate?.database(didFailBackgroundWork: "Upload failed", subtitle: "Image \(version) of cap \(id)")
|
||||
return false
|
||||
}
|
||||
if count > cap.count {
|
||||
update(count: count, for: cap.id)
|
||||
}
|
||||
removePendingUpload(for: id, version: version)
|
||||
|
||||
completed += 1
|
||||
let total = completed + pendingImageUploadCount
|
||||
delegate?.database(completedBackgroundWorkItem: "Uploading images", subtitle: "\(completed + 1) of \(total)")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func downloadMainImages() -> Bool {
|
||||
let missing = caps.map { $0.id }.filter { !storage.hasImage(for: $0) }
|
||||
let count = missing.count
|
||||
guard count > 0 else {
|
||||
log("No images to download")
|
||||
return true
|
||||
}
|
||||
log("Starting image downloads")
|
||||
|
||||
let group = DispatchGroup()
|
||||
let split = 50
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
for part in caps.split(intoPartsOf: split) {
|
||||
for id in part {
|
||||
let downloading = self.downloadImage(for: id) { _ in
|
||||
group.enter()
|
||||
|
||||
var shouldDownload = true
|
||||
let title = "Download images"
|
||||
let detail = "\(count) caps have no image. Would you like to download them now? (~ \(ByteCountFormatter.string(fromByteCount: Int64(count * 10000), countStyle: .file))). Grid view is not available until all images are downloaded."
|
||||
delegate?.database(needsUserConfirmation: title, body: detail) { proceed in
|
||||
shouldDownload = proceed
|
||||
group.leave()
|
||||
}
|
||||
group.wait()
|
||||
guard shouldDownload else {
|
||||
log("User skipped image download")
|
||||
return false
|
||||
}
|
||||
|
||||
group.enter()
|
||||
let queue = DispatchQueue(label: "images")
|
||||
let semaphore = DispatchSemaphore(value: 5)
|
||||
|
||||
var downloadsAreSuccessful = true
|
||||
var completed = 0
|
||||
for cap in missing {
|
||||
queue.async {
|
||||
guard downloadsAreSuccessful else {
|
||||
return
|
||||
}
|
||||
semaphore.wait()
|
||||
let url = self.storage.localImageUrl(for: cap)
|
||||
self.download.image(for: cap, to: url, queue: queue) { success in
|
||||
defer { semaphore.signal() }
|
||||
guard success else {
|
||||
self.delegate?.database(didFailBackgroundWork: "Download failed", subtitle: "Image of cap \(cap)")
|
||||
downloadsAreSuccessful = false
|
||||
group.leave()
|
||||
return
|
||||
}
|
||||
completed += 1
|
||||
self.delegate?.database(completedBackgroundWorkItem: "Downloading images", subtitle: "\(completed) of \(missing.count)")
|
||||
if completed == missing.count {
|
||||
group.leave()
|
||||
}
|
||||
if downloading {
|
||||
group.enter()
|
||||
}
|
||||
}
|
||||
if group.wait(timeout: .now() + .seconds(30)) != .success {
|
||||
self.log("Timed out waiting for images to be downloaded")
|
||||
}
|
||||
downloaded += part.count
|
||||
self.log("Finished \(downloaded) of \(total) image downloads")
|
||||
update()
|
||||
}
|
||||
self.log("Finished all image downloads")
|
||||
}
|
||||
guard group.wait(timeout: .now() + TimeInterval(missing.count * 2)) == .success else {
|
||||
log("Timed out downloading images")
|
||||
return false
|
||||
}
|
||||
log("Finished all image downloads")
|
||||
return true
|
||||
}
|
||||
|
||||
private func createThumbnails() -> Bool {
|
||||
let missing = caps.map { $0.id }.filter { !storage.hasThumbnail(for: $0) }
|
||||
guard missing.count > 0 else {
|
||||
log("No thumbnails to create")
|
||||
return true
|
||||
}
|
||||
log("Creating thumbnails")
|
||||
let queue = DispatchQueue(label: "thumbnails")
|
||||
let semaphore = DispatchSemaphore(value: 5)
|
||||
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
var thumbnailsAreSuccessful = true
|
||||
var completed = 0
|
||||
for cap in missing {
|
||||
queue.async {
|
||||
guard thumbnailsAreSuccessful else {
|
||||
return
|
||||
}
|
||||
semaphore.wait()
|
||||
defer { semaphore.signal() }
|
||||
guard let image = self.storage.image(for: cap) else {
|
||||
self.log("No image for cap \(cap) to create thumbnail")
|
||||
self.delegate?.database(didFailBackgroundWork: "Creation failed", subtitle: "Thumbnail of cap \(cap)")
|
||||
thumbnailsAreSuccessful = false
|
||||
group.leave()
|
||||
return
|
||||
}
|
||||
let thumb = Cap.thumbnail(for: image)
|
||||
guard self.storage.save(thumbnail: thumb, for: cap) else {
|
||||
self.log("Failed to save thumbnail for cap \(cap)")
|
||||
self.delegate?.database(didFailBackgroundWork: "Image not saved", subtitle: "Thumbnail of cap \(cap)")
|
||||
thumbnailsAreSuccessful = false
|
||||
group.leave()
|
||||
return
|
||||
}
|
||||
completed += 1
|
||||
self.delegate?.database(completedBackgroundWorkItem: "Creating thumbnails", subtitle: "\(completed) of \(missing.count)")
|
||||
if completed == missing.count {
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
guard group.wait(timeout: .now() + TimeInterval(missing.count * 2)) == .success else {
|
||||
log("Timed out creating thumbnails")
|
||||
return false
|
||||
}
|
||||
log("Finished all thumbnails")
|
||||
return true
|
||||
}
|
||||
|
||||
private func createColors() -> Bool {
|
||||
let missing = capIds.subtracting(capsWithColors)
|
||||
guard missing.count > 0 else {
|
||||
log("No colors to create")
|
||||
return true
|
||||
}
|
||||
log("Creating colors")
|
||||
let queue = DispatchQueue(label: "colors")
|
||||
let semaphore = DispatchSemaphore(value: 5)
|
||||
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
var colorsAreSuccessful = true
|
||||
var completed = 0
|
||||
for cap in missing {
|
||||
queue.async {
|
||||
guard colorsAreSuccessful else {
|
||||
return
|
||||
}
|
||||
semaphore.wait()
|
||||
defer { semaphore.signal() }
|
||||
guard let image = self.storage.ciImage(for: cap) else {
|
||||
self.log("No image for cap \(cap) to create color")
|
||||
self.delegate?.database(didFailBackgroundWork: "No thumbnail found", subtitle: "Color of cap \(cap)")
|
||||
colorsAreSuccessful = false
|
||||
group.leave()
|
||||
return
|
||||
}
|
||||
defer { self.context.clearCaches() }
|
||||
guard let color = image.averageColor(context: self.context) else {
|
||||
self.log("Failed to create color for cap \(cap)")
|
||||
self.delegate?.database(didFailBackgroundWork: "Calculation failed", subtitle: "Color of cap \(cap)")
|
||||
colorsAreSuccessful = false
|
||||
group.leave()
|
||||
return
|
||||
}
|
||||
guard self.set(color: color, for: cap) else {
|
||||
self.log("Failed to save color for cap \(cap)")
|
||||
self.delegate?.database(didFailBackgroundWork: "Color not saved", subtitle: "Color of cap \(cap)")
|
||||
colorsAreSuccessful = false
|
||||
group.leave()
|
||||
return
|
||||
}
|
||||
completed += 1
|
||||
self.delegate?.database(completedBackgroundWorkItem: "Creating colors", subtitle: "\(completed) of \(missing.count)")
|
||||
if completed == missing.count {
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
guard group.wait(timeout: .now() + TimeInterval(missing.count * 2)) == .success else {
|
||||
log("Timed out creating colors")
|
||||
return false
|
||||
}
|
||||
log("Finished all colors")
|
||||
return true
|
||||
}
|
||||
|
||||
func hasNewClassifier(completion: @escaping (_ version: Int?, _ size: Int64?) -> Void) {
|
||||
@@ -537,65 +951,6 @@ final class Database {
|
||||
}
|
||||
}
|
||||
|
||||
func downloadClassifier(progress: Download.Delegate.ProgressHandler? = nil, completion: @escaping (_ success: Bool) -> Void) {
|
||||
download.classifier(progress: progress) { url in
|
||||
guard let url = url else {
|
||||
self.log("Failed to download classifier")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
let compiledUrl: URL
|
||||
do {
|
||||
compiledUrl = try MLModel.compileModel(at: url)
|
||||
} catch {
|
||||
self.log("Failed to compile downloaded classifier: \(error)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
guard app.storage.save(recognitionModelAt: compiledUrl) else {
|
||||
self.log("Failed to save classifier")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
completion(true)
|
||||
self.download.classifierVersion { version in
|
||||
guard let version = version else {
|
||||
self.log("Failed to download classifier version")
|
||||
return
|
||||
}
|
||||
self.classifierVersion = version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadImageCounts(completion: @escaping (_ success: Bool) -> Void) {
|
||||
log("Refreshing server image counts")
|
||||
download.imageCounts { counts in
|
||||
guard let counts = counts else {
|
||||
self.log("Failed to download server image counts")
|
||||
DispatchQueue.main.async {
|
||||
completion(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
let newCaps = self.didDownload(imageCounts: counts)
|
||||
|
||||
guard newCaps.count > 0 else {
|
||||
DispatchQueue.main.async {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
self.log("Found \(newCaps.count) new caps on the server.")
|
||||
self.downloadInfo(for: newCaps) { success in
|
||||
DispatchQueue.main.async {
|
||||
completion(success)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func didDownload(imageCounts newCounts: [Int]) -> [Int : Int] {
|
||||
let capsCounts = capDict
|
||||
if newCounts.count != capsCounts.count {
|
||||
@@ -662,119 +1017,6 @@ final class Database {
|
||||
}
|
||||
}
|
||||
|
||||
private func uploadNextItem() {
|
||||
let capUploads = self.pendingCapUploads
|
||||
if let id = capUploads.first {
|
||||
|
||||
return
|
||||
}
|
||||
let imageUploads = pendingImageUploads
|
||||
guard imageUploads.count > 0 else {
|
||||
log("No pending image uploads")
|
||||
return
|
||||
}
|
||||
uploadRemainingImages()
|
||||
}
|
||||
|
||||
private func upload(cap: Int) {
|
||||
|
||||
}
|
||||
|
||||
func uploadRemainingData() {
|
||||
guard !isInOfflineMode else {
|
||||
log("Not uploading pending data due to offline mode")
|
||||
return
|
||||
}
|
||||
let uploads = self.pendingCapUploads
|
||||
guard uploads.count > 0 else {
|
||||
log("No pending cap uploads")
|
||||
uploadRemainingImages()
|
||||
return
|
||||
}
|
||||
log("\(uploads.count) cap uploads pending")
|
||||
|
||||
var remaining = uploads.count
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
let group = DispatchGroup()
|
||||
for cap in uploads {
|
||||
group.enter()
|
||||
self.upload.upload(name: cap.name, for: cap.id) { success in
|
||||
group.leave()
|
||||
if success {
|
||||
self.log("Uploaded cap \(cap.id)")
|
||||
self.update(uploaded: true, for: cap.id)
|
||||
} else {
|
||||
self.log("Failed to upload cap \(cap.id)")
|
||||
return
|
||||
}
|
||||
|
||||
remaining -= 1
|
||||
|
||||
}
|
||||
guard group.wait(timeout: .now() + .seconds(60)) == .success else {
|
||||
self.log("Timed out uploading cap \(cap.id)")
|
||||
return
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.uploadRemainingImages()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func uploadRemainingImages() {
|
||||
let uploads = pendingImageUploads
|
||||
guard uploads.count > 0 else {
|
||||
log("No pending image uploads")
|
||||
return
|
||||
}
|
||||
log("\(uploads.count) image uploads pending")
|
||||
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
let group = DispatchGroup()
|
||||
for (id, version) in uploads {
|
||||
guard let cap = self.cap(for: id) else {
|
||||
self.log("No cap \(id) to upload image \(version)")
|
||||
self.removePendingUpload(of: id, version: version)
|
||||
continue
|
||||
}
|
||||
guard cap.uploaded else {
|
||||
self.log("Cap \(id) not uploaded, skipping image upload")
|
||||
continue
|
||||
}
|
||||
group.enter()
|
||||
self.upload.uploadImage(for: id, version: version) { count in
|
||||
group.leave()
|
||||
guard let _ = count else {
|
||||
self.log("Failed to upload version \(version) of cap \(id)")
|
||||
return
|
||||
}
|
||||
self.log("Uploaded version \(version) of cap \(id)")
|
||||
self.removePendingUpload(of: id, version: version)
|
||||
}
|
||||
guard group.wait(timeout: .now() + .seconds(60)) == .success else {
|
||||
self.log("Timed out uploading version \(version) of cap \(id)")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func removePendingUpload(of cap: Int, version: Int) -> Bool {
|
||||
do {
|
||||
let query = upload.table.filter(upload.rowCapId == cap && upload.rowCapVersion == version).delete()
|
||||
try db.run(query)
|
||||
log("Deleted pending upload of cap \(cap) version \(version)")
|
||||
return true
|
||||
} catch {
|
||||
log("Failed to delete pending upload of cap \(cap) version \(version)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func setMainImage(of cap: Int, to version: Int) {
|
||||
guard version != 0 else {
|
||||
log("No need to switch main image with itself for cap \(cap)")
|
||||
@@ -785,7 +1027,7 @@ final class Database {
|
||||
self.log("Could not make \(version) the main image for cap \(cap)")
|
||||
return
|
||||
}
|
||||
guard app.storage.switchMainImage(to: version, for: cap) else {
|
||||
guard self.storage.switchMainImage(to: version, for: cap) else {
|
||||
self.log("Could not switch \(version) to main image for cap \(cap)")
|
||||
return
|
||||
}
|
||||
|
@@ -111,6 +111,25 @@ final class Download {
|
||||
|
||||
// MARK: Downloading data
|
||||
|
||||
func image(for cap: Int, to url: URL, timeout: TimeInterval = 30) -> Bool {
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
var result = true
|
||||
let success = image(for: cap, version: 0, to: url) { success in
|
||||
result = success
|
||||
group.leave()
|
||||
}
|
||||
guard success else {
|
||||
log("Already downloading image for cap \(cap)")
|
||||
return false
|
||||
}
|
||||
guard group.wait(timeout: .now() + timeout) == .success else {
|
||||
log("Timed out downloading image for cap \(cap)")
|
||||
return false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
Download an image for a cap.
|
||||
- Parameter cap: The id of the cap.
|
||||
@@ -119,7 +138,7 @@ final class Download {
|
||||
- Returns: `true`, of the file download was started, `false`, if the image is already downloading.
|
||||
*/
|
||||
@discardableResult
|
||||
func image(for cap: Int, version: Int = 0, completion: @escaping (_ image: UIImage?) -> Void) -> Bool {
|
||||
func image(for cap: Int, version: Int = 0, to url: URL, queue: DispatchQueue = .main, completion: @escaping (Bool) -> Void) -> Bool {
|
||||
// Check if main image, and already being downloaded
|
||||
if version == 0 {
|
||||
guard !downloadingMainImages.contains(cap) else {
|
||||
@@ -127,24 +146,28 @@ final class Download {
|
||||
}
|
||||
downloadingMainImages.insert(cap)
|
||||
}
|
||||
let url = serverImageUrl(for: cap, version: version)
|
||||
let serverUrl = serverImageUrl(for: cap, version: version)
|
||||
let query = "Image of cap \(cap) version \(version)"
|
||||
let task = session.downloadTask(with: url) { fileUrl, response, error in
|
||||
let task = session.downloadTask(with: serverUrl) { fileUrl, response, error in
|
||||
if version == 0 {
|
||||
DispatchQueue.main.async {
|
||||
queue.async {
|
||||
self.downloadingMainImages.remove(cap)
|
||||
}
|
||||
}
|
||||
guard let fileUrl = self.convertResponse(to: query, fileUrl, response, error) else {
|
||||
completion(nil)
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
guard let image = app.storage.saveImage(at: fileUrl, for: cap, version: version) else {
|
||||
self.log("Request '\(query)' could not move downloaded file")
|
||||
completion(nil)
|
||||
return
|
||||
do {
|
||||
if FileManager.default.fileExists(atPath: url.path) {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
}
|
||||
try FileManager.default.moveItem(at: fileUrl, to: url)
|
||||
} catch {
|
||||
self.log("Failed to move downloaded image for cap \(cap): \(error)")
|
||||
completion(false)
|
||||
}
|
||||
completion(image)
|
||||
completion(true)
|
||||
}
|
||||
task.resume()
|
||||
return true
|
||||
|
@@ -11,7 +11,30 @@ import UIKit
|
||||
import CoreML
|
||||
import Vision
|
||||
|
||||
final class Storage {
|
||||
|
||||
|
||||
protocol ImageProvider: class {
|
||||
|
||||
func image(for cap: Int) -> UIImage?
|
||||
|
||||
func image(for cap: Int, version: Int) -> UIImage?
|
||||
|
||||
func ciImage(for cap: Int) -> CIImage?
|
||||
}
|
||||
|
||||
protocol ThumbnailCreationDelegate {
|
||||
|
||||
func thumbnailCreation(progress: Int, total: Int)
|
||||
|
||||
func thumbnailCreationFailed()
|
||||
|
||||
func thumbnailCreationIsMissingImages()
|
||||
|
||||
func thumbnailCreationCompleted()
|
||||
}
|
||||
|
||||
|
||||
final class Storage: ImageProvider {
|
||||
|
||||
// MARK: Paths
|
||||
|
||||
@@ -35,7 +58,7 @@ final class Storage {
|
||||
baseUrl.appendingPathComponent("model.mlmodel")
|
||||
}
|
||||
|
||||
private func localImageUrl(for cap: Int, version: Int) -> URL {
|
||||
func localImageUrl(for cap: Int, version: Int = 0) -> URL {
|
||||
baseUrl.appendingPathComponent("\(cap)-\(version).jpg")
|
||||
}
|
||||
|
||||
@@ -114,7 +137,7 @@ final class Storage {
|
||||
- parameter cap: The cap id
|
||||
- returns: True, if the image was saved
|
||||
*/
|
||||
func save(thumbnailData: Data, for cap: Int) -> Bool {
|
||||
private func save(thumbnailData: Data, for cap: Int) -> Bool {
|
||||
write(thumbnailData, to: thumbnailUrl(for: cap))
|
||||
}
|
||||
|
||||
@@ -227,7 +250,7 @@ final class Storage {
|
||||
- note: Removes invalid image data on disk, if the data is not a valid image
|
||||
- note: Must be called on the main thread
|
||||
*/
|
||||
func image(for cap: Int, version: Int = 0) -> UIImage? {
|
||||
func image(for cap: Int, version: Int) -> UIImage? {
|
||||
guard let data = imageData(for: cap, version: version) else {
|
||||
return nil
|
||||
}
|
||||
@@ -239,6 +262,10 @@ final class Storage {
|
||||
return image
|
||||
}
|
||||
|
||||
func image(for cap: Int) -> UIImage? {
|
||||
image(for: cap, version: 0)
|
||||
}
|
||||
|
||||
/**
|
||||
Get the thumbnail data for a cap.
|
||||
If the image exists on disk, it is returned.
|
||||
|
@@ -57,6 +57,10 @@ struct Upload {
|
||||
}
|
||||
}
|
||||
|
||||
func existsQuery(for cap: Int, version: Int) -> ScalarQuery<Int> {
|
||||
table.filter(rowCapId == cap && rowCapVersion == version).count
|
||||
}
|
||||
|
||||
func insertQuery(for cap: Int, version: Int) -> Insert {
|
||||
table.insert(rowCapId <- cap, rowCapVersion <- version)
|
||||
}
|
||||
@@ -97,36 +101,55 @@ struct Upload {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func uploadImage(for cap: Int, version: Int, completion: @escaping (_ count: Int?) -> Void) {
|
||||
guard let url = app.storage.existingImageUrl(for: cap, version: version) else {
|
||||
completion(nil)
|
||||
return
|
||||
func upload(_ cap: Cap, timeout: TimeInterval = 30) -> Bool {
|
||||
upload(name: cap.name, for: cap.id, timeout: timeout)
|
||||
}
|
||||
|
||||
func upload(name: String, for cap: Int, timeout: TimeInterval = 30) -> Bool {
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
var result = true
|
||||
upload(name: name, for: cap) { success in
|
||||
if success {
|
||||
self.log("Uploaded cap \(cap)")
|
||||
} else {
|
||||
result = false
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
guard group.wait(timeout: .now() + timeout) == .success else {
|
||||
log("Timed out uploading cap \(cap)")
|
||||
return false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func upload(imageAt url: URL, for cap: Int, completion: @escaping (_ count: Int?) -> Void) {
|
||||
var request = URLRequest(url: serverImageUploadUrl(for: cap))
|
||||
request.httpMethod = "POST"
|
||||
let task = URLSession.shared.uploadTask(with: request, fromFile: url) { data, response, error in
|
||||
if let error = error {
|
||||
self.log("Failed to upload image \(version) of cap \(cap): \(error)")
|
||||
self.log("Failed to upload image of cap \(cap): \(error)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let response = response else {
|
||||
self.log("Failed to upload image \(version) of cap \(cap): No response")
|
||||
self.log("Failed to upload image of cap \(cap): No response")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let urlResponse = response as? HTTPURLResponse else {
|
||||
self.log("Failed to upload image \(version) of cap \(cap): \(response)")
|
||||
self.log("Failed to upload image of cap \(cap): \(response)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard urlResponse.statusCode == 200 else {
|
||||
self.log("Failed to upload image \(version) of cap \(cap): Response \(urlResponse.statusCode)")
|
||||
self.log("Failed to upload image of cap \(cap): Response \(urlResponse.statusCode)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let d = data, let string = String(data: d, encoding: .utf8), let int = Int(string) else {
|
||||
self.log("Failed to upload image \(version) of cap \(cap): Invalid response")
|
||||
self.log("Failed to upload image of cap \(cap): Invalid response")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
@@ -135,6 +158,21 @@ struct Upload {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func upload(imageAt url: URL, of cap: Int, timeout: TimeInterval = 30) -> Int? {
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
var result: Int? = nil
|
||||
upload(imageAt: url, for: cap) { count in
|
||||
result = count
|
||||
group.leave()
|
||||
}
|
||||
guard group.wait(timeout: .now() + timeout) == .success else {
|
||||
log("Timed out uploading image of \(cap)")
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the main image for a cap to a different version.
|
||||
|
||||
|
Reference in New Issue
Block a user