Merge PR #3410: Issue #3406: Auto Import says when it can't read the watched folder

This commit is contained in:
aiongg
2026-10-31 02:09:28 +00:00
4 changed files with 68 additions and 4 deletions
+29 -4
View File
@@ -81,6 +81,10 @@ impl LightcraftApp {
focus_scan::pump(self, ctx, now);
// auto import: list the watched folder every few seconds (on a worker thread: it may be on
// a network share), then import what's new like any import (also on a worker thread)
// a folder that couldn't be read stops being one once another is watched (or none)
if self.ui.auto_import_error.as_ref().is_some_and(|(f, _)| self.session.import_defaults.auto_folder.as_ref() != Some(f)) {
self.ui.auto_import_error = None;
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(folder) = self.session.import_defaults.auto_folder.clone() {
const LABEL: &str = "Auto Import";
@@ -91,11 +95,32 @@ impl LightcraftApp {
&& !self.tasks.is_running(LABEL)
{
self.ui.auto_import_at = now;
let work = move || lightcraft_engine::cmd::library::list_auto_import_folder(&folder);
let done = |app: &mut LightcraftApp, _ctx: &egui::Context, listing: Result<Vec<(String, u64)>, String>| {
let listed = folder.clone();
let work = move || lightcraft_engine::cmd::library::list_auto_import_folder(&listed);
let done = move |app: &mut LightcraftApp, _ctx: &egui::Context, listing: Result<Vec<(String, u64)>, String>| {
// the watched folder changed while this one was listed: its result is for nothing on screen
if app.session.import_defaults.auto_folder.as_ref() != Some(&folder) {
return;
}
let listing = match listing {
Ok(l) => l,
Err(e) => return log::debug!("auto import: {e}"),
Ok(l) => {
if app.ui.auto_import_error.as_ref().is_some_and(|(f, _)| *f == folder) {
app.ui.auto_import_error = None;
log::info!("auto import: the watched folder can be read again: {folder}");
}
l
}
// removed, renamed, unmounted or not allowed: said once, not every few seconds
Err(e) => {
let error = Some((folder, e));
if app.ui.auto_import_error != error {
if let Some((_, e)) = &error {
log::warn!("auto import: can't read the watched folder {e}");
}
app.ui.auto_import_error = error;
}
return;
}
};
let p = serde_json::json!({"listing": listing, "start": false});
let Ok(r) = app.session.execute("library.autoImportScan", &p) else { return };
@@ -163,6 +163,15 @@ pub(super) fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens)
let _ = app.run("library.autoImport", json!({"folder": null}));
}
});
// the last listing of the watched folder failed: say why (it reads "OS error: reason")
if let (Some(folder), Some((failed, e))) = (&d.auto_folder, &app.ui.auto_import_error)
&& folder == failed
{
let reason = e.strip_prefix(folder.as_str()).and_then(|r| r.strip_prefix(": ")).unwrap_or(e);
row(ui, t, "", |ui| {
ui.label(RichText::new(crate::i18n::tr_format!("Can't read this folder: {}", reason)).size(11.0).color(t.caution));
});
}
if d.auto_folder.is_some() {
row(ui, t, "", |ui| {
let mut copy = d.auto_copy;
+5
View File
@@ -166,6 +166,10 @@ pub struct UiState {
/// scanning).
#[serde(skip)]
pub auto_import_at: f64,
/// The watched folder that couldn't be listed and why, as the last listing found it: logged once when it
/// starts and once when the folder can be read again, and shown in Settings ▸ Import.
#[serde(skip)]
pub auto_import_error: Option<(String, String)>,
/// When the automatic XMP writes that failed were last tried again (monotonic app time), and how
/// many were waiting then.
#[serde(skip)]
@@ -424,6 +428,7 @@ impl Default for UiState {
external_edits: Vec::new(),
was_focused: true,
auto_import_at: 0.0,
auto_import_error: None,
sidecar_retry_at: 0.0,
sidecars_waiting: 0,
known_cards: None,
+25
View File
@@ -1107,3 +1107,28 @@ fn public_drop_take_preserves_reentered_barrier_order_and_clears_protection() {
assert_eq!(photos(&h), 0, "neither taken handles nor refused arrivals replay");
}
}
/// A watched folder that can't be listed (removed, unmounted, not allowed) is noted with the reason, for
/// Settings ▸ Import to show, and the note goes once the folder can be read again.
#[test]
fn auto_import_notes_a_watched_folder_it_cannot_read() {
let dir = Scratch::new("auto-unreadable");
let mut h = app(&dir);
let watch = dir.path("watch");
h.app.session.import_defaults.auto_folder = Some(watch.clone());
assert!(h.step_until(T, |h| h.app.ui.auto_import_error.is_some()), "the missing folder is noted");
let (folder, e) = h.app.ui.auto_import_error.clone().unwrap();
assert_eq!(folder, watch);
assert!(e.starts_with(&watch) && e.len() > watch.len() + 2, "the error says why: {e}");
// another folder watched: the note was about the old one, so it goes, and a listing of the old one
// still under way doesn't bring it back
h.app.session.import_defaults.auto_folder = Some(dir.path("trip"));
h.step();
assert!(h.app.ui.auto_import_error.is_none(), "{:?}", h.app.ui.auto_import_error);
h.app.session.import_defaults.auto_folder = Some(watch.clone());
assert!(h.step_until(T, |h| h.app.ui.auto_import_error.is_some()), "back to the missing folder: noted again");
write_png(&dir.path("watch/w.png"), 3);
assert!(h.step_until(T, |h| h.app.ui.auto_import_error.is_none()), "readable again: the note goes");
assert!(h.step_until(T, |h| photos(h) == 1), "and the photo arrives");
h.app.session.import_defaults.auto_folder = None;
}