Merge PR #2041: fix(camera-profiles): invalidate cached renders after subsecond profile changes

This commit is contained in:
aiongg
2026-10-22 02:42:06 +00:00
+57 -23
View File
@@ -159,29 +159,31 @@ pub fn get(model: &str) -> Option<Arc<CameraProfile>> {
/// keys, so thumbnails rendered before a profile existed are not reused after.
pub fn cache_key() -> u64 {
static KEY: OnceLock<u64> = OnceLock::new();
*KEY.get_or_init(|| {
let entries = dir().and_then(|d| std::fs::read_dir(d).ok());
let mut files: Vec<(String, u64, u64)> = entries
.into_iter()
.flatten()
.flatten()
.filter(|e| e.path().extension().is_some_and(|x| x == "json"))
.filter_map(|e| {
let meta = e.metadata().ok()?;
let modified = meta.modified().ok()?.duration_since(std::time::UNIX_EPOCH).ok()?.as_secs();
Some((e.file_name().to_string_lossy().into_owned(), meta.len(), modified))
})
.collect();
files.sort();
let mut h = lightcraft_preview::Hasher128::new();
for (name, len, modified) in &files {
h.str(name).u64(*len).u64(*modified);
}
for (model, json) in BUNDLED {
h.str(model).str(json);
}
h.finish().0 as u64
})
*KEY.get_or_init(|| cache_key_for(dir().as_deref()))
}
fn cache_key_for(directory: Option<&Path>) -> u64 {
let entries = directory.and_then(|d| std::fs::read_dir(d).ok());
let mut files: Vec<(String, u64, std::time::Duration)> = entries
.into_iter()
.flatten()
.flatten()
.filter(|e| e.path().extension().is_some_and(|x| x == "json"))
.filter_map(|e| {
let meta = e.metadata().ok()?;
let modified = meta.modified().ok()?.duration_since(std::time::UNIX_EPOCH).ok()?;
Some((e.file_name().to_string_lossy().into_owned(), meta.len(), modified))
})
.collect();
files.sort();
let mut h = lightcraft_preview::Hasher128::new();
for (name, len, modified) in &files {
h.str(name).u64(*len).u64(modified.as_secs()).u64(u64::from(modified.subsec_nanos()));
}
for (model, json) in BUNDLED {
h.str(model).str(json);
}
h.finish().0 as u64
}
/// Per camera model: the photos read and their pooled colour pairs.
@@ -294,6 +296,38 @@ mod tests {
assert!(bundled("No Such Camera").is_none());
}
#[test]
fn same_second_profile_replacement_changes_render_cache_key() {
let dir = std::env::temp_dir().join(format!("lc-camera-profile-cache-{}", std::process::id()));
let mut first = profile();
first.model = "ILCE-7CR".into();
let path = save(&first, &dir).unwrap();
let set_modified = |millis| {
let time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_700_000_000) + std::time::Duration::from_millis(millis);
let file = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
file.set_times(std::fs::FileTimes::new().set_modified(time)).unwrap();
file.metadata().unwrap()
};
let before = set_modified(100);
assert_eq!(load(&path).unwrap(), first);
let before_key = cache_key_for(Some(&dir));
let mut second = first;
second.matrix[0][0] = 1.7;
save(&second, &dir).unwrap();
let after = set_modified(350);
assert_eq!(load(&path).unwrap(), second);
assert_eq!(before.len(), after.len());
let before_time = before.modified().unwrap().duration_since(std::time::UNIX_EPOCH).unwrap();
let after_time = after.modified().unwrap().duration_since(std::time::UNIX_EPOCH).unwrap();
assert_eq!(before_time.as_secs(), after_time.as_secs());
assert_ne!(before_time.subsec_nanos(), after_time.subsec_nanos());
let after_key = cache_key_for(Some(&dir));
let _ = std::fs::remove_dir_all(dir);
assert_ne!(before_key, after_key, "a changed profile must not reuse the previous render cache");
}
#[test]
fn file_names_are_safe() {
assert_eq!(file_name("ILCE-7M4"), "ILCE-7M4.json");