From 97e2c7ff518e544e8b6bb7df675f00b4555e580d Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Fri, 9 Oct 2026 18:28:27 -0400 Subject: [PATCH] Synchronize Folder: background face search doesn't make a scan stale (review fix) A scan was current only while the folder's photo records were the very same; the face search adding regions in the background replaced them, so confirming could keep failing as stale while it worked through the folder. A photo now counts as changed only when something synchronizing depends on changed: its file, its place in the library, or what a sidecar is compared with (regions aside). Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/sync.rs | 15 ++++++++++++++- crates/engine/src/tests_sync.rs | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/sync.rs b/crates/engine/src/sync.rs index 5e74fb220..b07566caf 100644 --- a/crates/engine/src/sync.rs +++ b/crates/engine/src/sync.rs @@ -238,6 +238,19 @@ pub fn scan_with(input: SyncInput, progress: &SyncProgress) -> FolderChanges { changes } +/// Whether a photo is the same as far as synchronizing goes: its file, its place in the library +/// and what a sidecar is compared with. Face and pet regions found in the background meanwhile +/// don't make a scan stale. +fn same_for_sync(a: &Arc, b: &Arc) -> bool { + if Arc::ptr_eq(a, b) { + return true; + } + let meta = |p: &Photo| lightcraft_catalog::Meta { regions: Vec::new(), ..p.meta.clone() }; + (&a.source, a.deleted, a.local, a.copy_of, &a.content_hash, a.file_size, a.rating, a.flag, a.label, &a.captured, &a.edited, &a.develop) + == (&b.source, b.deleted, b.local, b.copy_of, &b.content_hash, b.file_size, b.rating, b.flag, b.label, &b.captured, &b.edited, &b.develop) + && meta(a) == meta(b) +} + /// The content a hash stands for: a Duplicate's own hash is the original's plus `:dup`. fn base_hash(h: &str) -> &str { h.split(':').next().unwrap_or(h) @@ -388,7 +401,7 @@ impl Session { let mut then: Vec = c.seen.iter().map(|p| p.id).collect(); now.sort(); then.sort(); - let unchanged = now == then && c.seen.iter().all(|p| self.catalog.photo(p.id).is_some_and(|q| Arc::ptr_eq(p, q))); + let unchanged = now == then && c.seen.iter().all(|p| self.catalog.photo(p.id).is_some_and(|q| same_for_sync(p, q))); unchanged.then_some(c) } } diff --git a/crates/engine/src/tests_sync.rs b/crates/engine/src/tests_sync.rs index 616745120..724487ff2 100644 --- a/crates/engine/src/tests_sync.rs +++ b/crates/engine/src/tests_sync.rs @@ -451,3 +451,18 @@ fn renamed_duplicates_are_each_relinked() { let r = s.execute("folder.synchronize", &json!({"path": dir.path("trip")})).unwrap(); assert_eq!(r["relinked"], 2, "{r}"); } + +#[test] +fn face_regions_found_meanwhile_leave_the_scan_current() { + let dir = Scratch::new("faces"); + let mut s = library(&dir); + write_png(&dir.path("trip/c.png"), 3); + scan(&mut s, &dir.path("trip")); + // background face search adds a region to a photo of the folder + let a = s.catalog.photos().find(|p| p.file_name == "a.png").unwrap().id; + let mut meta = s.catalog.photo(a).unwrap().meta.clone(); + meta.regions.push(lightcraft_meta::Region { rect: Default::default(), kind: lightcraft_meta::RegionKind::Face, name: None, description: None }); + s.catalog.apply(lightcraft_catalog::Op::SetMeta { id: a, meta: Box::new(meta) }).unwrap(); + let r = s.execute("folder.synchronize", &json!({"path": dir.path("trip"), "scanned": true})).unwrap(); + assert_eq!(r["imported"], 1, "{r}"); +}