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 <noreply@anthropic.com>
This commit is contained in:
Nathaniel Ledford
2026-10-09 18:28:27 -04:00
co-authored by Claude Opus 5.5
parent 898ecee27c
commit 97e2c7ff51
2 changed files with 29 additions and 1 deletions
+14 -1
View File
@@ -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<Photo>, b: &Arc<Photo>) -> 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<id>`.
fn base_hash(h: &str) -> &str {
h.split(':').next().unwrap_or(h)
@@ -388,7 +401,7 @@ impl Session {
let mut then: Vec<PhotoId> = 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)
}
}
+15
View File
@@ -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}");
}