use super::{ToolResult, ToolStats};
use std::fs::{self, File};
use std::io::{self, BufReader, BufWriter, Read, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
pub struct BackupConfig {
pub source: PathBuf,
pub target: PathBuf,
pub compress: bool,
pub incremental: bool,
pub verify: bool,
pub include_metadata: bool,
}
pub struct RestoreConfig {
pub source: PathBuf,
pub target: PathBuf,
pub verify: bool,
pub overwrite: bool,
}
#[derive(Debug)]
struct BackupMetadata {
timestamp: SystemTime,
source_path: PathBuf,
file_count: usize,
total_size: u64,
compressed: bool,
incremental: bool,
}
pub async fn run(
source: PathBuf,
target: PathBuf,
compress: bool,
incremental: bool,
) -> ToolResult {
let config = BackupConfig {
source,
target,
compress,
incremental,
verify: true,
include_metadata: true,
};
backup(config).await
}
pub async fn backup(config: BackupConfig) -> ToolResult {
let mut stats = ToolStats::new();
println!("OxiRS Database Backup");
println!("=====================\n");
if !config.source.exists() {
return Err(format!(
"Source directory does not exist: {}",
config.source.display()
)
.into());
}
if !config.source.is_dir() {
return Err(format!("Source must be a directory: {}", config.source.display()).into());
}
println!("Source: {}", config.source.display());
println!("Target: {}", config.target.display());
println!(
"Mode: {}",
if config.incremental {
"Incremental"
} else {
"Full"
}
);
println!(
"Compression: {}\n",
if config.compress { "Yes" } else { "No" }
);
if let Some(parent) = config.target.parent() {
fs::create_dir_all(parent)?;
}
let files = collect_files(&config.source)?;
println!("Found {} file(s) to backup", files.len());
let total_size: u64 = files
.iter()
.map(|f| f.metadata().ok().map(|m| m.len()).unwrap_or(0))
.sum();
println!("Total size: {:.2} MB\n", total_size as f64 / 1_048_576.0);
let backup_start = std::time::Instant::now();
if config.compress {
backup_compressed(&files, &config.source, &config.target)?;
} else {
backup_uncompressed(&files, &config.source, &config.target)?;
}
let backup_time = backup_start.elapsed();
if config.include_metadata {
write_backup_metadata(&config, &files, total_size)?;
}
if config.verify {
println!("\nVerifying backup...");
verify_backup(&config.source, &config.target, config.compress)?;
println!("✓ Backup verified successfully");
}
println!("\nBackup completed in {:.2}s", backup_time.as_secs_f64());
println!("Backup saved to: {}", config.target.display());
stats.items_processed = files.len();
stats.finish();
stats.print_summary("Backup");
Ok(())
}
pub async fn restore(config: RestoreConfig) -> ToolResult {
let mut stats = ToolStats::new();
println!("OxiRS Database Restore");
println!("======================\n");
if !config.source.exists() {
return Err(format!("Backup file does not exist: {}", config.source.display()).into());
}
println!("Source: {}", config.source.display());
println!("Target: {}", config.target.display());
if config.target.exists() && !config.overwrite {
return Err(format!(
"Target directory already exists: {}. Use --overwrite to replace",
config.target.display()
)
.into());
}
fs::create_dir_all(&config.target)?;
let is_compressed = config.source.extension().and_then(|s| s.to_str()) == Some("tar")
|| config.source.extension().and_then(|s| s.to_str()) == Some("gz");
println!(
"Format: {}\n",
if is_compressed {
"Compressed"
} else {
"Uncompressed"
}
);
let restore_start = std::time::Instant::now();
if is_compressed {
restore_compressed(&config.source, &config.target)?;
} else {
restore_uncompressed(&config.source, &config.target)?;
}
let restore_time = restore_start.elapsed();
if config.verify {
println!("\nVerifying restore...");
verify_restore(&config.target)?;
println!("✓ Restore verified successfully");
}
println!("\nRestore completed in {:.2}s", restore_time.as_secs_f64());
println!("Database restored to: {}", config.target.display());
stats.finish();
stats.print_summary("Restore");
Ok(())
}
fn collect_files(dir: &Path) -> ToolResult<Vec<PathBuf>> {
let mut files = Vec::new();
fn collect_recursive(dir: &Path, files: &mut Vec<PathBuf>) -> ToolResult<()> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
collect_recursive(&path, files)?;
} else if path.is_file() {
files.push(path);
}
}
Ok(())
}
collect_recursive(dir, &mut files)?;
Ok(files)
}
fn backup_uncompressed(files: &[PathBuf], source_root: &Path, target: &Path) -> ToolResult<()> {
fs::create_dir_all(target)?;
for file in files {
let rel_path = file
.strip_prefix(source_root)
.map_err(|e| format!("Failed to get relative path: {}", e))?;
let target_file = target.join(rel_path);
if let Some(parent) = target_file.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(file, &target_file)?;
print!(".");
io::stdout().flush()?;
}
println!();
Ok(())
}
fn backup_compressed(files: &[PathBuf], source_root: &Path, target: &Path) -> ToolResult<()> {
let target_file = if target.extension().is_some() {
target.to_path_buf()
} else {
target.with_extension("tar")
};
let file = File::create(&target_file)?;
let mut writer = BufWriter::new(file);
for file_path in files {
let rel_path = file_path
.strip_prefix(source_root)
.map_err(|e| format!("Failed to get relative path: {}", e))?;
let mut file = File::open(file_path)?;
let metadata = file.metadata()?;
let size = metadata.len();
let path_str = rel_path.to_string_lossy();
let path_bytes = path_str.as_bytes();
writer.write_all(&(path_bytes.len() as u32).to_le_bytes())?;
writer.write_all(path_bytes)?;
writer.write_all(&size.to_le_bytes())?;
let mut buffer = vec![0u8; 8192];
loop {
let n = file.read(&mut buffer)?;
if n == 0 {
break;
}
writer.write_all(&buffer[..n])?;
}
print!(".");
io::stdout().flush()?;
}
writer.flush()?;
println!();
Ok(())
}
fn restore_uncompressed(source: &Path, target: &Path) -> ToolResult<()> {
copy_dir_recursive(source, target)?;
Ok(())
}
fn restore_compressed(source: &Path, target: &Path) -> ToolResult<()> {
let file = File::open(source)?;
let mut reader = BufReader::new(file);
loop {
let mut len_bytes = [0u8; 4];
match reader.read_exact(&mut len_bytes) {
Ok(_) => {}
Err(ref e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e.into()),
}
let path_len = u32::from_le_bytes(len_bytes) as usize;
let mut path_bytes = vec![0u8; path_len];
reader.read_exact(&mut path_bytes)?;
let rel_path =
String::from_utf8(path_bytes).map_err(|e| format!("Invalid UTF-8 in path: {}", e))?;
let mut size_bytes = [0u8; 8];
reader.read_exact(&mut size_bytes)?;
let size = u64::from_le_bytes(size_bytes);
let target_file = target.join(&rel_path);
if let Some(parent) = target_file.parent() {
fs::create_dir_all(parent)?;
}
let mut file = File::create(&target_file)?;
let mut remaining = size;
let mut buffer = vec![0u8; 8192];
while remaining > 0 {
let to_read = remaining.min(buffer.len() as u64) as usize;
reader.read_exact(&mut buffer[..to_read])?;
file.write_all(&buffer[..to_read])?;
remaining -= to_read as u64;
}
print!(".");
io::stdout().flush()?;
}
println!();
Ok(())
}
fn copy_dir_recursive(source: &Path, target: &Path) -> ToolResult<()> {
fs::create_dir_all(target)?;
for entry in fs::read_dir(source)? {
let entry = entry?;
let file_type = entry.file_type()?;
let source_path = entry.path();
let target_path = target.join(entry.file_name());
if file_type.is_dir() {
copy_dir_recursive(&source_path, &target_path)?;
} else if file_type.is_file() {
fs::copy(&source_path, &target_path)?;
}
}
Ok(())
}
fn verify_backup(source: &Path, target: &Path, _compressed: bool) -> ToolResult<()> {
let source_files = collect_files(source)?;
let source_size: u64 = source_files
.iter()
.map(|f| f.metadata().ok().map(|m| m.len()).unwrap_or(0))
.sum();
if target.is_file() {
let backup_size = target.metadata()?.len();
if backup_size == 0 {
return Err("Backup file is empty".into());
}
println!(" Source: {:.2} MB", source_size as f64 / 1_048_576.0);
println!(" Backup: {:.2} MB", backup_size as f64 / 1_048_576.0);
} else {
for source_file in &source_files {
let rel_path = source_file
.strip_prefix(source)
.map_err(|e| format!("Failed to get relative path: {}", e))?;
let target_file = target.join(rel_path);
if !target_file.exists() {
return Err(format!("File missing in backup: {}", rel_path.display()).into());
}
}
println!(" {} file(s) verified", source_files.len());
}
Ok(())
}
fn verify_restore(target: &Path) -> ToolResult<()> {
if !target.exists() {
return Err("Restore target does not exist".into());
}
if !target.is_dir() {
return Err("Restore target is not a directory".into());
}
let files = collect_files(target)?;
if files.is_empty() {
return Err("No files found in restore target".into());
}
println!(" {} file(s) restored", files.len());
Ok(())
}
fn write_backup_metadata(
config: &BackupConfig,
files: &[PathBuf],
total_size: u64,
) -> ToolResult<()> {
let metadata = BackupMetadata {
timestamp: SystemTime::now(),
source_path: config.source.clone(),
file_count: files.len(),
total_size,
compressed: config.compress,
incremental: config.incremental,
};
let metadata_path = if config.target.is_dir() {
config.target.join("backup_metadata.txt")
} else {
config.target.with_extension("metadata.txt")
};
let mut file = File::create(metadata_path)?;
writeln!(file, "OxiRS Backup Metadata")?;
writeln!(file, "====================")?;
writeln!(file, "Timestamp: {:?}", metadata.timestamp)?;
writeln!(file, "Source: {}", metadata.source_path.display())?;
writeln!(file, "Files: {}", metadata.file_count)?;
writeln!(file, "Size: {} bytes", metadata.total_size)?;
writeln!(file, "Compressed: {}", metadata.compressed)?;
writeln!(file, "Incremental: {}", metadata.incremental)?;
Ok(())
}