Archiv-Wechsel mit Migrations-Option

Bei gesetztem Archiv-Home fragt ein Dialog nach: Dokumente ins neue
Archiv verschieben (Checkbox, standardmäßig aus — nichts wandert
implizit) oder liegen lassen. change_archive_home_cmd setzt das neue
Home, zieht auf Wunsch die Einträge um (gleiche Platte rename, sonst
Kopie + Löschen; Kollision im Ziel bricht laut ab) und nimmt die Rechte
des alten Ordners zurück.
This commit is contained in:
marcus hinz
2026-07-20 13:48:49 +02:00
parent 347fc70982
commit cf2c25df79
8 changed files with 180 additions and 2 deletions
+1
View File
@@ -44,6 +44,7 @@ fn main() {
"panel_archive_dir_cmd",
"panel_archive_cmd",
"set_archive_home_cmd",
"change_archive_home_cmd",
"clear_archive_home_cmd",
"archive_docs_cmd",
"reveal_path_cmd",
+2 -1
View File
@@ -53,6 +53,7 @@
"allow-clear-archive-home-cmd",
"allow-archive-docs-cmd",
"allow-delete-preview",
"allow-delete-project-scoped"
"allow-delete-project-scoped",
"allow-change-archive-home-cmd"
]
}
@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-change-archive-home-cmd"
description = "Enables the change_archive_home_cmd command without any pre-configured scope."
commands.allow = ["change_archive_home_cmd"]
[[permission]]
identifier = "deny-change-archive-home-cmd"
description = "Denies the change_archive_home_cmd command without any pre-configured scope."
commands.deny = ["change_archive_home_cmd"]
+1
View File
@@ -282,6 +282,7 @@ fn invoke_handlers() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send +
commands::panel_archive_dir_cmd,
commands::panel_archive_cmd,
commands::set_archive_home_cmd,
commands::change_archive_home_cmd,
commands::clear_archive_home_cmd,
commands::archive_docs_cmd,
commands::reveal_path_cmd,
+12
View File
@@ -366,6 +366,18 @@ pub(crate) fn set_archive_home_cmd(project: String, dir: String) -> Result<(), S
crate::domain::archive::set_project_archive_home(&project, &dir)
}
/// Wechselt das Archiv-Home (Einstellungsdialog, wenn schon eins gesetzt
/// ist): optional ziehen die Dokumente mit um; die Rechte des alten Ordners
/// werden zurückgenommen.
#[tauri::command]
pub(crate) fn change_archive_home_cmd(
project: String,
dir: String,
migrate: bool,
) -> Result<(), String> {
crate::domain::archive::change_project_archive_home(&project, &dir, migrate)
}
/// Wählt das Archiv ab (Einstellungsdialog): Config-Eintrag und Permissions
/// weg, der Ordner bleibt liegen.
#[tauri::command]
+89
View File
@@ -95,6 +95,73 @@ pub(crate) fn add_archive_permission(
crate::domain::write_atomic(&sp, &(raw + "\n"))
}
/// Wechselt das Archiv-Home: neues Home setzen (validieren, anlegen, Rechte,
/// Config), auf Wunsch die Dokumente aus dem alten Home hinüberziehen, dann
/// die Rechte des alten zurücknehmen. Ohne `migrate` bleibt das alte Archiv
/// unverändert liegen — nichts wird implizit verschoben; der Umzug ist eine
/// bewusste Option im Dialog.
pub(crate) fn change_project_archive_home(
project: &str,
dir: &str,
migrate: bool,
) -> Result<(), String> {
let paths = Paths::real();
let old = read_project_config_in(&paths, project)?.archive_home;
set_project_archive_home(project, dir)?;
let new_home = expand_home(&paths, dir);
if let Some(old_c) = old {
let old_home = expand_home(&paths, &old_c);
if old_home == new_home {
return Ok(());
}
if migrate && old_home.is_dir() {
move_dir_contents(&old_home, &new_home)?;
}
remove_archive_permission(&paths, project, &old_c)?;
}
Ok(())
}
/// Zieht alle Einträge von `from` nach `to` um. Gleiche Platte per rename,
/// über Dateisystemgrenzen als Kopie + Löschen. Ein im Ziel schon vorhandener
/// Name bricht laut ab — nichts wird überschrieben.
fn move_dir_contents(from: &std::path::Path, to: &std::path::Path) -> Result<(), String> {
for entry in fs::read_dir(from).map_err(|e| format!("{}: {e}", from.display()))? {
let entry = entry.map_err(|e| format!("{}: {e}", from.display()))?;
let src = entry.path();
let dest = to.join(entry.file_name());
if dest.exists() {
return Err(format!("existiert schon im neuen Archiv: {}", dest.display()));
}
match fs::rename(&src, &dest) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::CrossesDevices => {
copy_recursive(&src, &dest)?;
if src.is_dir() {
fs::remove_dir_all(&src).map_err(|e| format!("{}: {e}", src.display()))?;
} else {
fs::remove_file(&src).map_err(|e| format!("{}: {e}", src.display()))?;
}
}
Err(e) => return Err(format!("{}: {e}", src.display())),
}
}
Ok(())
}
fn copy_recursive(src: &std::path::Path, dest: &std::path::Path) -> Result<(), String> {
if src.is_dir() {
fs::create_dir_all(dest).map_err(|e| format!("{}: {e}", dest.display()))?;
for entry in fs::read_dir(src).map_err(|e| format!("{}: {e}", src.display()))? {
let entry = entry.map_err(|e| format!("{}: {e}", src.display()))?;
copy_recursive(&entry.path(), &dest.join(entry.file_name()))?;
}
} else {
fs::copy(src, dest).map_err(|e| format!("{}: {e}", src.display()))?;
}
Ok(())
}
/// Wählt das Archiv ab: archiveHome aus der config.json entfernen und die
/// beim Setzen eingetragenen Rechte (additionalDirectories + Edit-Allow) aus
/// der Projekt-settings.json zurücknehmen. Der Ordner selbst bleibt liegen.
@@ -451,6 +518,28 @@ mod tests {
assert_eq!(parse_tag_list(&map["tags"]), vec!["adr", "infra"]);
}
/// Umzug: Einträge wandern komplett, Namenskollision bricht laut ab.
#[test]
fn archiv_umzug_verschiebt_und_kollidiert_laut() {
let home = crate::domain::testutil::tmp_paths().home;
let (alt, neu) = (home.join("alt"), home.join("neu"));
fs::create_dir_all(alt.join("ordner")).unwrap();
fs::write(alt.join("doc.md"), "inhalt").unwrap();
fs::write(alt.join("ordner/tief.md"), "tief").unwrap();
fs::create_dir_all(&neu).unwrap();
move_dir_contents(&alt, &neu).unwrap();
assert_eq!(fs::read_to_string(neu.join("doc.md")).unwrap(), "inhalt");
assert_eq!(fs::read_to_string(neu.join("ordner/tief.md")).unwrap(), "tief");
assert!(fs::read_dir(&alt).unwrap().next().is_none());
// Kollision: gleicher Name im Ziel → Fehler, nichts überschrieben
fs::write(alt.join("doc.md"), "neuer").unwrap();
let err = move_dir_contents(&alt, &neu).unwrap_err();
assert!(err.contains("existiert schon"));
assert_eq!(fs::read_to_string(neu.join("doc.md")).unwrap(), "inhalt");
}
#[test]
fn check_folder_relativ_ohne_punktpunkt() {
assert!(check_folder("konzepte/panel").is_ok());