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:
@@ -135,17 +135,19 @@ final class BackupEngine: ObservableObject {
|
||||
// uploaded by other tools), a manifest-only index will misidentify those files
|
||||
// as "not backed up" and re-upload them on every "Back up again" tap.
|
||||
// Use the broader NAS directory listing as the filter when the manifest is sparse.
|
||||
// Build a hybrid filter index: manifest localIdentifiers (primary) + all NAS
|
||||
// filenames (fallback). This ensures files uploaded by other tools are recognised
|
||||
// as "already on NAS" by filename and are not re-uploaded on every backup run.
|
||||
let filterIndex: ManifestIndex
|
||||
let cachedDirCount = await NASManifestCache.shared.directoryCount ?? 0
|
||||
if baseIndex.totalCount < cachedDirCount {
|
||||
if let cachedFilenames = await NASManifestCache.shared.directoryFilenames,
|
||||
cachedFilenames.count > baseIndex.totalCount {
|
||||
filterIndex = await Task.detached(priority: .utility) {
|
||||
ManifestIndex(nasFilenames: cachedFilenames)
|
||||
ManifestIndex(manifest: baseManifest, nasFilenames: cachedFilenames, totalCount: cachedDirCount)
|
||||
}.value
|
||||
logger.info("Filter index: \(cachedFilenames.count) cached NAS filenames (manifest=\(baseIndex.totalCount))")
|
||||
logger.info("Filter index: hybrid — \(baseIndex.totalCount) manifest + \(cachedFilenames.count) cached NAS filenames")
|
||||
} else {
|
||||
// No cached filenames — do a fresh listing (connection is already open)
|
||||
let nasItems = (try? await transfer.listDirectory(at: connection.remotePath)) ?? []
|
||||
let names = nasItems.compactMap { item -> String? in
|
||||
guard !item.isDirectory, !item.name.hasPrefix("."),
|
||||
@@ -154,9 +156,9 @@ final class BackupEngine: ObservableObject {
|
||||
}
|
||||
await NASManifestCache.shared.setDirectoryFilenames(names)
|
||||
filterIndex = await Task.detached(priority: .utility) {
|
||||
ManifestIndex(nasFilenames: names)
|
||||
ManifestIndex(manifest: baseManifest, nasFilenames: names, totalCount: names.count)
|
||||
}.value
|
||||
logger.info("Filter index: \(names.count) fresh NAS filenames (manifest=\(baseIndex.totalCount))")
|
||||
logger.info("Filter index: hybrid — \(baseIndex.totalCount) manifest + \(names.count) fresh NAS filenames")
|
||||
}
|
||||
} else {
|
||||
filterIndex = baseIndex
|
||||
@@ -169,8 +171,7 @@ final class BackupEngine: ObservableObject {
|
||||
let pendingAssets: [PhotoAsset] = await Task.detached(priority: .userInitiated) {
|
||||
assets.filter { asset in
|
||||
!filterIndex.matches(localIdentifier: asset.localIdentifier) &&
|
||||
!(filterIndex.isFilenameOnly && filterIndex.matches(filename: asset.filename)) &&
|
||||
!filterIndex.matchesUnclaimed(filename: asset.filename)
|
||||
!filterIndex.matches(filename: asset.filename)
|
||||
}
|
||||
}.value
|
||||
signposter.endInterval("BuildPendingQueue", spFilter, "\(pendingAssets.count) pending")
|
||||
|
||||
Reference in New Issue
Block a user