diff --git a/crates/engine/src/files/fs.rs b/crates/engine/src/files/fs.rs index 7012211af6..549c83b19c 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 e8bcebe4c2..6138202574 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 1aaf93b0ea..08a169d108 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -5,6 +5,13 @@ ### The Edit menu groups Select Only Active Photo and Paste Settings from Previous with their kin - Select Only Active Photo now sits under Deselect All, and Paste Settings from Previous under Paste Selected Settings…, instead of both after Settings…, near the end of the Edit menu. On macOS, where Settings… lives in the application menu, the Edit menu no longer shows two separator lines in a row. +### 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-25 ### `>=` and `<=` work in the search box