feat(cli): attachment ops — attach / attachments / extract

Respects AttachmentCaps from settings.enc; content-addressed aid
comes from core::encrypt_attachment.
This commit is contained in:
adlee-was-taken
2026-04-19 22:27:13 -04:00
parent b5015b3e9b
commit cbd1dbd706

View File

@@ -1080,9 +1080,99 @@ fn cmd_trash_empty() -> Result<()> {
eprintln!("Emptied trash: {} item(s)", purged_titles.len()); eprintln!("Emptied trash: {} item(s)", purged_titles.len());
Ok(()) Ok(())
} }
fn cmd_attach(_q: String, _file: PathBuf) -> Result<()> { bail!("not yet implemented"); } fn cmd_attach(query: String, file: PathBuf) -> Result<()> {
fn cmd_attachments(_q: String) -> Result<()> { bail!("not yet implemented"); } use std::fs;
fn cmd_extract(_q: String, _aid: String, _out: Option<PathBuf>) -> Result<()> { bail!("not yet implemented"); } use relicario_core::{encrypt_attachment, AttachmentRef};
use relicario_core::time::now_unix;
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)?;
let settings = vault.load_settings()?;
let caps = settings.attachment_caps;
if item.attachments.len() as u32 >= caps.per_item_max_count {
anyhow::bail!("item already has {} attachments (max {})",
item.attachments.len(), caps.per_item_max_count);
}
let bytes = fs::read(&file)
.with_context(|| format!("failed to read {}", file.display()))?;
let enc = encrypt_attachment(&bytes, vault.key(), caps.per_attachment_max_bytes)?;
let filename = file.file_name()
.ok_or_else(|| anyhow::anyhow!("file path has no filename: {}", file.display()))?
.to_string_lossy()
.into_owned();
let mime_type = guess_mime(&filename);
let aref = AttachmentRef {
id: enc.id.clone(),
filename,
mime_type,
size: bytes.len() as u64,
created: now_unix(),
};
let att_dir = vault.root().join("attachments").join(item.id.as_str());
fs::create_dir_all(&att_dir)?;
fs::write(att_dir.join(format!("{}.enc", enc.id.as_str())), &enc.bytes)?;
item.attachments.push(aref);
item.modified = now_unix();
vault.save_item(&item)?;
manifest.upsert(&item);
vault.save_manifest(&manifest)?;
let paths = [
format!("items/{}.enc", item.id.as_str()),
"manifest.enc".into(),
format!("attachments/{}/{}.enc", item.id.as_str(), enc.id.as_str()),
];
let path_refs: Vec<&str> = paths.iter().map(|s| s.as_str()).collect();
commit_paths(&vault, &format!("attach: {}{} ({})",
file.display(), item.title, item.id.as_str()), &path_refs)?;
eprintln!("Attached {} to {} (aid={})", file.display(), item.title, enc.id.as_str());
Ok(())
}
fn cmd_attachments(query: String) -> Result<()> {
let vault = crate::session::UnlockedVault::unlock_interactive()?;
let manifest = vault.load_manifest()?;
let entry = resolve_query(&manifest, &query)?;
let item = vault.load_item(&entry.id)?;
if item.attachments.is_empty() { eprintln!("(no attachments)"); return Ok(()); }
println!("{:<17} {:>12} {:<22} {}", "AID", "SIZE", "MIME", "FILENAME");
for a in &item.attachments {
println!("{:<17} {:>12} {:<22} {}", a.id.as_str(), a.size, a.mime_type, a.filename);
}
Ok(())
}
fn cmd_extract(query: String, aid: String, out: Option<PathBuf>) -> Result<()> {
use std::fs;
use relicario_core::decrypt_attachment;
let vault = crate::session::UnlockedVault::unlock_interactive()?;
let manifest = vault.load_manifest()?;
let entry = resolve_query(&manifest, &query)?;
let item = vault.load_item(&entry.id)?;
let aref = item.attachments.iter().find(|a| a.id.as_str() == aid)
.ok_or_else(|| anyhow::anyhow!("no attachment {aid} on {}", item.title))?;
let path = vault.root().join("attachments").join(item.id.as_str())
.join(format!("{}.enc", aid));
let bytes = fs::read(&path)
.with_context(|| format!("failed to read {}", path.display()))?;
let plaintext = decrypt_attachment(&bytes, vault.key())?;
let out_path = out.unwrap_or_else(|| PathBuf::from(&aref.filename));
fs::write(&out_path, plaintext.as_slice())
.with_context(|| format!("failed to write {}", out_path.display()))?;
eprintln!("Wrote {} bytes to {}", plaintext.len(), out_path.display());
Ok(())
}
fn cmd_generate(_l: u32, _b: bool, _w: u32, _s: String, _sep: String) -> Result<()> { bail!("not yet implemented"); } fn cmd_generate(_l: u32, _b: bool, _w: u32, _s: String, _sep: String) -> Result<()> { bail!("not yet implemented"); }
fn cmd_settings(_a: SettingsAction) -> Result<()> { bail!("not yet implemented"); } fn cmd_settings(_a: SettingsAction) -> Result<()> { bail!("not yet implemented"); }
fn cmd_sync() -> Result<()> { bail!("not yet implemented"); } fn cmd_sync() -> Result<()> { bail!("not yet implemented"); }