From 1259506edd2ffbcab8f963f254abcc7b6a4c7a39 Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Fri, 9 Oct 2026 18:08:36 -0400 Subject: [PATCH] Synchronize Folder: an absent folder is offline, disks only on request (review fix) A folder that isn't there (moved whole, or on a disk that isn't connected) used to scan as "every photo missing", one tick away from moving a whole disk to Recently Deleted. The scan (on its worker, so an asleep share never stalls the UI) now reports it as offline and both commands refuse it; the dialog says so. A whole disk or a folder holding disks needs `disk: true`, as Remove Disk from Library does; the disk rows pass it. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/library.rs | 2 +- crates/engine/src/cmd/sync.rs | 8 +++--- crates/engine/src/sync.rs | 32 +++++++++++++++++++---- crates/engine/src/tests_sync.rs | 39 ++++++++++++++++++++++++++++- crates/ui-egui/locales/de.json | 3 ++- crates/ui-egui/locales/es.json | 3 ++- crates/ui-egui/locales/ja.json | 3 ++- crates/ui-egui/locales/pt-br.json | 3 ++- crates/ui-egui/locales/ru.json | 3 ++- crates/ui-egui/locales/zh-hans.json | 3 ++- crates/ui-egui/locales/zh-hant.json | 3 ++- crates/ui-egui/src/panels/left.rs | 3 ++- crates/ui-egui/src/state.rs | 6 +++++ crates/ui-egui/src/sync.rs | 20 ++++++++++----- 14 files changed, 106 insertions(+), 25 deletions(-) diff --git a/crates/engine/src/cmd/library.rs b/crates/engine/src/cmd/library.rs index 569716596..4732bab7b 100644 --- a/crates/engine/src/cmd/library.rs +++ b/crates/engine/src/cmd/library.rs @@ -1323,7 +1323,7 @@ fn merge_rules(base: &lightcraft_catalog::Filter, patch: &Value, c: &str) -> Res /// Whether `path` lies above a disk: some of its `ids` photos are on a disk (not the startup /// disk) that the folder does not lie in. Such a folder (`/Volumes`, `/mnt`…) is not a folder of /// one disk but a way to reach several. -fn covers_other_disks(s: &Session, path: &str, ids: &[PhotoId]) -> bool { +pub(crate) fn covers_other_disks(s: &Session, path: &str, ids: &[PhotoId]) -> bool { ids.iter() .filter_map(|id| s.catalog.photo(*id)) .filter_map(|p| lightcraft_catalog::folders::volume_of(p)) diff --git a/crates/engine/src/cmd/sync.rs b/crates/engine/src/cmd/sync.rs index ecb252df6..31c0af6b2 100644 --- a/crates/engine/src/cmd/sync.rs +++ b/crates/engine/src/cmd/sync.rs @@ -17,12 +17,12 @@ pub fn specs() -> Vec { "Scan Folder for Changes", [], None, - "{path} — compare a folder of the library (and the folders inside it) with the disk; changes nothing → {path, new: [{path, name, format, …}] files the library doesn't have, duplicates: files whose content it already has, unreadable: [{path, error}], missing: [{id, path}] photos whose file is gone, metadata: [{id, path}] photos whose XMP sidecar was saved after they came in or were last edited here and says something the library doesn't}", + "{path, disk?: bool (a whole disk or share, or a folder holding disks)} — compare a folder of the library (and the folders inside it) with the disk; changes nothing → {path, new: [{path, name, format, …}] files the library doesn't have, duplicates: files whose content it already has, unreadable: [{path, error}], missing: [{id, path}] photos whose file is gone, metadata: [{id, path}] photos whose XMP sidecar was saved after they came in or were last edited here and says something the library doesn't}", always, |s, p| { const C: &str = "folder.scanChanges"; let path = path(p, C)?; - let changes = scan(s, &path).map_err(|e| bad(C, e.to_string()))?; + let changes = scan(s, &path, bool_or(p, "disk", false)).map_err(|e| bad(C, e.to_string()))?; let v = serde_json::to_value(&changes).unwrap_or_default(); s.folder_changes = Some(changes); Ok(v) @@ -33,7 +33,7 @@ pub fn specs() -> Vec { "Synchronize Folder", [], None, - "{path, importNew?: true, removeMissing?: false, readMetadata?: false} — bring a folder of the library up to date with the disk (see folder.scanChanges): import its new files in place, move photos whose file is gone to Recently Deleted, read XMP sidecars saved by other apps (the sidecar wins); acts on the last folder.scanChanges of the folder while nothing changed in the library since (else scans again); one undo step, no file is touched → {imported, removed, read, failed: [[path, error]]}", + "{path, disk?: bool, importNew?: true, removeMissing?: false, readMetadata?: false} — bring a folder of the library up to date with the disk (see folder.scanChanges): import its new files in place, move photos whose file is gone to Recently Deleted, read XMP sidecars saved by other apps (the sidecar wins); acts on the last folder.scanChanges of the folder while nothing changed in the library since (else scans again); one undo step, no file is touched → {imported, removed, read, failed: [[path, error]]}", always, sync ), @@ -51,7 +51,7 @@ fn sync(s: &mut Session, p: &Value) -> Result { }; let changes = match s.take_folder_changes(&path) { Some(c) => c, - None => scan(s, &path).map_err(|e| bad(C, e.to_string()))?, + None => scan(s, &path, bool_or(p, "disk", false)).map_err(|e| bad(C, e.to_string()))?, }; let r = synchronize(s, changes, choice)?; Ok(json!({"imported": r.imported, "removed": r.removed, "read": r.read, "failed": r.failed})) diff --git a/crates/engine/src/sync.rs b/crates/engine/src/sync.rs index 3803abd81..95b4951c9 100644 --- a/crates/engine/src/sync.rs +++ b/crates/engine/src/sync.rs @@ -65,6 +65,9 @@ pub struct FolderChanges { pub missing: Vec, /// Photos whose sidecar has news. pub metadata: Vec, + /// The folder itself is not there (moved or renamed whole, or on a disk that isn't + /// connected): nothing else is reported, rather than every photo as missing. + pub offline: bool, /// The probes of the new files, kept for the import that follows. #[serde(skip)] probes: HashMap, @@ -110,8 +113,10 @@ fn invalid(msg: impl Into) -> EngineError { } impl SyncInput { - /// The scan of folder `path`: a folder the library holds photos in, not the startup disk. - pub fn new(s: &mut Session, path: &str) -> Result { + /// The scan of folder `path`: a folder the library holds photos in, not the startup disk. A + /// whole disk or share, or a folder that holds disks (`/Volumes`, `/mnt`…), only with `disk` + /// (as for Remove Disk from Library: its missing photos can be a whole disk's). + pub fn new(s: &mut Session, path: &str, disk: bool) -> Result { let path = path.trim(); if path.is_empty() { return Err(invalid("missing `path`")); @@ -124,6 +129,9 @@ impl SyncInput { if ids.is_empty() { return Err(invalid(format!("{path}: no photo in the library was imported from it"))); } + if !disk && (lightcraft_catalog::folders::is_disk_root(path) || crate::cmd::library::covers_other_disks(s, path, &ids)) { + return Err(invalid(format!("{path} is a whole disk or holds disks: pass `disk: true` to synchronize it"))); + } let photos = ids.iter().filter_map(|id| s.catalog.photo(*id).map(|p| (Arc::clone(p), s.sidecar_naming(*id)))).collect(); let (scan, _) = ScanInput::new(s, &[path.to_string()]); Ok(SyncInput { folder: path.to_string(), revision: s.catalog.revision, scan, photos, labels: s.catalog.clone() }) @@ -137,6 +145,9 @@ impl SyncInput { /// library's photos, which run on several threads too. Stops early when `progress.cancel` is set. pub fn scan_with(input: SyncInput, progress: &ScanProgress) -> FolderChanges { let SyncInput { folder, revision, scan, photos, labels } = input; + if !cfg!(target_arch = "wasm32") && !Path::new(&folder).is_dir() { + return FolderChanges { path: folder, revision, offline: true, ..Default::default() }; + } // (no threads on the web: one after the other there) #[cfg(target_arch = "wasm32")] let (out, checks) = (crate::import::scan_with(scan, std::slice::from_ref(&folder), progress), check_photos(&photos, &labels, progress)); @@ -224,9 +235,17 @@ fn check_photos(photos: &[(Arc, SidecarNaming)], labels: &Catalog, progre } /// [`scan_with`] on the calling thread. -pub fn scan(s: &mut Session, path: &str) -> Result { - let input = SyncInput::new(s, path)?; - Ok(scan_with(input, &ScanProgress::default())) +pub fn scan(s: &mut Session, path: &str, disk: bool) -> Result { + let input = SyncInput::new(s, path, disk)?; + let changes = scan_with(input, &ScanProgress::default()); + if changes.offline { + return Err(not_there(&changes.path)); + } + Ok(changes) +} + +fn not_there(path: &str) -> EngineError { + invalid(format!("{path} is not there (moved, renamed, or on a disk that isn't connected)")) } impl Session { @@ -265,6 +284,9 @@ fn sidecar_has_news(labels: &Catalog, p: &Photo, naming: SidecarNaming) -> bool /// Act on `changes` as `choice` says, as one undo step ("Synchronize Folder"). A missing photo /// whose file is back by now is left alone. pub fn synchronize(s: &mut Session, changes: FolderChanges, choice: SyncChoice) -> Result { + if changes.offline { + return Err(not_there(&changes.path)); + } let mark = s.undo.len(); let mut report = SyncReport::default(); let FolderChanges { new, missing, metadata, probes, .. } = changes; diff --git a/crates/engine/src/tests_sync.rs b/crates/engine/src/tests_sync.rs index 3cda16b36..58055a70e 100644 --- a/crates/engine/src/tests_sync.rs +++ b/crates/engine/src/tests_sync.rs @@ -243,7 +243,7 @@ fn a_cancelled_scan_stops_checking_files() { let dir = Scratch::new("cancel"); let mut s = library(&dir); std::fs::remove_file(dir.path("trip/day1/b.png")).unwrap(); - let input = crate::sync::SyncInput::new(&mut s, &dir.path("trip")).unwrap(); + let input = crate::sync::SyncInput::new(&mut s, &dir.path("trip"), false).unwrap(); let progress = crate::import::ScanProgress::default(); progress.cancel.store(true, std::sync::atomic::Ordering::Relaxed); let c = crate::sync::scan_with(input, &progress); @@ -270,3 +270,40 @@ fn many_photos_are_checked_in_parallel_with_the_same_answer() { assert_eq!(paths(&r, "missing"), expect_missing); assert_eq!(paths(&r, "metadata"), expect_meta); } + +#[test] +fn a_folder_that_is_not_there_is_not_scanned_as_if_everything_went_missing() { + let dir = Scratch::new("offline"); + let mut s = library(&dir); + // an unplugged disk, or a folder moved away whole + std::fs::rename(dir.0.join("trip"), dir.0.join("trip-elsewhere")).unwrap(); + for cmd in ["folder.scanChanges", "folder.synchronize"] { + let e = s.execute(cmd, &json!({"path": dir.path("trip"), "removeMissing": true})).unwrap_err().to_string(); + assert!(e.contains("not there"), "{cmd}: {e}"); + } + assert_eq!(s.catalog.photos().filter(|p| p.in_library()).count(), 2, "nothing was removed"); +} + +#[test] +fn a_whole_disk_or_a_folder_holding_disks_is_synchronized_only_on_request() { + let dir = Scratch::new("disks"); + let mut s = library(&dir); + // a library photo on another disk makes the scratch folder's root look like it holds disks + let id = s.catalog.alloc_photo_id(); + let p = lightcraft_catalog::Photo::new( + id, + lightcraft_catalog::Source::File { path: "/Volumes/nas/x.jpg".into() }, + "x.jpg", + "JPEG", + 6, + 4, + "2026-01-01", + ); + s.catalog.apply(lightcraft_catalog::Op::AddPhoto { photo: Box::new(p) }).unwrap(); + if std::path::Path::new("/Volumes").is_dir() { + let e = s.execute("folder.scanChanges", &json!({"path": "/Volumes"})).unwrap_err().to_string(); + assert!(e.contains("disk: true"), "{e}"); + } + // a folder inside one disk needs nothing more + assert!(s.execute("folder.scanChanges", &json!({"path": dir.path("trip")})).is_ok()); +} diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index e747ad2f2..31525843f 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -1903,5 +1903,6 @@ "Missing photos move to Recently Deleted and can be restored; no file on disk is touched.": "Fehlende Fotos werden nach „Zuletzt gelöscht“ verschoben und können wiederhergestellt werden; keine Datei auf dem Datenträger wird verändert.", "Reading a sidecar replaces the photo's rating, labels, metadata and edits with what it says.": "Beim Lesen einer Filialdatei werden Bewertung, Markierungen, Metadaten und Bearbeitungen des Fotos durch ihren Inhalt ersetzt.", "Skipped: files already in the library elsewhere": "Übersprungen: Dateien, die bereits an anderer Stelle in der Bibliothek sind", - "Skipped: files that can't be read": "Übersprungen: Dateien, die nicht gelesen werden können" + "Skipped: files that can't be read": "Übersprungen: Dateien, die nicht gelesen werden können", + "This folder isn't there: it was moved or renamed, or its disk isn't connected.": "Dieser Ordner ist nicht vorhanden: Er wurde verschoben oder umbenannt, oder sein Datenträger ist nicht verbunden." } diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index 0b29f570b..b6adc732d 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -1748,5 +1748,6 @@ "Missing photos move to Recently Deleted and can be restored; no file on disk is touched.": "Las fotos que faltan pasan a Eliminadas recientemente y se pueden restaurar; no se toca ningún archivo del disco.", "Reading a sidecar replaces the photo's rating, labels, metadata and edits with what it says.": "Leer un archivo complementario sustituye la valoración, las etiquetas, los metadatos y las ediciones de la foto por lo que contiene.", "Skipped: files already in the library elsewhere": "Omitidos: archivos que ya están en la biblioteca en otro lugar", - "Skipped: files that can't be read": "Omitidos: archivos que no se pueden leer" + "Skipped: files that can't be read": "Omitidos: archivos que no se pueden leer", + "This folder isn't there: it was moved or renamed, or its disk isn't connected.": "Esta carpeta no está: se movió o se le cambió el nombre, o su disco no está conectado." } diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index ae2023ff7..242a687a6 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -1748,5 +1748,6 @@ "Missing photos move to Recently Deleted and can be restored; no file on disk is touched.": "見つからない写真は「最近削除した写真」に移動し、復元できます。ディスク上のファイルには手を加えません", "Reading a sidecar replaces the photo's rating, labels, metadata and edits with what it says.": "サイドカーを読み込むと、写真のレーティング、ラベル、メタデータ、編集内容がその内容に置き換わります。", "Skipped: files already in the library elsewhere": "スキップ: 別の場所ですでにライブラリにあるファイル", - "Skipped: files that can't be read": "スキップ: 読み込めないファイル" + "Skipped: files that can't be read": "スキップ: 読み込めないファイル", + "This folder isn't there: it was moved or renamed, or its disk isn't connected.": "このフォルダが見つかりません。移動または名前の変更がされたか、ディスクが接続されていません。" } diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index 71369387f..00f720119 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -1748,5 +1748,6 @@ "Missing photos move to Recently Deleted and can be restored; no file on disk is touched.": "As fotos ausentes vão para Excluídos recentemente e podem ser restauradas; nenhum arquivo no disco é alterado.", "Reading a sidecar replaces the photo's rating, labels, metadata and edits with what it says.": "Ler um arquivo auxiliar substitui a classificação, os rótulos, os metadados e as edições da foto pelo que ele contém.", "Skipped: files already in the library elsewhere": "Ignorados: arquivos que já estão na biblioteca em outro lugar", - "Skipped: files that can't be read": "Ignorados: arquivos que não podem ser lidos" + "Skipped: files that can't be read": "Ignorados: arquivos que não podem ser lidos", + "This folder isn't there: it was moved or renamed, or its disk isn't connected.": "Esta pasta não está lá: foi movida ou renomeada, ou o disco dela não está conectado." } diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index dc2b35756..16c6f7cdf 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -1421,5 +1421,6 @@ "Missing photos move to Recently Deleted and can be restored; no file on disk is touched.": "Отсутствующие фото перемещаются в «Недавно удалённые» и могут быть восстановлены; файлы на диске не затрагиваются.", "Reading a sidecar replaces the photo's rating, labels, metadata and edits with what it says.": "Чтение файла XMP заменяет рейтинг, метки, метаданные и правки фото его содержимым.", "Skipped: files already in the library elsewhere": "Пропущено: файлы, которые уже есть в медиатеке в другом месте", - "Skipped: files that can't be read": "Пропущено: файлы, которые не удаётся прочитать" + "Skipped: files that can't be read": "Пропущено: файлы, которые не удаётся прочитать", + "This folder isn't there: it was moved or renamed, or its disk isn't connected.": "Этой папки нет: её переместили или переименовали, или её диск не подключён." } diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index 2123391d6..0206938d5 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -1748,5 +1748,6 @@ "Missing photos move to Recently Deleted and can be restored; no file on disk is touched.": "缺失的照片会移到“最近删除”,可以恢复;磁盘上的文件不会被改动。", "Reading a sidecar replaces the photo's rating, labels, metadata and edits with what it says.": "读取附属文件会用其内容替换照片的评级、标签、元数据和编辑。", "Skipped: files already in the library elsewhere": "已跳过:已在图库其他位置的文件", - "Skipped: files that can't be read": "已跳过:无法读取的文件" + "Skipped: files that can't be read": "已跳过:无法读取的文件", + "This folder isn't there: it was moved or renamed, or its disk isn't connected.": "此文件夹不存在:它已被移动或重命名,或其磁盘未连接。" } diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index 8fa254cdf..ffc873730 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -1748,5 +1748,6 @@ "Missing photos move to Recently Deleted and can be restored; no file on disk is touched.": "遺失的照片會移至「最近刪除」,可以還原;磁碟上的檔案不會被更動。", "Reading a sidecar replaces the photo's rating, labels, metadata and edits with what it says.": "讀取附屬檔案會以其內容取代照片的評等、標籤、中繼資料和編輯。", "Skipped: files already in the library elsewhere": "已略過:已在照片庫其他位置的檔案", - "Skipped: files that can't be read": "已略過:無法讀取的檔案" + "Skipped: files that can't be read": "已略過:無法讀取的檔案", + "This folder isn't there: it was moved or renamed, or its disk isn't connected.": "此資料夾不存在:它已被移動或重新命名,或其磁碟未連接。" } diff --git a/crates/ui-egui/src/panels/left.rs b/crates/ui-egui/src/panels/left.rs index 3c0a0c216..dff42c1e0 100644 --- a/crates/ui-egui/src/panels/left.rs +++ b/crates/ui-egui/src/panels/left.rs @@ -1247,7 +1247,8 @@ fn synchronize_item(app: &mut LightcraftApp, ui: &mut egui::Ui, n: &FolderNode, .on_hover_text(crate::i18n::tr("Find photos added to or missing from this folder on disk, and XMP sidecars changed by other apps")); crate::widgets::register(ui.ctx(), "folderSynchronize", r.rect); if r.clicked() { - if let Err(e) = crate::sync::open(app, &n.path, name) { + // a disk, or a folder holding disks, was chosen as such + if let Err(e) = crate::sync::open(app, &n.path, name, n.volume || !n.selectable) { app.toast(ui.ctx(), e); } ui.close(); diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index e1a754747..0857263df 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -671,6 +671,9 @@ pub enum Dialog { path: String, /// What the dialog calls it (a folder's last two names). name: String, + /// A whole disk, or a folder holding disks (`folder.synchronize` `disk`). + #[serde(default)] + disk: bool, /// What the scan found (`None` while it runs). counts: Option, import_new: bool, @@ -685,6 +688,9 @@ pub enum Dialog { #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct SyncCounts { + /// The folder isn't there (moved, renamed, or on a disk that isn't connected). + #[serde(default)] + pub offline: bool, pub new: usize, pub duplicates: usize, pub unreadable: usize, diff --git a/crates/ui-egui/src/sync.rs b/crates/ui-egui/src/sync.rs index 24bad291a..26c1ed680 100644 --- a/crates/ui-egui/src/sync.rs +++ b/crates/ui-egui/src/sync.rs @@ -22,9 +22,9 @@ pub struct SyncTask { } /// Open the dialog for folder `path` (called `name` in it) and start scanning. -pub fn open(app: &mut LightcraftApp, path: &str, name: &str) -> Result<(), String> { +pub fn open(app: &mut LightcraftApp, path: &str, name: &str, disk: bool) -> Result<(), String> { cancel(app); - let input = SyncInput::new(&mut app.session, path).map_err(|e| e.to_string())?; + let input = SyncInput::new(&mut app.session, path, disk).map_err(|e| e.to_string())?; let progress = Arc::new(ScanProgress::default()); let (tx, rx) = std::sync::mpsc::channel(); let p = progress.clone(); @@ -40,6 +40,7 @@ pub fn open(app: &mut LightcraftApp, path: &str, name: &str) -> Result<(), Strin app.ui.dialog = Some(Dialog::SynchronizeFolder { path: path.to_string(), name: name.to_string(), + disk, counts: None, import_new: d.import_new, remove_missing: d.remove_missing, @@ -87,6 +88,7 @@ pub fn poll(app: &mut LightcraftApp, ctx: &egui::Context) { }; app.sync = None; let c = SyncCounts { + offline: changes.offline, new: changes.new.len(), duplicates: changes.duplicates, unreadable: changes.unreadable.len(), @@ -106,7 +108,7 @@ fn progress(app: &LightcraftApp) -> Option<(usize, usize)> { /// The dialog's body. pub fn body(app: &LightcraftApp, ui: &mut egui::Ui, dlg: &mut Dialog) { - let Dialog::SynchronizeFolder { path, name, counts, import_new, remove_missing, read_metadata } = dlg else { return }; + let Dialog::SynchronizeFolder { path, name, counts, import_new, remove_missing, read_metadata, .. } = dlg else { return }; let t = Tokens::get(ui.ctx()); ui.set_min_width(420.0); ui.label(crate::i18n::tr("Bring the library up to date with this folder and the folders inside it:")); @@ -123,6 +125,10 @@ pub fn body(app: &LightcraftApp, ui: &mut egui::Ui, dlg: &mut Dialog) { }); return; }; + if c.offline { + ui.label(crate::i18n::tr("This folder isn't there: it was moved or renamed, or its disk isn't connected.")); + return; + } let choice = |ui: &mut egui::Ui, on: &mut bool, n: usize, text: String, widget: &str| { let r = ui.add_enabled(n > 0, egui::Checkbox::new(on, text)); crate::widgets::register(ui.ctx(), widget, r.rect); @@ -152,11 +158,13 @@ pub fn body(app: &LightcraftApp, ui: &mut egui::Ui, dlg: &mut Dialog) { /// Synchronize: `folder.synchronize` with the dialog's choices. pub fn confirm(app: &mut LightcraftApp, dlg: &Dialog) -> Result { - let Dialog::SynchronizeFolder { path, counts: Some(_), import_new, remove_missing, read_metadata, .. } = dlg else { + let Dialog::SynchronizeFolder { path, disk, counts: Some(_), import_new, remove_missing, read_metadata, .. } = dlg else { return Err("the folder is still being scanned".into()); }; - let r = - app.run("folder.synchronize", json!({"path": path, "importNew": import_new, "removeMissing": remove_missing, "readMetadata": read_metadata})); + let r = app.run( + "folder.synchronize", + json!({"path": path, "disk": disk, "importNew": import_new, "removeMissing": remove_missing, "readMetadata": read_metadata}), + ); app.session.folder_changes = None; r }