From 530c479f1995c2953e411352424579cd5b4cc1be Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Wed, 6 May 2026 18:48:00 -0400 Subject: [PATCH] refactor(cli): move trash family (rm/restore/purge/trash) into commands/ --- crates/relicario-cli/src/commands/mod.rs | 1 + crates/relicario-cli/src/commands/trash.rs | 132 ++++++++++++++++++ crates/relicario-cli/src/main.rs | 147 ++------------------- 3 files changed, 144 insertions(+), 136 deletions(-) create mode 100644 crates/relicario-cli/src/commands/trash.rs diff --git a/crates/relicario-cli/src/commands/mod.rs b/crates/relicario-cli/src/commands/mod.rs index 90ac424..952d233 100644 --- a/crates/relicario-cli/src/commands/mod.rs +++ b/crates/relicario-cli/src/commands/mod.rs @@ -13,6 +13,7 @@ pub mod list; pub mod rate; pub mod status; pub mod sync; +pub mod trash; use anyhow::Result; diff --git a/crates/relicario-cli/src/commands/trash.rs b/crates/relicario-cli/src/commands/trash.rs new file mode 100644 index 0000000..5bd7799 --- /dev/null +++ b/crates/relicario-cli/src/commands/trash.rs @@ -0,0 +1,132 @@ +//! Trash umbrella: `rm` (soft-delete), `restore`, `purge` (permanent), +//! `trash list` / `trash empty`. + +use anyhow::Result; + +use crate::TrashAction; + +pub fn cmd_rm(query: String) -> Result<()> { + let vault = crate::session::UnlockedVault::unlock_interactive()?; + let mut manifest = vault.load_manifest()?; + let entry = super::resolve_query(&manifest, &query)?; + let id = entry.id.clone(); + let _ = entry; + let mut item = vault.load_item(&id)?; + item.soft_delete(); + vault.save_item(&item)?; + manifest.upsert(&item); + vault.save_manifest(&manifest)?; + crate::refresh_groups_cache(vault.root(), &manifest); + super::commit_paths(&vault, &format!("trash: {} ({})", crate::helpers::sanitize_for_commit(&item.title), item.id.as_str()), + &[&format!("items/{}.enc", item.id.as_str()), "manifest.enc"])?; + eprintln!("Moved to trash: {}", item.title); + Ok(()) +} + +pub fn cmd_restore(query: String) -> Result<()> { + let vault = crate::session::UnlockedVault::unlock_interactive()?; + let mut manifest = vault.load_manifest()?; + let entry = super::resolve_query(&manifest, &query)?; + let id = entry.id.clone(); + let _ = entry; + let mut item = vault.load_item(&id)?; + item.restore(); + vault.save_item(&item)?; + manifest.upsert(&item); + vault.save_manifest(&manifest)?; + crate::refresh_groups_cache(vault.root(), &manifest); + super::commit_paths(&vault, &format!("restore: {} ({})", crate::helpers::sanitize_for_commit(&item.title), item.id.as_str()), + &[&format!("items/{}.enc", item.id.as_str()), "manifest.enc"])?; + eprintln!("Restored: {}", item.title); + Ok(()) +} + +/// Inner purge: assumes vault is already unlocked and manifest is loaded. +/// Caller is responsible for saving the manifest and committing afterwards. +pub(super) fn purge_item( + vault: &crate::session::UnlockedVault, + manifest: &mut relicario_core::Manifest, + id: &relicario_core::ItemId, + title: &str, +) -> Result<()> { + use std::fs; + + let item_path = vault.item_path(id); + if item_path.exists() { fs::remove_file(&item_path)?; } + let att_dir = vault.root().join("attachments").join(id.as_str()); + if att_dir.exists() { fs::remove_dir_all(&att_dir)?; } + manifest.remove(id); + + let _ = crate::helpers::git_command(vault.root(), &["rm", "-rf", "--ignore-unmatch", + &format!("items/{}.enc", id.as_str()), + &format!("attachments/{}", id.as_str()), + ]).status()?; + // Note: caller adds+commits manifest.enc after processing all purges. + eprintln!("Purged: {title}"); + Ok(()) +} + +pub fn cmd_purge(query: String) -> Result<()> { + let vault = crate::session::UnlockedVault::unlock_interactive()?; + let mut manifest = vault.load_manifest()?; + let entry = super::resolve_query(&manifest, &query)?; + let id = entry.id.clone(); + let title = entry.title.clone(); + let _ = entry; + + purge_item(&vault, &mut manifest, &id, &title)?; + vault.save_manifest(&manifest)?; + crate::refresh_groups_cache(vault.root(), &manifest); + + let status = crate::helpers::git_command(vault.root(), &["add", "manifest.enc"]).status()?; + if !status.success() { anyhow::bail!("git add manifest.enc failed"); } + let status = crate::helpers::git_command(vault.root(), + &["commit", "-m", &format!("purge: {} ({})", title, id.as_str())]).status()?; + if !status.success() { anyhow::bail!("git commit failed"); } + Ok(()) +} + +pub fn cmd_trash(action: TrashAction) -> Result<()> { + match action { + TrashAction::List => super::list::cmd_list(None, None, None, true), + TrashAction::Empty => cmd_trash_empty(), + } +} + +pub fn cmd_trash_empty() -> Result<()> { + use relicario_core::time::now_unix; + + let vault = crate::session::UnlockedVault::unlock_interactive()?; + let mut manifest = vault.load_manifest()?; + let settings = vault.load_settings()?; + let now = now_unix(); + + let purgeable: Vec<_> = manifest.items.values() + .filter(|e| match e.trashed_at { + Some(t) => settings.trash_retention.should_purge(t, now), + None => false, + }) + .map(|e| (e.id.clone(), e.title.clone())) + .collect(); + + if purgeable.is_empty() { + eprintln!("nothing past retention window"); + return Ok(()); + } + + let mut purged_titles = Vec::new(); + for (id, title) in purgeable { + purge_item(&vault, &mut manifest, &id, &title)?; + purged_titles.push(title); + } + + vault.save_manifest(&manifest)?; + let status = crate::helpers::git_command(vault.root(), &["add", "manifest.enc"]).status()?; + if !status.success() { anyhow::bail!("git add manifest.enc failed"); } + let status = crate::helpers::git_command(vault.root(), + &["commit", "-m", &format!("trash empty: purged {} item(s)", purged_titles.len())]).status()?; + if !status.success() { anyhow::bail!("git commit failed"); } + + eprintln!("Emptied trash: {} item(s)", purged_titles.len()); + Ok(()) +} diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index aa7fbf3..7d32281 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -212,7 +212,7 @@ enum Commands { } #[derive(Subcommand)] -enum AddKind { +pub(crate) enum AddKind { Login { #[arg(long)] title: Option, #[arg(long)] username: Option, @@ -276,7 +276,7 @@ enum AddKind { } #[derive(Subcommand)] -enum TrashAction { +pub(crate) enum TrashAction { /// List trashed items. List, /// Purge every trashed item past its retention window. @@ -284,7 +284,7 @@ enum TrashAction { } #[derive(Subcommand)] -enum SettingsAction { +pub(crate) enum SettingsAction { /// Show current settings as JSON. Show, /// Set trash retention (e.g., --days 30 or --forever). @@ -328,7 +328,7 @@ enum SettingsAction { } #[derive(Subcommand)] -enum BackupAction { +pub(crate) enum BackupAction { /// Pack the local vault into a single encrypted `.relbak` file. /// Backup passphrase is independent of the vault passphrase. Export { @@ -357,7 +357,7 @@ enum BackupAction { } #[derive(Subcommand)] -enum ImportAction { +pub(crate) enum ImportAction { /// Import a LastPass CSV export into the unlocked vault. /// Each row creates a new item with a freshly-minted ID; title /// collisions are kept (no dedup). Failed rows are skipped and @@ -369,7 +369,7 @@ enum ImportAction { } #[derive(Subcommand)] -enum DeviceAction { +pub(crate) enum DeviceAction { /// Register this machine as a new device. /// /// Generates two ed25519 keypairs: one for signing commits, one for push @@ -417,7 +417,7 @@ enum DeviceAction { } #[derive(clap::Subcommand)] -enum RecoveryQrCmd { +pub(crate) enum RecoveryQrCmd { /// Generate a recovery QR code and display it as ASCII art in the terminal. Generate, /// Unwrap a recovery QR payload (base64) to recover the image_secret as hex. @@ -433,10 +433,10 @@ fn main() -> Result<()> { Commands::List { r#type, group, tag, trashed } => commands::list::cmd_list(r#type, group, tag, trashed), Commands::Edit { query, totp_qr } => cmd_edit(query, totp_qr), Commands::History { query, show, field } => commands::list::cmd_history(query, show, field), - Commands::Rm { query } => cmd_rm(query), - Commands::Restore { query } => cmd_restore(query), - Commands::Purge { query } => cmd_purge(query), - Commands::Trash { action } => cmd_trash(action), + Commands::Rm { query } => commands::trash::cmd_rm(query), + Commands::Restore { query } => commands::trash::cmd_restore(query), + Commands::Purge { query } => commands::trash::cmd_purge(query), + Commands::Trash { action } => commands::trash::cmd_trash(action), Commands::Backup { action } => cmd_backup(action), Commands::Import { action } => cmd_import(action), Commands::Attach { query, file } => cmd_attach(query, file), @@ -979,94 +979,6 @@ fn push_history( }); } -fn cmd_rm(query: String) -> Result<()> { - let vault = crate::session::UnlockedVault::unlock_interactive()?; - let mut manifest = vault.load_manifest()?; - let entry = resolve_query(&manifest, &query)?; - let id = entry.id.clone(); - let _ = entry; - let mut item = vault.load_item(&id)?; - item.soft_delete(); - vault.save_item(&item)?; - manifest.upsert(&item); - vault.save_manifest(&manifest)?; - refresh_groups_cache(vault.root(), &manifest); - commit_paths(&vault, &format!("trash: {} ({})", crate::helpers::sanitize_for_commit(&item.title), item.id.as_str()), - &[&format!("items/{}.enc", item.id.as_str()), "manifest.enc"])?; - eprintln!("Moved to trash: {}", item.title); - Ok(()) -} - -fn cmd_restore(query: String) -> Result<()> { - let vault = crate::session::UnlockedVault::unlock_interactive()?; - let mut manifest = vault.load_manifest()?; - let entry = resolve_query(&manifest, &query)?; - let id = entry.id.clone(); - let _ = entry; - let mut item = vault.load_item(&id)?; - item.restore(); - vault.save_item(&item)?; - manifest.upsert(&item); - vault.save_manifest(&manifest)?; - refresh_groups_cache(vault.root(), &manifest); - commit_paths(&vault, &format!("restore: {} ({})", crate::helpers::sanitize_for_commit(&item.title), item.id.as_str()), - &[&format!("items/{}.enc", item.id.as_str()), "manifest.enc"])?; - eprintln!("Restored: {}", item.title); - Ok(()) -} - -/// Inner purge: assumes vault is already unlocked and manifest is loaded. -/// Caller is responsible for saving the manifest and committing afterwards. -fn purge_item( - vault: &crate::session::UnlockedVault, - manifest: &mut relicario_core::Manifest, - id: &relicario_core::ItemId, - title: &str, -) -> Result<()> { - use std::fs; - - let item_path = vault.item_path(id); - if item_path.exists() { fs::remove_file(&item_path)?; } - let att_dir = vault.root().join("attachments").join(id.as_str()); - if att_dir.exists() { fs::remove_dir_all(&att_dir)?; } - manifest.remove(id); - - let _ = crate::helpers::git_command(vault.root(), &["rm", "-rf", "--ignore-unmatch", - &format!("items/{}.enc", id.as_str()), - &format!("attachments/{}", id.as_str()), - ]).status()?; - // Note: caller adds+commits manifest.enc after processing all purges. - eprintln!("Purged: {title}"); - Ok(()) -} - -fn cmd_purge(query: String) -> Result<()> { - let vault = crate::session::UnlockedVault::unlock_interactive()?; - let mut manifest = vault.load_manifest()?; - let entry = resolve_query(&manifest, &query)?; - let id = entry.id.clone(); - let title = entry.title.clone(); - let _ = entry; - - purge_item(&vault, &mut manifest, &id, &title)?; - vault.save_manifest(&manifest)?; - refresh_groups_cache(vault.root(), &manifest); - - let status = crate::helpers::git_command(vault.root(), &["add", "manifest.enc"]).status()?; - if !status.success() { anyhow::bail!("git add manifest.enc failed"); } - let status = crate::helpers::git_command(vault.root(), - &["commit", "-m", &format!("purge: {} ({})", title, id.as_str())]).status()?; - if !status.success() { anyhow::bail!("git commit failed"); } - Ok(()) -} - -fn cmd_trash(action: TrashAction) -> Result<()> { - match action { - TrashAction::List => commands::list::cmd_list(None, None, None, true), - TrashAction::Empty => cmd_trash_empty(), - } -} - fn cmd_backup(action: BackupAction) -> Result<()> { match action { BackupAction::Export { out, include_image, image, no_history } => { @@ -1444,43 +1356,6 @@ fn print_warning(w: &relicario_core::import_lastpass::ImportWarning) { eprintln!("warning: {prefix} {}", w.message); } -fn cmd_trash_empty() -> Result<()> { - use relicario_core::time::now_unix; - - let vault = crate::session::UnlockedVault::unlock_interactive()?; - let mut manifest = vault.load_manifest()?; - let settings = vault.load_settings()?; - let now = now_unix(); - - let purgeable: Vec<_> = manifest.items.values() - .filter(|e| match e.trashed_at { - Some(t) => settings.trash_retention.should_purge(t, now), - None => false, - }) - .map(|e| (e.id.clone(), e.title.clone())) - .collect(); - - if purgeable.is_empty() { - eprintln!("nothing past retention window"); - return Ok(()); - } - - let mut purged_titles = Vec::new(); - for (id, title) in purgeable { - purge_item(&vault, &mut manifest, &id, &title)?; - purged_titles.push(title); - } - - vault.save_manifest(&manifest)?; - let status = crate::helpers::git_command(vault.root(), &["add", "manifest.enc"]).status()?; - if !status.success() { anyhow::bail!("git add manifest.enc failed"); } - let status = crate::helpers::git_command(vault.root(), - &["commit", "-m", &format!("trash empty: purged {} item(s)", purged_titles.len())]).status()?; - if !status.success() { anyhow::bail!("git commit failed"); } - - eprintln!("Emptied trash: {} item(s)", purged_titles.len()); - Ok(()) -} fn cmd_attach(query: String, file: PathBuf) -> Result<()> { use std::fs; use relicario_core::{encrypt_attachment, AttachmentRef};