fix: NAS Archive always shows real directory count, not manifest entry count
- NASManifestCache: add directoryCount field (Int?) stored independently from manifest.entries.count; add setDirectoryCount(_:) and addToDirectoryCount(_:); update(_:) now preserves existing directoryCount so manifest refreshes never reset the real folder count to zero - BackupEngine step 3: switch from "NAS if non-empty, else cache" to max(NAS, cache) by entry count — a 1-entry NAS manifest no longer overwrites a 264-entry cache, preventing history loss and the Already Safe = 1 regression - BackupEngine post-upload: call addToDirectoryCount(uploaded) so NAS Archive increments correctly (7422 → 7423) without requiring a full directory rescan - BackupStatusService.buildManifestIndex: call setDirectoryCount(dirCount) in both the validity-check path (manifest ≤ 1 entry) and the bootstrap path so the real folder count survives across reconcile cycles - BackupStatusService.writeManifest: same max(NAS, cache) base-selection logic to fix Already Safe = 1 after backup ends (merge base was 1-entry NAS instead of 264-entry cache) - BackupStatusService.reconcile (fast + full path): use max(index.totalCount, directoryCount) for nasArchiveTotal so Gallery and stats strip reflect the real NAS folder count, not just Kisani manifest entries Co-Authored-By: Kutesir <kutesir@provoc.ug> Co-Authored-By: Sentry <sentry@provoc.ug>
This commit is contained in:
@@ -19,33 +19,70 @@ actor NASManifestCache {
|
||||
private struct CachedEntry: Codable {
|
||||
var manifest: BackupManifest
|
||||
var savedAt: Date
|
||||
/// Real NAS destination folder file count from the last listDirectory scan.
|
||||
/// Stored independently from manifest.entries.count — the folder may contain
|
||||
/// files uploaded by other tools that are not in Kisani's manifest.
|
||||
var directoryCount: Int?
|
||||
}
|
||||
|
||||
private init() {
|
||||
let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
diskURL = caches.appendingPathComponent("kisani_manifest_cache.json")
|
||||
// Load from disk immediately so the first `manifest` access is instant.
|
||||
if let data = try? Data(contentsOf: diskURL),
|
||||
let entry = try? JSONDecoder.kisani.decode(CachedEntry.self, from: data) {
|
||||
cached = entry
|
||||
log.info("ManifestCache: loaded \(entry.manifest.entries.count) entries from disk")
|
||||
log.info("ManifestCache: loaded \(entry.manifest.entries.count) entries from disk, dirCount=\(entry.directoryCount.map(String.init) ?? "nil")")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the cached manifest without any I/O. Never nil after first reconcile.
|
||||
var manifest: BackupManifest? { cached?.manifest }
|
||||
|
||||
/// Real NAS folder file count from the last listDirectory scan.
|
||||
/// nil until the first full reconcile completes.
|
||||
var directoryCount: Int? { cached?.directoryCount }
|
||||
|
||||
/// True when the cache is absent or older than the stale threshold.
|
||||
var isStale: Bool {
|
||||
guard let c = cached else { return true }
|
||||
return Date().timeIntervalSince(c.savedAt) > NASManifestCache.staleInterval
|
||||
}
|
||||
|
||||
/// Persist a freshly-downloaded manifest. Call after every successful NAS download.
|
||||
/// Persist a freshly-downloaded manifest. Preserves any existing directoryCount.
|
||||
func update(_ manifest: BackupManifest) {
|
||||
let entry = CachedEntry(manifest: manifest, savedAt: Date())
|
||||
cached = entry
|
||||
cached = CachedEntry(manifest: manifest, savedAt: Date(), directoryCount: cached?.directoryCount)
|
||||
log.info("ManifestCache: updated — \(manifest.entries.count) entries")
|
||||
saveToDisk()
|
||||
}
|
||||
|
||||
/// Store the real NAS destination folder file count (from listDirectory scan).
|
||||
func setDirectoryCount(_ count: Int) {
|
||||
if var entry = cached {
|
||||
entry.directoryCount = count
|
||||
cached = entry
|
||||
} else {
|
||||
cached = CachedEntry(manifest: BackupManifest(), savedAt: .distantPast, directoryCount: count)
|
||||
}
|
||||
log.info("ManifestCache: directoryCount=\(count)")
|
||||
saveToDisk()
|
||||
}
|
||||
|
||||
/// Increment the stored directory count after a successful upload batch.
|
||||
func addToDirectoryCount(_ delta: Int) {
|
||||
guard delta > 0 else { return }
|
||||
let next = (cached?.directoryCount ?? 0) + delta
|
||||
if var entry = cached {
|
||||
entry.directoryCount = next
|
||||
cached = entry
|
||||
} else {
|
||||
cached = CachedEntry(manifest: BackupManifest(), savedAt: .distantPast, directoryCount: next)
|
||||
}
|
||||
log.info("ManifestCache: directoryCount += \(delta) → \(next)")
|
||||
saveToDisk()
|
||||
}
|
||||
|
||||
private func saveToDisk() {
|
||||
guard let entry = cached else { return }
|
||||
Task.detached(priority: .utility) { [entry, url = diskURL] in
|
||||
guard let data = try? JSONEncoder.kisani.encode(entry) else { return }
|
||||
try? data.write(to: url, options: .atomic)
|
||||
|
||||
Reference in New Issue
Block a user