Fix Already Safe = 0: hybrid ManifestIndex preserves localIdentifiers + NAS filenames

The root cause: buildManifestIndex was discarding manifest localIdentifiers and
switching to pure filename-only matching whenever dirCount > manifest.totalCount
(which is always true when the NAS has files from other tools). If filenames didn't
match exactly, Already Safe dropped to 0 on every stale-cache reconcile.

Fix: introduce ManifestIndex(manifest:nasFilenames:totalCount:) — a hybrid that keeps
Kisani's localIdentifiers as primary keys AND adds all NAS directory filenames for
filename-fallback. Used in:
- buildManifestIndex (full reconcile)
- fast path (when dirCount > cached.entries.count, previously gated at <= 1)
- BackupEngine step 3b (so "Back up again" only uploads genuinely missing files)

Also simplify countSafe / pendingIDs / step-4 filter to always try filename fallback
(removes the isFilenameOnly gate that blocked matches when the index had IDs).

Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
This commit is contained in:
Robin Kutesa
2026-05-19 00:32:03 +03:00
parent d5b1f13434
commit 6ae567d008
4 changed files with 49 additions and 33 deletions

View File

@@ -85,6 +85,29 @@ struct ManifestIndex: Sendable {
self.totalCount = names.count
}
// Hybrid: manifest localIdentifiers (Kisani primary key) + all NAS filenames
// for filename-fallback matching when the NAS directory has more files than the
// manifest tracks (uploaded by other tools). totalCount = actual NAS file count.
init(manifest: BackupManifest, nasFilenames: [String], totalCount: Int) {
var ids = Set<String>(minimumCapacity: manifest.entries.count)
var names = Set<String>(minimumCapacity: manifest.entries.count + nasFilenames.count)
var unclaimed = Set<String>()
for entry in manifest.entries {
let lower = entry.filename.lowercased()
names.insert(lower)
if !entry.localIdentifier.isEmpty {
ids.insert(entry.localIdentifier)
} else {
unclaimed.insert(lower)
}
}
for name in nasFilenames { names.insert(name.lowercased()) }
self.localIdentifiers = ids
self.lowercasedFilenames = names
self.unclaimedFilenames = unclaimed
self.totalCount = totalCount
}
// Build from NAS directory listing (bootstrap no localIdentifiers known)
init(nasListing items: [NASItem]) {
var names = Set<String>()