fix: pending queue uses NAS directory filenames when manifest is sparse
BackupEngine was building pendingAssets from ManifestIndex(manifest:) which only knows about files Kisani previously uploaded. When most NAS files were uploaded by other tools, nearly all phone photos passed the pending filter and "Back up again" would queue and attempt to re-upload everything. Fix: step 3b builds a filterIndex from the cached NAS directory filenames (or a fresh listDirectory scan when cache is unavailable) when the directory has more files than the manifest. The filename-based filterIndex correctly identifies only photos whose filenames are absent from the NAS, so "Back up again" queues exactly the delta — not a full library re-upload. The baseManifest/baseIndex are unchanged and still drive the manifest write. Co-Authored-By: Kutesir <kutesir@provoc.ug> Co-Authored-By: Sentry <sentry@provoc.ug>
This commit is contained in:
@@ -130,15 +130,47 @@ final class BackupEngine: ObservableObject {
|
||||
signposter.endInterval("ManifestLoad", spManifest, "\(baseIndex.totalCount) entries")
|
||||
logger.info("Manifest base: \(baseIndex.totalCount) entries, isFilenameOnly=\(baseIndex.isFilenameOnly)")
|
||||
|
||||
// ── 3b. Build filter index ─────────────────────────────────────────
|
||||
// When the NAS directory has more files than Kisani's manifest (e.g. files
|
||||
// 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.
|
||||
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)
|
||||
}.value
|
||||
logger.info("Filter index: \(cachedFilenames.count) cached NAS filenames (manifest=\(baseIndex.totalCount))")
|
||||
} 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("."),
|
||||
item.name != BackupManifest.remoteFilename else { return nil }
|
||||
return item.name
|
||||
}
|
||||
await NASManifestCache.shared.setDirectoryFilenames(names)
|
||||
filterIndex = await Task.detached(priority: .utility) {
|
||||
ManifestIndex(nasFilenames: names)
|
||||
}.value
|
||||
logger.info("Filter index: \(names.count) fresh NAS filenames (manifest=\(baseIndex.totalCount))")
|
||||
}
|
||||
} else {
|
||||
filterIndex = baseIndex
|
||||
}
|
||||
|
||||
try Task.checkCancellation()
|
||||
|
||||
// ── 4. Build pending queue off the main actor ──────────────────────
|
||||
let spFilter = signposter.beginInterval("BuildPendingQueue")
|
||||
let pendingAssets: [PhotoAsset] = await Task.detached(priority: .userInitiated) {
|
||||
assets.filter { asset in
|
||||
!baseIndex.matches(localIdentifier: asset.localIdentifier) &&
|
||||
!(baseIndex.isFilenameOnly && baseIndex.matches(filename: asset.filename)) &&
|
||||
!baseIndex.matchesUnclaimed(filename: asset.filename)
|
||||
!filterIndex.matches(localIdentifier: asset.localIdentifier) &&
|
||||
!(filterIndex.isFilenameOnly && filterIndex.matches(filename: asset.filename)) &&
|
||||
!filterIndex.matchesUnclaimed(filename: asset.filename)
|
||||
}
|
||||
}.value
|
||||
signposter.endInterval("BuildPendingQueue", spFilter, "\(pendingAssets.count) pending")
|
||||
|
||||
Reference in New Issue
Block a user