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
@@ -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.@@ -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());+}@@ -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; }