refactor(cli): move trash family (rm/restore/purge/trash) into commands/
This commit is contained in:
@@ -13,6 +13,7 @@ pub mod list;
|
|||||||
pub mod rate;
|
pub mod rate;
|
||||||
pub mod status;
|
pub mod status;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
|
pub mod trash;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
|||||||
132
crates/relicario-cli/src/commands/trash.rs
Normal file
132
crates/relicario-cli/src/commands/trash.rs
Normal file
@@ -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(())
|
||||||
|
}
|
||||||
@@ -212,7 +212,7 @@ enum Commands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum AddKind {
|
pub(crate) enum AddKind {
|
||||||
Login {
|
Login {
|
||||||
#[arg(long)] title: Option<String>,
|
#[arg(long)] title: Option<String>,
|
||||||
#[arg(long)] username: Option<String>,
|
#[arg(long)] username: Option<String>,
|
||||||
@@ -276,7 +276,7 @@ enum AddKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum TrashAction {
|
pub(crate) enum TrashAction {
|
||||||
/// List trashed items.
|
/// List trashed items.
|
||||||
List,
|
List,
|
||||||
/// Purge every trashed item past its retention window.
|
/// Purge every trashed item past its retention window.
|
||||||
@@ -284,7 +284,7 @@ enum TrashAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum SettingsAction {
|
pub(crate) enum SettingsAction {
|
||||||
/// Show current settings as JSON.
|
/// Show current settings as JSON.
|
||||||
Show,
|
Show,
|
||||||
/// Set trash retention (e.g., --days 30 or --forever).
|
/// Set trash retention (e.g., --days 30 or --forever).
|
||||||
@@ -328,7 +328,7 @@ enum SettingsAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum BackupAction {
|
pub(crate) enum BackupAction {
|
||||||
/// Pack the local vault into a single encrypted `.relbak` file.
|
/// Pack the local vault into a single encrypted `.relbak` file.
|
||||||
/// Backup passphrase is independent of the vault passphrase.
|
/// Backup passphrase is independent of the vault passphrase.
|
||||||
Export {
|
Export {
|
||||||
@@ -357,7 +357,7 @@ enum BackupAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum ImportAction {
|
pub(crate) enum ImportAction {
|
||||||
/// Import a LastPass CSV export into the unlocked vault.
|
/// Import a LastPass CSV export into the unlocked vault.
|
||||||
/// Each row creates a new item with a freshly-minted ID; title
|
/// Each row creates a new item with a freshly-minted ID; title
|
||||||
/// collisions are kept (no dedup). Failed rows are skipped and
|
/// collisions are kept (no dedup). Failed rows are skipped and
|
||||||
@@ -369,7 +369,7 @@ enum ImportAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum DeviceAction {
|
pub(crate) enum DeviceAction {
|
||||||
/// Register this machine as a new device.
|
/// Register this machine as a new device.
|
||||||
///
|
///
|
||||||
/// Generates two ed25519 keypairs: one for signing commits, one for push
|
/// Generates two ed25519 keypairs: one for signing commits, one for push
|
||||||
@@ -417,7 +417,7 @@ enum DeviceAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Subcommand)]
|
#[derive(clap::Subcommand)]
|
||||||
enum RecoveryQrCmd {
|
pub(crate) enum RecoveryQrCmd {
|
||||||
/// Generate a recovery QR code and display it as ASCII art in the terminal.
|
/// Generate a recovery QR code and display it as ASCII art in the terminal.
|
||||||
Generate,
|
Generate,
|
||||||
/// Unwrap a recovery QR payload (base64) to recover the image_secret as hex.
|
/// 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::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::Edit { query, totp_qr } => cmd_edit(query, totp_qr),
|
||||||
Commands::History { query, show, field } => commands::list::cmd_history(query, show, field),
|
Commands::History { query, show, field } => commands::list::cmd_history(query, show, field),
|
||||||
Commands::Rm { query } => cmd_rm(query),
|
Commands::Rm { query } => commands::trash::cmd_rm(query),
|
||||||
Commands::Restore { query } => cmd_restore(query),
|
Commands::Restore { query } => commands::trash::cmd_restore(query),
|
||||||
Commands::Purge { query } => cmd_purge(query),
|
Commands::Purge { query } => commands::trash::cmd_purge(query),
|
||||||
Commands::Trash { action } => cmd_trash(action),
|
Commands::Trash { action } => commands::trash::cmd_trash(action),
|
||||||
Commands::Backup { action } => cmd_backup(action),
|
Commands::Backup { action } => cmd_backup(action),
|
||||||
Commands::Import { action } => cmd_import(action),
|
Commands::Import { action } => cmd_import(action),
|
||||||
Commands::Attach { query, file } => cmd_attach(query, file),
|
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<()> {
|
fn cmd_backup(action: BackupAction) -> Result<()> {
|
||||||
match action {
|
match action {
|
||||||
BackupAction::Export { out, include_image, image, no_history } => {
|
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);
|
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<()> {
|
fn cmd_attach(query: String, file: PathBuf) -> Result<()> {
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use relicario_core::{encrypt_attachment, AttachmentRef};
|
use relicario_core::{encrypt_attachment, AttachmentRef};
|
||||||
|
|||||||
Reference in New Issue
Block a user