From abb3f0315479dc383b99900ce92b41ae7db33e55 Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Tue, 27 Oct 2026 13:40:50 -0400 Subject: [PATCH] LR-IMP-ADD-DIALOG: an import's probe opens each file once The probe asked for a file's size, read it (an open and its own size question), and then asked for its modification time again by path: three round trips to a network share's server for what one open can answer. It now opens the file once and takes the size and the modification time from the open file. What is read, the work-gate weight, and the text of the error for a file that can't be read are unchanged. --- crates/engine/src/files/fs.rs | 18 ++++++++++++------ crates/engine/src/files/tests.rs | 15 +++++++++++++++ docs/whats-new.md | 9 +++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/files/fs.rs b/crates/engine/src/files/fs.rs index dd1f5273c..562f0b60c 100644 --- a/crates/engine/src/files/fs.rs +++ b/crates/engine/src/files/fs.rs @@ -76,11 +76,17 @@ pub fn fs_pair_loader() -> PairLoader { pub fn fs_hooks() -> (FileLoader, FileProbe) { let loader: FileLoader = Arc::new(|path: &str, max_edge: usize| fs_load(path, max_edge, |bytes| load_vec(bytes, max_edge))); let probe: FileProbe = Arc::new(|path: &str| { - let len = std::fs::metadata(path).map(|m| usize::try_from(m.len()).unwrap_or(usize::MAX)).unwrap_or(0); + use std::io::Read; + // one open, then everything from the open file: its size and modification time are not + // round trips of their own (on a network share each of them is one) + let mut file = std::fs::File::open(path).map_err(|e| format!("{path}: {e}"))?; + let meta = file.metadata().ok(); + let len = meta.as_ref().map_or(0, |m| usize::try_from(m.len()).unwrap_or(usize::MAX)); let _permit = crate::memory::work_gate().acquire(len); - let bytes = std::fs::read(path).map_err(|e| format!("{path}: {e}"))?; + let mut bytes = Vec::new(); + file.read_to_end(&mut bytes).map_err(|e| format!("{path}: {e}"))?; let mut info = probe_bytes(path, &bytes)?; - info.file_modified = file_modified(path); + info.file_modified = meta.as_ref().and_then(modified_iso); Ok(info) }); (loader, probe) @@ -104,10 +110,10 @@ fn fs_load(path: &str, max_edge: usize, load: impl FnOnce(Vec) -> Result< r } -/// `path`'s modification time as ISO 8601 (see [`ProbeInfo::file_modified`]); `None` when the +/// The modification time in `meta` as ISO 8601 (see [`ProbeInfo::file_modified`]); `None` when the /// filesystem doesn't say or the time is before 1970. -fn file_modified(path: &str) -> Option { - let t = std::fs::metadata(path).ok()?.modified().ok()?; +fn modified_iso(meta: &std::fs::Metadata) -> Option { + let t = meta.modified().ok()?; let secs = i64::try_from(t.duration_since(std::time::UNIX_EPOCH).ok()?.as_secs()).ok()?; Some(crate::import::civil(secs)) } diff --git a/crates/engine/src/files/tests.rs b/crates/engine/src/files/tests.rs index e8bcebe4c..613820257 100644 --- a/crates/engine/src/files/tests.rs +++ b/crates/engine/src/files/tests.rs @@ -427,6 +427,21 @@ fn undated_files_take_the_file_date() { let _ = std::fs::remove_dir_all(&dir); } +/// The probe opens the file once and says the same thing as before about one that can't be read: +/// its path, then the system's words (a missing file, a folder). +#[test] +fn a_probe_of_a_file_that_cannot_be_read_names_the_path_and_the_cause() { + let dir = std::env::temp_dir().join(format!("lightcraft-probe-errors-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let (_, probe) = fs_hooks(); + for path in [dir.join("no-such-file.jpg"), dir.clone()] { + let path = path.to_string_lossy().to_string(); + let e = probe(&path).unwrap_err(); + assert_eq!(e, format!("{path}: {}", std::fs::read(&path).unwrap_err())); + } + let _ = std::fs::remove_dir_all(&dir); +} + /// Issue #367: probes read headers, not pixels, and report what the decode-based probe did: /// oriented dimensions (EXIF orientation 6 swaps them), metadata, the error for a truncated /// file or one that isn't an image. diff --git a/docs/whats-new.md b/docs/whats-new.md index c6c259200..7c0c42809 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -1,5 +1,14 @@ # What's new in LightCraft +## 2026-10-27 + +### Reading a photo from a network share asks the server less +- Checking a photo (before an import, and for Reload, Recheck, Relink and Convert) asked for the file's size, then + opened it, then asked for its modification time as a question of its own. It now opens the file once and reads + both from the open file, so each photo costs the server fewer round trips (about a quarter less time to read a + set of photos from an SMB share, in one measurement on one share). What is imported and the messages for a file + that can't be read are the same. + ## 2026-10-24 ### Relative-date filters and smart albums follow the clock