Add new images to image cache
This commit is contained in:
parent
9b9ce63f7c
commit
68f82ce367
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user