From 5a21fc9d9733a58de997053fd99825f44ea69063 Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Fri, 9 Oct 2026 18:38:52 -0400 Subject: [PATCH] Import: a file gone or changed since the review is probed again The import reused the review's probes for every file it had seen, even one deleted (or rewritten) since: such a file was added as a photo of a file that isn't there. A cached probe is now used only while the file is there with the same size (checked in prepare_files, on the import's worker thread; the web's files keep their probes). Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/import.rs | 13 +++++++++++-- crates/engine/src/tests_import.rs | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/import.rs b/crates/engine/src/import.rs index 124ac3903..7fc537a46 100644 --- a/crates/engine/src/import.rs +++ b/crates/engine/src/import.rs @@ -850,8 +850,17 @@ impl ImportJob { } } - // files a preceding scan already probed are not read again (a network share is slow to read) - let cached: Vec> = todo.iter().map(|f| self.cache.remove(f)).collect(); + // files a preceding scan already probed are not read again (a network share is slow to read), + // as long as they are still there, as they were: a file gone or changed since is probed again + let cached: Vec> = todo + .iter() + .map(|f| { + // (the web's files are not std::fs files: kept as probed) + self.cache + .remove(f) + .filter(|info| cfg!(target_arch = "wasm32") || std::fs::metadata(f).is_ok_and(|m| m.is_file() && m.len() == info.file_size)) + }) + .collect(); let missing: Vec = todo.iter().zip(&cached).filter(|(_, c)| c.is_none()).map(|(f, _)| f.clone()).collect(); let progress = ScanProgress::default(); let mut fresh = probe_all(self.probe.as_ref(), &missing, &progress).into_iter(); diff --git a/crates/engine/src/tests_import.rs b/crates/engine/src/tests_import.rs index 4a4cad539..2fdaace85 100644 --- a/crates/engine/src/tests_import.rs +++ b/crates/engine/src/tests_import.rs @@ -1105,3 +1105,17 @@ fn folder_import_picks_up_undecodable_raw_containers_as_preview_only() { assert!(s.catalog.photos().all(|p| p.preview_only.is_some() && p.kind == lightcraft_catalog::MediaKind::Raw && (p.width, p.height) == (40, 30))); let _ = std::fs::remove_dir_all(&src); } + +#[test] +fn a_file_gone_since_the_import_review_is_not_imported() { + let src = temp_dir("gone-since-review"); + write_png(&src.join("a.png"), 1); + write_png(&src.join("b.png"), 2); + let mut s = Session::new().with_fs(); + s.execute("library.importPreview", &json!({"paths": [src.to_string_lossy()]})).unwrap(); + std::fs::remove_file(src.join("b.png")).unwrap(); + let r = s.execute("library.import", &json!({"paths": [src.join("a.png").to_string_lossy(), src.join("b.png").to_string_lossy()]})).unwrap(); + assert_eq!((ids(&r, "imported"), ids(&r, "failed")), (1, 1), "{r}"); + assert_eq!(s.catalog.len(), 1, "no photo for a file that isn't there"); + let _ = std::fs::remove_dir_all(&src); +}