Add new images to image cache
This commit is contained in:
parent
9b9ce63f7c
commit
68f82ce367
@ -49,8 +49,29 @@ final class Database: ObservableObject {
|
||||
changedCapStorage = newValue.map { "\($0)" }.joined(separator: ",")
|
||||
}
|
||||
}
|
||||
|
||||
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?
|
||||
|
||||
@ -128,7 +149,7 @@ final class Database: ObservableObject {
|
||||
}
|
||||
|
||||
private var serverClassifierVersionUrl: URL {
|
||||
serverUrl.appendingPathComponent("classifier.version")
|
||||
serverUrl.appendingPathComponent("version")
|
||||
}
|
||||
|
||||
private var gridStorageFolder: URL {
|
||||
@ -255,7 +276,10 @@ final class Database: ObservableObject {
|
||||
#warning("Merge changed caps with server updates")
|
||||
} else {
|
||||
oldCap.update(with: cap)
|
||||
caps[cap.id] = oldCap
|
||||
let save = oldCap
|
||||
DispatchQueue.main.async {
|
||||
self.caps[cap.id] = save
|
||||
}
|
||||
updates += 1
|
||||
}
|
||||
}
|
||||
@ -289,10 +313,10 @@ final class Database: ObservableObject {
|
||||
self.serverClassifierVersion = serverVersion
|
||||
}
|
||||
guard serverVersion > self.classifierVersion else {
|
||||
print("No new classifier available")
|
||||
print("No new classifier available (Local: \(classifierVersion) Server: \(serverVersion))")
|
||||
return false
|
||||
}
|
||||
print("New classifier \(serverVersion) available")
|
||||
print("New classifier available (Local: \(classifierVersion) Server: \(serverVersion))")
|
||||
return true
|
||||
}
|
||||
|
||||
@ -348,45 +372,29 @@ final class Database: ObservableObject {
|
||||
|
||||
@discardableResult
|
||||
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)")
|
||||
return false
|
||||
}
|
||||
guard ensureFolderExistence(imageUploadFolderUrl) else {
|
||||
guard images.save(image, for: CapImage(cap: cap.id, version: cap.imageCount)) else {
|
||||
return false
|
||||
}
|
||||
guard let data = image.jpegData(compressionQuality: imageCompressionQuality) else {
|
||||
log("Failed to compress image for cap: \(capId)")
|
||||
return false
|
||||
log("Saved image \(cap.imageCount) for cap \(capId)")
|
||||
if imageUploads[capId] != nil {
|
||||
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)
|
||||
let url = imageUploadFolderUrl.appendingPathComponent("\(capId)-\(hash).jpg")
|
||||
do {
|
||||
try data.write(to: url)
|
||||
} catch {
|
||||
log("Failed to save \(url.lastPathComponent): \(error)")
|
||||
return false
|
||||
DispatchQueue.main.async {
|
||||
self.caps[capId]!.imageCount += 1
|
||||
}
|
||||
log("Saved \(url.lastPathComponent) for upload")
|
||||
caps[capId]?.imageCount += 1
|
||||
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
|
||||
|
||||
func startRegularUploads() {
|
||||
@ -437,12 +445,8 @@ final class Database: ObservableObject {
|
||||
changedCaps.contains(cap) || imageUploads[cap] != nil
|
||||
}
|
||||
|
||||
private var pendingImageUploads: [URL] {
|
||||
(try? fm.contentsOfDirectory(at: imageUploadFolderUrl, includingPropertiesForKeys: nil)) ?? []
|
||||
}
|
||||
|
||||
var pendingImageUploadCount: Int {
|
||||
pendingImageUploads.count
|
||||
imageUploads.values.reduce(0) { $0 + $1.count }
|
||||
}
|
||||
|
||||
private func capId(from url: URL) -> Int? {
|
||||
@ -454,24 +458,27 @@ final class Database: ObservableObject {
|
||||
log("No server authentication to upload to server")
|
||||
return
|
||||
}
|
||||
for url in pendingImageUploads {
|
||||
guard let capId = capId(from: url) else {
|
||||
log("Unexpected image \(url.lastPathComponent) in upload folder")
|
||||
continue
|
||||
}
|
||||
guard fm.fileExists(atPath: url.path) else {
|
||||
log("Missing image \(url.lastPathComponent) in upload folder")
|
||||
continue
|
||||
}
|
||||
guard await upload(imageAt: url, for: capId) else {
|
||||
log("Failed to upload image \(url.lastPathComponent)")
|
||||
continue
|
||||
}
|
||||
log("Uploaded image \(url.lastPathComponent)")
|
||||
do {
|
||||
try fm.removeItem(at: url)
|
||||
} catch {
|
||||
log("Failed to remove uploaded image \(url.lastPathComponent): \(error)")
|
||||
for (cap, images) in imageUploads {
|
||||
for image in images {
|
||||
guard let url = self.images.availableImageUrl(CapImage(cap: cap, version: image)) else {
|
||||
log("Missing upload image \(image) for cap \(cap)")
|
||||
continue
|
||||
}
|
||||
guard await upload(imageAt: url, for: cap) else {
|
||||
log("Failed to upload image \(url.lastPathComponent)")
|
||||
continue
|
||||
}
|
||||
log("Uploaded image \(image) for cap \(cap)")
|
||||
let remaining = imageUploads[cap]?.filter { $0 != image }
|
||||
if let r = remaining, !r.isEmpty {
|
||||
DispatchQueue.main.async {
|
||||
self.imageUploads[cap] = r
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.imageUploads[cap] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ final class ImageCache {
|
||||
private let session: URLSession = .shared
|
||||
|
||||
private let thumbnailQuality: CGFloat = 0.7
|
||||
|
||||
private let imageQuality: CGFloat = 0.3
|
||||
|
||||
init(folder: URL, server: URL, thumbnailSize: CGFloat) throws {
|
||||
self.folder = folder
|
||||
@ -26,12 +28,38 @@ final class ImageCache {
|
||||
}
|
||||
|
||||
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 {
|
||||
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?) -> ()) {
|
||||
Task {
|
||||
|
Loading…
Reference in New Issue
Block a user