use std::path::{Path, PathBuf};
pub(crate) fn cmd_snapshot_save(name: &str, state_dir: &Path) -> Result<(), String> {
if !state_dir.exists() {
return Err(format!(
"state directory does not exist: {}",
state_dir.display()
));
}
let snap_dir = snapshots_dir(state_dir).join(name);
if snap_dir.exists() {
return Err(format!("snapshot '{name}' already exists"));
}
std::fs::create_dir_all(&snap_dir).map_err(|e| format!("cannot create snapshot dir: {e}"))?;
copy_dir_recursive(state_dir, &snap_dir, "snapshots")?;
let meta = format!(
"created_at: \"{}\"\nname: \"{}\"\n",
crate::tripwire::eventlog::now_iso8601(),
name
);
std::fs::write(snap_dir.join(".snapshot.yaml"), meta)
.map_err(|e| format!("cannot write snapshot metadata: {e}"))?;
println!("Snapshot saved: {name}");
Ok(())
}
pub(crate) fn cmd_snapshot_list(state_dir: &Path, json: bool) -> Result<(), String> {
let snap_base = snapshots_dir(state_dir);
if !snap_base.exists() {
if json {
println!("[]");
} else {
println!("No snapshots.");
}
return Ok(());
}
let mut snapshots: Vec<(String, String)> = Vec::new();
let entries =
std::fs::read_dir(&snap_base).map_err(|e| format!("cannot read snapshots dir: {e}"))?;
for entry in entries.flatten() {
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
let name = entry.file_name().to_string_lossy().to_string();
let meta_path = entry.path().join(".snapshot.yaml");
let created = if meta_path.exists() {
std::fs::read_to_string(&meta_path)
.ok()
.and_then(|c| {
c.lines().find(|l| l.starts_with("created_at:")).map(|l| {
l.trim_start_matches("created_at:")
.trim()
.trim_matches('"')
.to_string()
})
})
.unwrap_or_else(|| "unknown".to_string())
} else {
"unknown".to_string()
};
snapshots.push((name, created));
}
}
snapshots.sort_by(|a, b| a.0.cmp(&b.0));
if json {
let items: Vec<serde_json::Value> = snapshots
.iter()
.map(|(n, c)| serde_json::json!({"name": n, "created_at": c}))
.collect();
println!(
"{}",
serde_json::to_string_pretty(&items).map_err(|e| format!("JSON error: {e}"))?
);
} else if snapshots.is_empty() {
println!("No snapshots.");
} else {
for (name, created) in &snapshots {
println!(" {name} ({created})");
}
}
Ok(())
}
pub(crate) fn cmd_snapshot_restore(name: &str, state_dir: &Path, yes: bool) -> Result<(), String> {
let snap_dir = snapshots_dir(state_dir).join(name);
if !snap_dir.exists() {
return Err(format!("snapshot '{name}' does not exist"));
}
if !yes {
return Err("Restore will overwrite current state. Use --yes to confirm.".to_string());
}
let entries =
std::fs::read_dir(state_dir).map_err(|e| format!("cannot read state dir: {e}"))?;
for entry in entries.flatten() {
let name_os = entry.file_name();
if name_os.to_string_lossy() == "snapshots" {
continue;
}
let path = entry.path();
if path.is_dir() {
std::fs::remove_dir_all(&path)
.map_err(|e| format!("cannot remove {}: {}", path.display(), e))?;
} else {
std::fs::remove_file(&path)
.map_err(|e| format!("cannot remove {}: {}", path.display(), e))?;
}
}
copy_dir_recursive(&snap_dir, state_dir, ".snapshot.yaml")?;
println!("Restored snapshot: {name}");
Ok(())
}
pub(crate) fn cmd_snapshot_delete(name: &str, state_dir: &Path) -> Result<(), String> {
let snap_dir = snapshots_dir(state_dir).join(name);
if !snap_dir.exists() {
return Err(format!("snapshot '{name}' does not exist"));
}
std::fs::remove_dir_all(&snap_dir).map_err(|e| format!("cannot delete snapshot: {e}"))?;
println!("Deleted snapshot: {name}");
Ok(())
}
pub(crate) fn snapshots_dir(state_dir: &Path) -> PathBuf {
state_dir.join("snapshots")
}
pub(crate) fn copy_dir_recursive(src: &Path, dst: &Path, skip: &str) -> Result<(), String> {
let entries =
std::fs::read_dir(src).map_err(|e| format!("cannot read {}: {}", src.display(), e))?;
for entry in entries.flatten() {
let name = entry.file_name();
if name.to_string_lossy() == skip {
continue;
}
let src_path = entry.path();
let dst_path = dst.join(&name);
if src_path.is_dir() {
std::fs::create_dir_all(&dst_path)
.map_err(|e| format!("cannot create {}: {}", dst_path.display(), e))?;
copy_dir_recursive(&src_path, &dst_path, "")?;
} else {
std::fs::copy(&src_path, &dst_path).map_err(|e| {
format!(
"cannot copy {} → {}: {}",
src_path.display(),
dst_path.display(),
e
)
})?;
}
}
Ok(())
}