fix: manifest cache not updated after backup causes NAS Archive = 1 and backup loop

- writeManifest now updates NASManifestCache after every successful NAS write;
  previously the fast-path reconcile would read the pre-backup cache entry
  (e.g. 1 entry) and overwrite nasArchiveTotal / alreadySafe with stale values
- BackupEngine seeds NASManifestCache when it loads the manifest so the cache
  is always at least as fresh as what the engine sees at backup-start time
- Remove snapshot.nasArchiveTotal += uploaded from refreshAfterBackup; the
  correct total comes from the merged manifest written to NAS, not an additive
  delta from a potentially-wrong baseline
- Fix JSONDecoder() → JSONDecoder.kisani throughout manifest load paths so
  ISO8601 dates decode correctly instead of silently failing and returning nil
- Add retry with exponential backoff (max 3 attempts, 2 s / 4 s intervals)
  to the upload loop; permanently-failed items are now marked .failed only
  after all retries are exhausted

Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
This commit is contained in:
Robin Kutesa
2026-05-18 00:03:10 +03:00
parent a98822d7b2
commit c7555a2001
2 changed files with 100 additions and 59 deletions

View File

@@ -68,7 +68,8 @@ final class BackupStatusService: NSObject, ObservableObject {
func refreshAfterBackup(entries: [ManifestEntry], connection: NASConnection) {
let uploaded = entries.count
snapshot.alreadySafe = min(snapshot.phoneTotal, snapshot.alreadySafe + uploaded)
snapshot.nasArchiveTotal += uploaded
// nasArchiveTotal is NOT updated optimistically writeManifest will update
// NASManifestCache with the full merged count, and refresh(force: true) reads it.
saveSnapshot()
log.info("Post-backup optimistic update: +\(uploaded) safe")
Task {
@@ -234,7 +235,7 @@ final class BackupStatusService: NSObject, ObservableObject {
let data = try await transfer.downloadData(at: path)
// Decode and build index off main actor JSONDecoder is not cheap for large manifests.
let decoded: (ManifestIndex, BackupManifest)? = await Task.detached(priority: .utility) {
guard let manifest = try? JSONDecoder().decode(BackupManifest.self, from: data)
guard let manifest = try? JSONDecoder.kisani.decode(BackupManifest.self, from: data)
else { return nil }
return (ManifestIndex(manifest: manifest), manifest)
}.value
@@ -332,23 +333,29 @@ final class BackupStatusService: NSObject, ObservableObject {
let existingData = try? await transfer.downloadData(at: manifestPath)
// Merge + JSON encode off main actor can be slow for large manifests.
let encoded: Data? = await Task.detached(priority: .utility) {
let result: (data: Data, manifest: BackupManifest)? = await Task.detached(priority: .utility) {
var manifest: BackupManifest
if let data = existingData,
let existing = try? JSONDecoder().decode(BackupManifest.self, from: data) {
let existing = try? JSONDecoder.kisani.decode(BackupManifest.self, from: data) {
manifest = existing
} else {
manifest = BackupManifest()
}
manifest.merge(entries: entries)
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
return try? encoder.encode(manifest)
guard let encoded = try? JSONEncoder.kisani.encode(manifest) else { return nil }
return (encoded, manifest)
}.value
guard let encoded else { return }
guard let (encoded, mergedManifest) = result else { return }
try await transfer.writeData(encoded, to: manifestPath)
log.info("Manifest written — \(entries.count) new entries")
// Update local cache immediately the next reconcile's fast path will read
// this fresh cache instead of re-downloading from NAS, preventing the stale-
// cache bug where nasArchiveTotal resets to the pre-backup count.
await NASManifestCache.shared.update(mergedManifest)
lastManifest = mergedManifest
log.info("Manifest written — \(entries.count) new entries, total \(mergedManifest.entries.count)")
} catch {
log.error("Manifest write failed (non-fatal): \(error.localizedDescription)")
}