use std::collections::HashSet;
use crate::error::LorumError;
pub fn run_backup_list(_config_path: Option<&str>) -> Result<(), LorumError> {
let dir = crate::backup::backup_dir()?;
if !dir.exists() {
println!("no backups found");
return Ok(());
}
let mut tool_names: HashSet<String> = HashSet::new();
for item in std::fs::read_dir(&dir)?.filter_map(|e| e.ok()) {
let name = match item.path().file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_string(),
None => continue,
};
if let Some((tool, _)) = name.split_once('-') {
tool_names.insert(tool.to_string());
}
}
let mut entries: Vec<(String, crate::backup::BackupInfo)> = Vec::new();
for tool_name in &tool_names {
match crate::backup::list_backups(tool_name) {
Ok(list) => {
for info in list {
entries.push((tool_name.clone(), info));
}
}
Err(_) => continue,
}
}
if entries.is_empty() {
println!("no backups found");
return Ok(());
}
entries.sort_by(|a, b| b.1.timestamp.cmp(&a.1.timestamp));
println!("{:<15} {:<20} {:>10} FILE", "TOOL", "TIME", "SIZE");
for (tool, b) in &entries {
let size = format_size(b.size);
println!(
"{:<15} {:<20} {:>10} {}",
tool, b.time_display, size, b.name
);
}
Ok(())
}
fn format_size(size: u64) -> String {
if size == 0 {
return "0B".to_string();
}
const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
let mut idx = 0;
let mut s = size;
while s >= 1024 && idx < UNITS.len() - 1 {
s /= 1024;
idx += 1;
}
if idx == 0 {
format!("{}{}", s, UNITS[idx])
} else {
let divisor = 1024u64.pow(idx as u32);
let whole = size / divisor;
let rem = size % divisor;
let tenths = (rem * 10 + divisor / 2) / divisor;
if tenths == 0 {
format!("{}{}", whole, UNITS[idx])
} else if tenths >= 10 {
format!("{}.{}{}", whole + 1, 0, UNITS[idx])
} else {
format!("{}.{}{}", whole, tenths, UNITS[idx])
}
}
}
pub fn run_backup_create(
tools: &[String],
all: bool,
_config_path: Option<&str>,
) -> Result<(), LorumError> {
let tool_names: Vec<String> = if all || tools.is_empty() {
crate::adapters::all_adapters()
.iter()
.map(|a| a.name().to_string())
.collect()
} else {
tools.to_vec()
};
let mut created = 0;
for tool_name in &tool_names {
let adapter = match crate::adapters::find_adapter(tool_name) {
Some(a) => a,
None => {
eprintln!("warning: unknown tool '{tool_name}'");
continue;
}
};
for path in adapter.config_paths() {
if path.exists() {
match crate::backup::create_backup(tool_name, &path) {
Ok(backup_path) => {
println!("created backup: {}", backup_path.display());
created += 1;
}
Err(e) => {
eprintln!("warning: failed to backup {}: {e}", path.display());
}
}
}
}
}
println!("created {created} backup(s)");
Ok(())
}
pub fn run_backup_restore(
tool: &str,
backup: Option<&str>,
_config_path: Option<&str>,
) -> Result<(), LorumError> {
let adapter = crate::adapters::find_adapter(tool)
.ok_or_else(|| LorumError::AdapterNotFound { name: tool.into() })?;
let paths = adapter.config_paths();
if paths.is_empty() {
return Err(LorumError::Other {
message: format!("no config path for {tool}"),
});
}
if let Some(backup_file) = backup {
let backup_path = std::path::Path::new(backup_file);
let backup_path = if backup_path.is_relative() {
crate::backup::backup_dir()?.join(backup_file)
} else {
backup_path.to_path_buf()
};
if !backup_path.exists() {
return Err(LorumError::ConfigNotFound { path: backup_path });
}
let canonical_backup =
std::fs::canonicalize(&backup_path).map_err(|e| LorumError::Io { source: e })?;
let canonical_backup_dir = std::fs::canonicalize(crate::backup::backup_dir()?)
.map_err(|e| LorumError::Io { source: e })?;
if !canonical_backup.starts_with(&canonical_backup_dir) {
return Err(LorumError::Other {
message: format!(
"backup path '{}' is outside the backup directory",
backup_path.display()
),
});
}
crate::backup::restore_backup_from_path(&backup_path, &paths[0])?;
println!("restored {tool} from {}", backup_path.display());
} else {
crate::backup::restore_backup(tool, &paths[0])?;
println!("restored {tool} from latest backup");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_backup_create_empty_tools_all_false() {
let result = run_backup_create(&[], false, None);
assert!(result.is_ok());
}
#[test]
fn run_backup_create_with_specific_tools() {
let result = run_backup_create(&["nonexistent-tool-xyz".into()], false, None);
assert!(result.is_ok());
}
#[test]
fn format_size_zero() {
assert_eq!(format_size(0), "0B");
}
#[test]
fn format_size_bytes() {
assert_eq!(format_size(512), "512B");
}
#[test]
fn format_size_kb() {
assert_eq!(format_size(1024), "1KB");
}
#[test]
fn format_size_mb() {
assert_eq!(format_size(1_048_576), "1MB");
}
#[test]
fn format_size_gb() {
assert_eq!(format_size(1_073_741_824), "1GB");
}
#[test]
fn format_size_fractional() {
assert_eq!(format_size(1536), "1.5KB");
}
#[test]
fn format_size_large_gb() {
assert_eq!(format_size(2_147_483_648), "2GB");
}
}