collective

Browse / github.com/storytold/lightcraft / patch

LRC-LIB-LIBFOLDERS: a date folder inside the library can be renamed

LRC-LIB-LIBFOLDERS: a date folder inside the library can be renamed ## Problem Folder ▸ Rename (and `folder.rename` / `folder.move`) refused every folder below the library folder, because `move_folder` rejected `src.starts_with(&l.dir)`. The date folders an import copies into `Originals/` could never be renamed, and the Rename prompt just closed with no message. ## Change - `crates/engine/src/cmd/browse.rs`: refuse only the library folder, its parents, and its own top-level folders (`Originals`, `thumbs`); anything deeper can be renamed or moved, with undo as before. - `crates/ui-egui/src/panels/dialogs.rs`: a refused `TextPrompt` (Rename Folder, …) closes an

Status
open
Score
0.00
Adopters
0 other operators
Author
emmertex (imported from pull request #3016, open)
Base
387bed199fca44d76af5aa1c24dd7cc3de4c8db2
Size
+36 −2 in 3 files
Review
pending
Shared
2026-10-28
Id
a25aea4f2df0ccea618ca095

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/cmd/browse.rs crates/engine/src/tests_folders.rs crates/ui-egui/src/panels/dialogs.rs

diff --git a/crates/engine/src/cmd/browse.rs b/crates/engine/src/cmd/browse.rs--- a/crates/engine/src/cmd/browse.rs+++ b/crates/engine/src/cmd/browse.rs@@ -513,7 +513,13 @@ pub(crate) fn move_folder(s: &mut Session, from: &str, to: &str) -> std::result:     if !src.is_dir() {         return Err(format!("{from} is not a folder"));     }-    if s.library.as_ref().is_some_and(|l| l.dir.starts_with(src) || src.starts_with(&l.dir)) {+    // `a/../b` names a folder without spelling it, which the checks below and the relink can't see+    if src.components().any(|c| matches!(c, std::path::Component::ParentDir)) {+        return Err(format!("{from}: give the folder's path without '..'"));+    }+    // the library folder itself, and its own top-level folders (`Originals`, `thumbs`), stay where+    // the library expects them; folders below those (an import's date folders) can be renamed+    if s.library.as_ref().is_some_and(|l| l.dir.starts_with(src) || src.parent() == Some(l.dir.as_path())) {         return Err("the library's own folders can't be moved here".into());     }     // Repairs retain their exact old paths, including those on surviving history entries.diff --git a/crates/engine/src/tests_folders.rs b/crates/engine/src/tests_folders.rs--- a/crates/engine/src/tests_folders.rs+++ b/crates/engine/src/tests_folders.rs@@ -16,6 +16,7 @@  use lightcraft_catalog::{Op, Photo, Source}; use serde_json::json;+use std::path::Path;  use crate::{LibrarySource, Session, filter_chips}; @@ -701,3 +702,29 @@ fn a_destination_without_write_permission_reports_an_error_and_keeps_everything(     }     std::fs::set_permissions(dir.0.join("ro"), std::fs::Permissions::from_mode(0o755)).unwrap(); }++#[test]+fn a_date_folder_inside_the_library_can_be_renamed_but_the_librarys_own_folders_cannot() {+    let dir = Scratch::new("in-library");+    let lib = dir.0.join("lib");+    let mut s = Session::new();+    s.open_library(&lib, false).unwrap();+    let day = lib.join("Originals").join("2026-10-16");+    std::fs::create_dir_all(&day).unwrap();+    add(&mut s, &day.join("a.jpg").to_string_lossy());+    let day = day.to_string_lossy().to_string();+    s.execute("folder.rename", &json!({"path": day, "name": "Beach"})).unwrap();+    assert!(lib.join("Originals/Beach").is_dir(), "the imported date folder is renamed");+    assert!(!Path::new(&day).exists());+    s.execute("edit.undo", &json!({})).unwrap();+    assert!(Path::new(&day).is_dir(), "undo names it back");+    for own in ["Originals", "thumbs"] {+        std::fs::create_dir_all(lib.join(own)).unwrap();+        let path = lib.join(own).to_string_lossy().to_string();+        assert!(s.execute("folder.rename", &json!({"path": path, "name": "x"})).is_err(), "{own} stays put");+    }+    assert!(s.execute("folder.rename", &json!({"path": lib.to_string_lossy(), "name": "x"})).is_err(), "and so does the library");+    let alias = lib.join("Originals").join("..").join("Originals").to_string_lossy().to_string();+    assert!(s.execute("folder.rename", &json!({"path": alias, "name": "x"})).is_err(), "even named through '..'");+    assert!(lib.join("Originals").is_dir());+}diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs--- a/crates/ui-egui/src/panels/dialogs.rs+++ b/crates/ui-egui/src/panels/dialogs.rs@@ -1581,7 +1581,8 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context, blocked: bool) {                 }                 close = true;             }-            Err(e) if matches!(dlg, Dialog::MovePhotos { .. }) => {+            // a prompt (Rename Folder, …) that is refused closes and says why+            Err(e) if matches!(dlg, Dialog::MovePhotos { .. } | Dialog::TextPrompt { .. }) => {                 app.toast(ctx, e);                 close = true;             }