use serde::{Deserialize, Serialize};
use sochdb_core::{Result, SochDBError};
use std::fs::{self, File};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupMetadata {
pub timestamp_us: u64,
pub created_at: String,
pub size_bytes: u64,
pub file_count: usize,
pub database_version: String,
pub checksum: String,
pub source_path: String,
}
impl BackupMetadata {
pub fn generate_name(&self) -> String {
format!("sochdb-backup-{}", self.timestamp_us)
}
}
pub struct BackupManager {
source_path: PathBuf,
}
impl BackupManager {
pub fn new<P: AsRef<Path>>(source_path: P) -> Self {
Self {
source_path: source_path.as_ref().to_path_buf(),
}
}
pub fn create_backup<P: AsRef<Path>>(&self, destination: P) -> Result<BackupMetadata> {
let dest_path = destination.as_ref();
fs::create_dir_all(dest_path).map_err(|e| {
SochDBError::Backup(format!("Failed to create backup directory: {}", e))
})?;
let timestamp_us = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_micros() as u64;
let created_at = chrono::Local::now().to_rfc3339();
let files_to_backup = self.collect_files()?;
if files_to_backup.is_empty() {
return Err(SochDBError::Backup(
"No files found in source database".to_string(),
));
}
let mut total_size = 0u64;
let mut checksums = Vec::new();
for (rel_path, src_path) in &files_to_backup {
let dest_file_path = dest_path.join(rel_path);
if let Some(parent) = dest_file_path.parent() {
fs::create_dir_all(parent).map_err(|e| {
SochDBError::Backup(format!("Failed to create directory: {}", e))
})?;
}
fs::copy(src_path, &dest_file_path).map_err(|e| {
SochDBError::Backup(format!("Failed to copy file {}: {}", rel_path, e))
})?;
let metadata = fs::metadata(&dest_file_path)
.map_err(|e| SochDBError::Backup(format!("Failed to read file metadata: {}", e)))?;
total_size += metadata.len();
let checksum = self.calculate_file_checksum(&dest_file_path)?;
checksums.push(format!("{}:{}", rel_path, checksum));
}
let overall_checksum = self.calculate_string_checksum(&checksums.join("\n"));
let metadata = BackupMetadata {
timestamp_us,
created_at,
size_bytes: total_size,
file_count: files_to_backup.len(),
database_version: env!("CARGO_PKG_VERSION").to_string(),
checksum: overall_checksum,
source_path: self.source_path.display().to_string(),
};
let manifest_path = dest_path.join("manifest.json");
let manifest_json = serde_json::to_string_pretty(&metadata)
.map_err(|e| SochDBError::Backup(format!("Failed to serialize manifest: {}", e)))?;
fs::write(&manifest_path, manifest_json)
.map_err(|e| SochDBError::Backup(format!("Failed to write manifest: {}", e)))?;
Ok(metadata)
}
pub fn restore_backup<P: AsRef<Path>>(&self, backup_path: P) -> Result<BackupMetadata> {
let backup_path = backup_path.as_ref();
let manifest_path = backup_path.join("manifest.json");
let manifest_json = fs::read_to_string(&manifest_path)
.map_err(|e| SochDBError::Backup(format!("Failed to read manifest: {}", e)))?;
let metadata: BackupMetadata = serde_json::from_str(&manifest_json)
.map_err(|e| SochDBError::Backup(format!("Failed to parse manifest: {}", e)))?;
fs::create_dir_all(&self.source_path).map_err(|e| {
SochDBError::Backup(format!("Failed to create destination directory: {}", e))
})?;
let files = self.collect_backup_files(backup_path)?;
for (rel_path, src_path) in files {
let dest_path = self.source_path.join(&rel_path);
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent).map_err(|e| {
SochDBError::Backup(format!("Failed to create directory: {}", e))
})?;
}
fs::copy(&src_path, &dest_path).map_err(|e| {
SochDBError::Backup(format!("Failed to restore file {}: {}", rel_path, e))
})?;
}
Ok(metadata)
}
pub fn list_backups<P: AsRef<Path>>(backup_dir: P) -> Result<Vec<BackupMetadata>> {
let backup_dir = backup_dir.as_ref();
if !backup_dir.exists() {
return Ok(Vec::new());
}
let mut backups = Vec::new();
let entries = fs::read_dir(backup_dir)
.map_err(|e| SochDBError::Backup(format!("Failed to read backup directory: {}", e)))?;
for entry in entries {
let entry = entry.map_err(|e| {
SochDBError::Backup(format!("Failed to read directory entry: {}", e))
})?;
let path = entry.path();
if path.is_dir() {
let manifest_path = path.join("manifest.json");
if manifest_path.exists() {
match fs::read_to_string(&manifest_path) {
Ok(json) => {
if let Ok(metadata) = serde_json::from_str::<BackupMetadata>(&json) {
backups.push(metadata);
}
}
Err(_) => continue,
}
}
}
}
backups.sort_by(|a, b| b.timestamp_us.cmp(&a.timestamp_us));
Ok(backups)
}
pub fn verify_backup<P: AsRef<Path>>(backup_path: P) -> Result<bool> {
let backup_path = backup_path.as_ref();
let manifest_path = backup_path.join("manifest.json");
let manifest_json = fs::read_to_string(&manifest_path)
.map_err(|e| SochDBError::Backup(format!("Failed to read manifest: {}", e)))?;
let _metadata: BackupMetadata = serde_json::from_str(&manifest_json)
.map_err(|e| SochDBError::Backup(format!("Failed to parse manifest: {}", e)))?;
let manager = BackupManager::new(backup_path);
let files = manager.collect_backup_files(backup_path)?;
if files.is_empty() {
return Ok(false);
}
Ok(true)
}
fn collect_files(&self) -> Result<Vec<(String, PathBuf)>> {
let mut files = Vec::new();
if !self.source_path.exists() {
return Err(SochDBError::Backup(
"Source database path does not exist".to_string(),
));
}
Self::collect_files_recursive(&self.source_path, &self.source_path, &mut files)?;
Ok(files)
}
fn collect_files_recursive(
current_path: &Path,
base_path: &Path,
files: &mut Vec<(String, PathBuf)>,
) -> Result<()> {
let entries = fs::read_dir(current_path)
.map_err(|e| SochDBError::Backup(format!("Failed to read directory: {}", e)))?;
for entry in entries {
let entry =
entry.map_err(|e| SochDBError::Backup(format!("Failed to read entry: {}", e)))?;
let path = entry.path();
if path.is_dir() {
Self::collect_files_recursive(&path, base_path, files)?;
} else {
let rel_path = path
.strip_prefix(base_path)
.unwrap()
.to_string_lossy()
.to_string();
files.push((rel_path, path));
}
}
Ok(())
}
fn collect_backup_files(&self, backup_path: &Path) -> Result<Vec<(String, PathBuf)>> {
let mut files = Vec::new();
Self::collect_backup_files_recursive(backup_path, backup_path, &mut files)?;
files.retain(|(rel_path, _)| rel_path != "manifest.json");
Ok(files)
}
fn collect_backup_files_recursive(
current_path: &Path,
base_path: &Path,
files: &mut Vec<(String, PathBuf)>,
) -> Result<()> {
let entries = fs::read_dir(current_path)
.map_err(|e| SochDBError::Backup(format!("Failed to read directory: {}", e)))?;
for entry in entries {
let entry =
entry.map_err(|e| SochDBError::Backup(format!("Failed to read entry: {}", e)))?;
let path = entry.path();
if path.is_dir() {
Self::collect_backup_files_recursive(&path, base_path, files)?;
} else {
let rel_path = path
.strip_prefix(base_path)
.unwrap()
.to_string_lossy()
.to_string();
files.push((rel_path, path));
}
}
Ok(())
}
fn calculate_file_checksum(&self, path: &Path) -> Result<String> {
use sha2::{Digest, Sha256};
let mut file = File::open(path)
.map_err(|e| SochDBError::Backup(format!("Failed to open file for checksum: {}", e)))?;
let mut hasher = Sha256::new();
let mut buffer = [0u8; 8192];
loop {
let n = file.read(&mut buffer).map_err(|e| {
SochDBError::Backup(format!("Failed to read file for checksum: {}", e))
})?;
if n == 0 {
break;
}
hasher.update(&buffer[..n]);
}
Ok(format!("{:x}", hasher.finalize()))
}
fn calculate_string_checksum(&self, data: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(data.as_bytes());
format!("{:x}", hasher.finalize())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_create_and_restore_backup() {
let db_dir = TempDir::new().unwrap();
let db_path = db_dir.path();
fs::write(db_path.join("test.sst"), b"test data").unwrap();
fs::write(db_path.join("wal.log"), b"wal data").unwrap();
fs::create_dir_all(db_path.join("subdir")).unwrap();
fs::write(db_path.join("subdir").join("index.dat"), b"index data").unwrap();
let backup_dir = TempDir::new().unwrap();
let backup_path = backup_dir.path().join("backup-1");
let manager = BackupManager::new(db_path);
let metadata = manager.create_backup(&backup_path).unwrap();
assert_eq!(metadata.file_count, 3);
assert!(metadata.size_bytes > 0);
assert!(backup_path.join("manifest.json").exists());
assert!(backup_path.join("test.sst").exists());
let restore_dir = TempDir::new().unwrap();
let restore_path = restore_dir.path().join("restored");
let restore_manager = BackupManager::new(&restore_path);
let restored_metadata = restore_manager.restore_backup(&backup_path).unwrap();
assert_eq!(restored_metadata.file_count, metadata.file_count);
assert!(restore_path.join("test.sst").exists());
assert!(restore_path.join("wal.log").exists());
assert!(restore_path.join("subdir").join("index.dat").exists());
let content = fs::read_to_string(restore_path.join("test.sst")).unwrap();
assert_eq!(content, "test data");
}
#[test]
fn test_list_backups() {
let backup_dir = TempDir::new().unwrap();
let backup_path = backup_dir.path();
let db_dir = TempDir::new().unwrap();
fs::write(db_dir.path().join("test.sst"), b"data").unwrap();
let manager = BackupManager::new(db_dir.path());
let backup1 = backup_path.join("backup-1");
let backup2 = backup_path.join("backup-2");
manager.create_backup(&backup1).unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
manager.create_backup(&backup2).unwrap();
let backups = BackupManager::list_backups(backup_path).unwrap();
assert_eq!(backups.len(), 2);
assert!(backups[0].timestamp_us > backups[1].timestamp_us);
}
#[test]
fn test_verify_backup() {
let db_dir = TempDir::new().unwrap();
fs::write(db_dir.path().join("test.sst"), b"data").unwrap();
let backup_dir = TempDir::new().unwrap();
let backup_path = backup_dir.path().join("backup");
let manager = BackupManager::new(db_dir.path());
manager.create_backup(&backup_path).unwrap();
let valid = BackupManager::verify_backup(&backup_path).unwrap();
assert!(valid);
}
}