- 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.
150 lines
5.1 KiB
Rust
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(())
|
|
}
|