diff --git a/crates/engine/src/cmd/previews.rs b/crates/engine/src/cmd/previews.rs index a1715d79..ea2ccb00 100644 --- a/crates/engine/src/cmd/previews.rs +++ b/crates/engine/src/cmd/previews.rs @@ -194,10 +194,7 @@ fn smart_run( let _one = SMART_WRITE.lock().unwrap_or_else(std::sync::PoisonError::into_inner); // Read either the new shard or a pre-upgrade flat file, without making new work // depend on having the original online. - let path = if path.exists() { path } else { - let legacy = path.file_name().map(|name| dir.join(name)); - legacy.filter(|old| old.exists()).unwrap_or(path) - }; + let path = crate::smart::existing_file_path(dir, path); if discard { if std::fs::remove_file(&path).is_ok() { n.removed += 1; @@ -329,7 +326,7 @@ pub(crate) fn refresh_stale_smart_previews(s: &mut Session) -> Option = s.catalog.photos().filter(|p| matches!(p.source, lightcraft_catalog::Source::File { .. }) && p.relative_wb()).collect(); let jobs: Vec = - photos.iter().map(|p| (p.id, crate::smart::file_path(&dir, p), Some(s.media.origin_ref(&p.source, max_edge)), true)).collect(); + photos.iter().map(|p| (p.id, crate::smart::sharded_file_path(&dir, p), Some(s.media.origin_ref(&p.source, max_edge)), true)).collect(); std::thread::Builder::new() .name("lc-smart-look".into()) .spawn(move || refresh_stale(&dir, jobs)) @@ -340,7 +337,11 @@ pub(crate) fn refresh_stale_smart_previews(s: &mut Session) -> Option) { - let stale: Vec = jobs.into_iter().filter(|(_, path, _, _)| crate::smart::is_stale(path)).collect(); + let stale: Vec = jobs + .into_iter() + .map(|(id, path, source, look)| (id, crate::smart::existing_file_path(dir, path), source, look)) + .filter(|(_, path, _, _)| crate::smart::is_stale(path)) + .collect(); if !stale.is_empty() { // `custom`: the folder is there already, and must not be recreated if it went away since match smart_run(dir, true, false, stale, None) { diff --git a/crates/engine/src/smart.rs b/crates/engine/src/smart.rs index cd003be4..532454a1 100644 --- a/crates/engine/src/smart.rs +++ b/crates/engine/src/smart.rs @@ -31,9 +31,18 @@ pub fn sharded_file_path(dir: &Path, p: &Photo) -> PathBuf { /// Read an existing proxy in either layout. New proxies use the sharded location; previews /// made by older releases still load and can be moved with smartPreviewsLocation. pub fn file_path(dir: &Path, p: &Photo) -> PathBuf { - let sharded = sharded_file_path(dir, p); - let legacy = dir.join(file_name(p)); - if sharded.exists() || !legacy.exists() { sharded } else { legacy } + existing_file_path(dir, sharded_file_path(dir, p)) +} + +/// Resolve a sharded path to an older flat file when present. Call only off the UI thread: +/// checking a removable drive synchronously could stall drawing. +pub fn existing_file_path(dir: &Path, sharded: PathBuf) -> PathBuf { + if sharded.exists() { + return sharded; + } + let Some(name) = sharded.file_name() else { return sharded }; + let legacy = dir.join(name); + if legacy.exists() { legacy } else { sharded } } /// The most header [`is_valid`] reads (the JSON line before the JPEG). @@ -189,7 +198,7 @@ pub fn migrate(from: &Path, to: &Path, what: Existing) -> (usize, Vec) { return (done, failed); } for src in files(from) { - let name = src.file_name().unwrap_or_default(); + let Some(name) = src.file_name() else { continue }; let dst = name.to_str().and_then(|n| n.strip_suffix(".lcsp")).filter(|stem| { stem.len() == 32 && stem.bytes().all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()) }).map_or_else(|| to.join(name), |stem| to.join(&stem[..2]).join(name)); diff --git a/crates/engine/src/tests_import.rs b/crates/engine/src/tests_import.rs index 193fdce8..7d257ffc 100644 --- a/crates/engine/src/tests_import.rs +++ b/crates/engine/src/tests_import.rs @@ -795,6 +795,13 @@ fn smart_previews_folder_is_chosen_per_library() { assert_eq!(loc["path"], lib.join("Smart Previews").to_string_lossy().as_ref()); s.execute("library.smartPreviews", &json!({})).unwrap(); assert_eq!(crate::smart::stats(&lib.join("Smart Previews")).0, 1); + // A flat proxy from older builds remains discoverable and migrates to the new sharded layout. + let dir = lib.join("Smart Previews"); + let photo = s.catalog.photo(id).unwrap(); + let shard = crate::smart::sharded_file_path(&dir, photo); + let flat = dir.join(crate::smart::file_name(photo)); + std::fs::rename(&shard, &flat).unwrap(); + assert_eq!(s.execute("photo.smartPreview", &json!({})).unwrap()["smartPreview"], true); // relative paths and missing drives are refused, and nothing is created on this drive assert!(s.execute("library.smartPreviewsLocation", &json!({"path": "rel/dir"})).is_err());