feat(cli): cmd_backup_restore — unpack .relbak into target dir

Refuses non-empty target, prompts for backup passphrase, writes the
full vault layout, untars .git/ when bundled or git-inits a fresh
'restore from backup <iso8601>' commit otherwise.

Also tightens error context on tar_directory's builder.finish().
This commit is contained in:
adlee-was-taken
2026-04-28 19:25:45 -04:00
parent 7ce57353f2
commit 734325a31f

View File

@@ -1440,13 +1440,103 @@ fn tar_directory(dir: &std::path::Path) -> Result<Vec<u8>> {
let mut builder = tar::Builder::new(&mut buf);
builder.append_dir_all(".", dir)
.with_context(|| format!("failed to tar {}", dir.display()))?;
builder.finish()?;
builder.finish().with_context(|| "failed to finalize git tar")?;
}
Ok(buf)
}
fn cmd_backup_restore(_input: PathBuf, _target: PathBuf) -> Result<()> {
anyhow::bail!("cmd_backup_restore not yet implemented")
fn cmd_backup_restore(input: PathBuf, target: PathBuf) -> Result<()> {
use std::fs;
use relicario_core::backup;
use zeroize::Zeroizing;
let target = if target.is_absolute() {
target
} else {
std::env::current_dir()?.join(&target)
};
if target.join(".relicario").exists() {
anyhow::bail!(
"target dir already contains a relicario vault; restore refuses to overwrite — use an empty directory: {}",
target.display()
);
}
fs::create_dir_all(&target)
.with_context(|| format!("failed to create target {}", target.display()))?;
// Read input file.
let bytes = fs::read(&input)
.with_context(|| format!("failed to read backup file {}", input.display()))?;
// Backup passphrase prompt.
let passphrase = if let Ok(p) = std::env::var("RELICARIO_TEST_BACKUP_PASSPHRASE") {
Zeroizing::new(p)
} else {
Zeroizing::new(rpassword::prompt_password("Backup passphrase: ")?)
};
let unpacked = backup::unpack_backup(&bytes, &passphrase)
.map_err(|e| match e {
relicario_core::RelicarioError::Decrypt =>
anyhow::anyhow!("wrong backup passphrase, or the file is corrupt"),
other => anyhow::anyhow!(other),
})?;
// Write vault layout.
let relicario_dir = target.join(".relicario");
fs::create_dir_all(&relicario_dir)?;
fs::create_dir_all(target.join("items"))?;
fs::create_dir_all(target.join("attachments"))?;
fs::write(relicario_dir.join("salt"), unpacked.salt)?;
fs::write(relicario_dir.join("params.json"), &unpacked.params_json)?;
fs::write(relicario_dir.join("devices.json"), &unpacked.devices_json)?;
fs::write(target.join("manifest.enc"), &unpacked.manifest_enc)?;
fs::write(target.join("settings.enc"), &unpacked.settings_enc)?;
for item in &unpacked.items {
fs::write(target.join("items").join(format!("{}.enc", item.id)), &item.ciphertext)?;
}
for a in &unpacked.attachments {
let dir = target.join("attachments").join(&a.item_id);
fs::create_dir_all(&dir)?;
fs::write(dir.join(format!("{}.enc", a.attachment_id)), &a.ciphertext)?;
}
// Reference image (if present).
if let Some(jpg) = &unpacked.reference_jpg {
let path = target.join("reference.jpg");
fs::write(&path, jpg)
.with_context(|| format!("failed to write reference image {}", path.display()))?;
}
// .git/ history.
if let Some(tar_bytes) = &unpacked.git_archive {
let mut archive = tar::Archive::new(tar_bytes.as_slice());
archive.unpack(target.join(".git"))
.with_context(|| "failed to untar .git/")?;
} else {
// No history bundled — start a fresh git repo.
let status = crate::helpers::git_command(&target, &["init"]).status()?;
if !status.success() { anyhow::bail!("git init failed"); }
// .gitignore — exclude reference image if present.
if target.join("reference.jpg").exists() {
fs::write(target.join(".gitignore"), "reference.jpg\n")?;
}
let _ = crate::helpers::git_command(&target, &["add", "."]).status()?;
let now_iso = crate::helpers::iso8601(relicario_core::now_unix());
let msg = format!("restore from backup {now_iso}");
let _ = crate::helpers::git_command(&target, &["commit", "-m", &msg]).status()?;
}
eprintln!(
"Restored vault to {}. Unlock with your passphrase + reference image.",
target.display()
);
Ok(())
}
fn cmd_trash_empty() -> Result<()> {