Files
relicario/crates/relicario-cli/src/commands/trash.rs
adlee-was-taken c4777cc0bb refactor(cli): apply simplify findings (Plan B Phases 4-6 polish)
- session.rs: drop save_manifest_raw — its only caller was
  after_manifest_change itself; the pub(crate) advertised the exact
  bypass-the-cache-refresh footgun the wrapper exists to eliminate.
  Inline the encrypt + atomic_write pair.
- session.rs: into_kdf_params(self) → to_kdf_params(&self). Body just
  copies three u32s; the consume-self had no ownership benefit and
  forced the round-trip test to rebuild a ParamsFile field-by-field.
- helpers.rs: add git_rm(repo, paths, context) wrapper around git_run
  + the load-bearing --ignore-unmatch flag. Replaces two near-identical
  three-line "build rm_args, extend, git_run" blocks in trash.rs.
- trash.rs: purge_item_filesystem drops the if x.exists() pre-checks
  (TOCTOU + redundant stat per item per trash-empty iteration). Uses
  ErrorKind::NotFound swallow on remove_file/remove_dir_all instead.
- basic_flows.rs: trim trash_empty_batches_into_one_commit's sleep
  comment to just the WHY.
2026-05-09 11:50:42 -04:00

150 lines
5.1 KiB
Rust

//! 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.after_manifest_change(&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.after_manifest_change(&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(())
}
/// Filesystem-only purge: removes the item.enc, attachments/<id>/, and updates
/// the manifest in memory. Returns the relative paths the caller must stage
/// via `git rm` after the loop. Does NOT invoke any git commands — the caller
/// batches them.
pub(super) fn purge_item_filesystem(
vault: &crate::session::UnlockedVault,
manifest: &mut relicario_core::Manifest,
id: &relicario_core::ItemId,
title: &str,
) -> Result<Vec<String>> {
use std::{fs, io::ErrorKind};
let item_rel = format!("items/{}.enc", id.as_str());
let att_rel = format!("attachments/{}", id.as_str());
let ignore_missing = |r: std::io::Result<()>| -> Result<()> {
match r {
Ok(()) => Ok(()),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
Err(e) => Err(e.into()),
}
};
ignore_missing(fs::remove_file(vault.item_path(id)))?;
ignore_missing(fs::remove_dir_all(vault.root().join("attachments").join(id.as_str())))?;
manifest.remove(id);
eprintln!("Purged: {title}");
Ok(vec![item_rel, att_rel])
}
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;
let paths = purge_item_filesystem(&vault, &mut manifest, &id, &title)?;
vault.after_manifest_change(&manifest)?;
let purge_ctx = format!("purge \"{}\" ({})", title, id.as_str());
crate::helpers::git_rm(vault.root(), &paths, &format!("{purge_ctx}: git rm"))?;
crate::helpers::git_run(
vault.root(),
&["add", "manifest.enc"],
&format!("{purge_ctx}: git add manifest.enc"),
)?;
crate::helpers::git_run(
vault.root(),
&["commit", "-m", &format!("purge: {} ({})", title, id.as_str())],
&format!("{purge_ctx}: git commit"),
)?;
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 all_paths: Vec<String> = Vec::new();
let purged_count = purgeable.len();
for (id, title) in purgeable {
let mut paths = purge_item_filesystem(&vault, &mut manifest, &id, &title)?;
all_paths.append(&mut paths);
}
vault.after_manifest_change(&manifest)?;
crate::helpers::git_rm(vault.root(), &all_paths, "trash empty: git rm")?;
crate::helpers::git_run(
vault.root(),
&["add", "manifest.enc"],
"trash empty: git add manifest.enc",
)?;
crate::helpers::git_run(
vault.root(),
&["commit", "-m", &format!("trash empty: purged {} item(s)", purged_count)],
"trash empty: git commit",
)?;
eprintln!("Emptied trash: {} item(s)", purged_count);
Ok(())
}