use clap::{Args, Subcommand};
use chrono::{TimeZone, Utc};
use serde::Serialize;
use crate::commands::Output;
use crate::error::Result;
use crate::vault::VaultManager;
#[derive(Args)]
pub struct SnapshotArgs {
#[command(subcommand)]
pub command: SnapshotCommand,
}
#[derive(Subcommand)]
pub enum SnapshotCommand {
Save {
name: Option<String>,
#[arg(short, long)]
description: Option<String>,
},
List,
Restore {
name: String,
},
Delete {
name: String,
},
Info {
name: String,
},
}
#[derive(Serialize)]
struct SnapshotOutput {
name: String,
created_at: String,
file_count: u64,
total_size: u64,
total_size_human: String,
description: Option<String>,
}
impl std::fmt::Display for SnapshotOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Snapshot: {}", self.name)?;
writeln!(f, "Created: {}", self.created_at)?;
writeln!(f, "Files: {}", self.file_count)?;
writeln!(f, "Size: {}", self.total_size_human)?;
if let Some(desc) = &self.description {
writeln!(f, "Description: {}", desc)?;
}
Ok(())
}
}
#[derive(Serialize)]
struct SnapshotListOutput {
snapshots: Vec<SnapshotOutput>,
}
impl std::fmt::Display for SnapshotListOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.snapshots.is_empty() {
writeln!(f, "No snapshots")?;
return Ok(());
}
writeln!(f, "{:<20} {:<20} {:>8} {:>12}", "NAME", "CREATED", "FILES", "SIZE")?;
writeln!(f, "{}", "-".repeat(64))?;
for snap in &self.snapshots {
writeln!(
f,
"{:<20} {:<20} {:>8} {:>12}",
snap.name, snap.created_at, snap.file_count, snap.total_size_human
)?;
}
Ok(())
}
}
#[derive(Serialize)]
struct RestoreOutput {
snapshot: String,
files_restored: u64,
dirs_restored: u64,
}
impl std::fmt::Display for RestoreOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Restored snapshot '{}': {} files, {} directories",
self.snapshot, self.files_restored, self.dirs_restored
)
}
}
fn format_size(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} bytes", bytes)
}
}
fn generate_snapshot_name() -> String {
let now = Utc::now();
now.format("snap-%Y%m%d-%H%M%S").to_string()
}
pub fn run(args: SnapshotArgs, output: &Output, vault: Option<String>) -> Result<()> {
let manager = VaultManager::new()?;
let backend = match vault {
Some(name) => manager.open(&name)?,
None => manager.open_current()?,
};
match args.command {
SnapshotCommand::Save { name, description } => {
let snap_name = name.unwrap_or_else(generate_snapshot_name);
let info = backend.save_snapshot(&snap_name, description.as_deref())?;
let details = serde_json::json!({
"file_count": info.file_count,
"total_size": info.total_size
});
let _ = backend.log_operation("snapshot_save", None, Some(&details.to_string()));
let ts = Utc.timestamp_opt(info.created_at, 0)
.single()
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
.unwrap_or_else(|| info.created_at.to_string());
let result = SnapshotOutput {
name: info.name,
created_at: ts,
file_count: info.file_count,
total_size: info.total_size,
total_size_human: format_size(info.total_size),
description: info.description,
};
if output.is_json() {
output.print_json(&result);
} else {
println!("Saved snapshot: {}", result.name);
}
}
SnapshotCommand::List => {
let snapshots = backend.list_snapshots()?;
let output_snapshots: Vec<SnapshotOutput> = snapshots
.into_iter()
.map(|s| {
let ts = Utc.timestamp_opt(s.created_at, 0)
.single()
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
.unwrap_or_else(|| s.created_at.to_string());
SnapshotOutput {
name: s.name,
created_at: ts,
file_count: s.file_count,
total_size: s.total_size,
total_size_human: format_size(s.total_size),
description: s.description,
}
})
.collect();
let result = SnapshotListOutput {
snapshots: output_snapshots,
};
output.print(&result);
}
SnapshotCommand::Restore { name } => {
let stats = backend.restore_snapshot(&name)?;
let details = serde_json::json!({
"files_restored": stats.files_restored,
"dirs_restored": stats.dirs_restored
});
let _ = backend.log_operation("snapshot_restore", None, Some(&details.to_string()));
let result = RestoreOutput {
snapshot: name,
files_restored: stats.files_restored,
dirs_restored: stats.dirs_restored,
};
output.print(&result);
}
SnapshotCommand::Delete { name } => {
backend.delete_snapshot(&name)?;
if output.is_json() {
output.print_json(&serde_json::json!({
"action": "delete",
"name": name
}));
} else {
println!("Deleted snapshot: {}", name);
}
}
SnapshotCommand::Info { name } => {
let info = backend.get_snapshot(&name)?;
let ts = Utc.timestamp_opt(info.created_at, 0)
.single()
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
.unwrap_or_else(|| info.created_at.to_string());
let result = SnapshotOutput {
name: info.name,
created_at: ts,
file_count: info.file_count,
total_size: info.total_size,
total_size_human: format_size(info.total_size),
description: info.description,
};
output.print(&result);
}
}
Ok(())
}