diff --git a/Services/BackupEngine.swift b/Services/BackupEngine.swift index 5902b00..26900b0 100644 --- a/Services/BackupEngine.swift +++ b/Services/BackupEngine.swift @@ -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")