diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index f0700e1..4b9db4c 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -1440,13 +1440,103 @@ fn tar_directory(dir: &std::path::Path) -> Result> { 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<()> {