Add new images to image cache

This commit is contained in:
Christoph Hagen 2022-06-24 12:06:39 +02:00
parent 9b9ce63f7c
commit 68f82ce367
2 changed files with 94 additions and 59 deletions

View File

@ -50,7 +50,28 @@ final class Database: ObservableObject {
} }
} }
private lazy var imageUploads: [Int: Int] = loadImageUploadCounts() @AppStorage("uploads")
private var pendingImageUploadStorage: String = ""
private(set) var imageUploads: [Int: [Int]] {
get {
pendingImageUploadStorage.components(separatedBy: ";").reduce(into: [:]) { dict, string in
let parts = string.components(separatedBy: "-")
guard parts.count == 2 else {
return
}
guard let cap = Int(parts[0]) else {
return
}
dict[cap] = parts[1].components(separatedBy: ":").compactMap(Int.init)
}
}
set {
pendingImageUploadStorage = newValue.map { cap, images in
"\(cap)-\(images.map { "\($0)" }.joined(separator: ":"))"
}.joined(separator: ";")
}
}
private var uploadTimer: Timer? private var uploadTimer: Timer?
@ -128,7 +149,7 @@ final class Database: ObservableObject {
} }
private var serverClassifierVersionUrl: URL { private var serverClassifierVersionUrl: URL {
serverUrl.appendingPathComponent("classifier.version") serverUrl.appendingPathComponent("version")
} }
private var gridStorageFolder: URL { private var gridStorageFolder: URL {
@ -255,7 +276,10 @@ final class Database: ObservableObject {
#warning("Merge changed caps with server updates") #warning("Merge changed caps with server updates")
} else { } else {
oldCap.update(with: cap) oldCap.update(with: cap)
caps[cap.id] = oldCap let save = oldCap
DispatchQueue.main.async {
self.caps[cap.id] = save
}
updates += 1 updates += 1
} }
} }
@ -289,10 +313,10 @@ final class Database: ObservableObject {
self.serverClassifierVersion = serverVersion self.serverClassifierVersion = serverVersion
} }
guard serverVersion > self.classifierVersion else { guard serverVersion > self.classifierVersion else {
print("No new classifier available") print("No new classifier available (Local: \(classifierVersion) Server: \(serverVersion))")
return false return false
} }
print("New classifier \(serverVersion) available") print("New classifier available (Local: \(classifierVersion) Server: \(serverVersion))")
return true return true
} }
@ -348,45 +372,29 @@ final class Database: ObservableObject {
@discardableResult @discardableResult
func save(_ image: UIImage, for capId: Int) -> Bool { func save(_ image: UIImage, for capId: Int) -> Bool {
guard caps[capId] != nil else { guard let cap = caps[capId] else {
log("Failed to save image for missing cap \(capId)") log("Failed to save image for missing cap \(capId)")
return false return false
} }
guard ensureFolderExistence(imageUploadFolderUrl) else { guard images.save(image, for: CapImage(cap: cap.id, version: cap.imageCount)) else {
return false return false
} }
guard let data = image.jpegData(compressionQuality: imageCompressionQuality) else { log("Saved image \(cap.imageCount) for cap \(capId)")
log("Failed to compress image for cap: \(capId)") if imageUploads[capId] != nil {
return false DispatchQueue.main.async {
self.imageUploads[capId]!.append(cap.imageCount)
}
} else {
DispatchQueue.main.async {
self.imageUploads[capId] = [cap.imageCount]
}
} }
let hash = Data(SHA256.hash(data: data)).hexEncoded.prefix(16) DispatchQueue.main.async {
let url = imageUploadFolderUrl.appendingPathComponent("\(capId)-\(hash).jpg") self.caps[capId]!.imageCount += 1
do {
try data.write(to: url)
} catch {
log("Failed to save \(url.lastPathComponent): \(error)")
return false
} }
log("Saved \(url.lastPathComponent) for upload")
caps[capId]?.imageCount += 1
return true return true
} }
private func loadImageUploadCounts() -> [Int : Int] {
var result = [Int : Int]()
pendingImageUploads.forEach { url in
guard let capId = capId(from: url) else {
return
}
if let old = result[capId] {
result[capId] = old + 1
} else {
result[capId] = 1
}
}
return result
}
// MARK: Uploads // MARK: Uploads
func startRegularUploads() { func startRegularUploads() {
@ -437,12 +445,8 @@ final class Database: ObservableObject {
changedCaps.contains(cap) || imageUploads[cap] != nil changedCaps.contains(cap) || imageUploads[cap] != nil
} }
private var pendingImageUploads: [URL] {
(try? fm.contentsOfDirectory(at: imageUploadFolderUrl, includingPropertiesForKeys: nil)) ?? []
}
var pendingImageUploadCount: Int { var pendingImageUploadCount: Int {
pendingImageUploads.count imageUploads.values.reduce(0) { $0 + $1.count }
} }
private func capId(from url: URL) -> Int? { private func capId(from url: URL) -> Int? {
@ -454,24 +458,27 @@ final class Database: ObservableObject {
log("No server authentication to upload to server") log("No server authentication to upload to server")
return return
} }
for url in pendingImageUploads { for (cap, images) in imageUploads {
guard let capId = capId(from: url) else { for image in images {
log("Unexpected image \(url.lastPathComponent) in upload folder") guard let url = self.images.availableImageUrl(CapImage(cap: cap, version: image)) else {
continue log("Missing upload image \(image) for cap \(cap)")
} continue
guard fm.fileExists(atPath: url.path) else { }
log("Missing image \(url.lastPathComponent) in upload folder") guard await upload(imageAt: url, for: cap) else {
continue log("Failed to upload image \(url.lastPathComponent)")
} continue
guard await upload(imageAt: url, for: capId) else { }
log("Failed to upload image \(url.lastPathComponent)") log("Uploaded image \(image) for cap \(cap)")
continue let remaining = imageUploads[cap]?.filter { $0 != image }
} if let r = remaining, !r.isEmpty {
log("Uploaded image \(url.lastPathComponent)") DispatchQueue.main.async {
do { self.imageUploads[cap] = r
try fm.removeItem(at: url) }
} catch { } else {
log("Failed to remove uploaded image \(url.lastPathComponent): \(error)") DispatchQueue.main.async {
self.imageUploads[cap] = nil
}
}
} }
} }
} }

View File

@ -15,6 +15,8 @@ final class ImageCache {
private let thumbnailQuality: CGFloat = 0.7 private let thumbnailQuality: CGFloat = 0.7
private let imageQuality: CGFloat = 0.3
init(folder: URL, server: URL, thumbnailSize: CGFloat) throws { init(folder: URL, server: URL, thumbnailSize: CGFloat) throws {
self.folder = folder self.folder = folder
self.server = server self.server = server
@ -26,13 +28,39 @@ final class ImageCache {
} }
private func localImageUrl(_ image: CapImage) -> URL { private func localImageUrl(_ image: CapImage) -> URL {
folder.appendingPathComponent(String(format: "%04d-%02d.jpg", image.cap, image.cap, image.version)) folder.appendingPathComponent(String(format: "%04d-%02d.jpg", image.cap, image.version))
} }
private func remoteImageUrl(_ image: CapImage) -> URL { private func remoteImageUrl(_ image: CapImage) -> URL {
server.appendingPathComponent(String(format: "images/%04d/%04d-%02d.jpg", image.cap, image.cap, image.version)) server.appendingPathComponent(String(format: "images/%04d/%04d-%02d.jpg", image.cap, image.cap, image.version))
} }
@discardableResult
func save(_ image: UIImage, for cap: CapImage) -> Bool {
guard let data = image.jpegData(compressionQuality: imageQuality) else {
return false
}
let localUrl = localImageUrl(cap)
guard removePossibleFile(localUrl) else {
return false
}
do {
try data.write(to: localUrl)
return true
} catch {
print("failed to save image \(localUrl.lastPathComponent): \(error)")
return false
}
}
func availableImageUrl(_ image: CapImage) -> URL? {
let url = localImageUrl(image)
guard exists(url) else {
return nil
}
return url
}
func image(_ image: CapImage, completion: @escaping (UIImage?) -> ()) { func image(_ image: CapImage, completion: @escaping (UIImage?) -> ()) {
Task { Task {
let image = await self.image(image) let image = await self.image(image)