use crate::error::{Result, TdbError};
use std::collections::{BTreeMap, HashMap};
use std::fs::{self, File};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BackupMetadata {
pub version: String,
pub backup_type: BackupType,
pub created_at: SystemTime,
pub source_path: String,
pub triple_count: usize,
pub dictionary_size: usize,
pub size_bytes: u64,
pub compressed: bool,
pub checksum: String,
pub parent_backup: Option<String>,
pub wal_lsn: Option<u64>,
pub file_manifest: FileManifest,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct FileManifest {
pub files: HashMap<String, FileMetadata>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FileMetadata {
pub size: u64,
pub modified: SystemTime,
pub checksum: u32,
pub compressed: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum BackupType {
Full,
Incremental,
}
#[derive(Debug, Clone)]
pub struct BackupConfig {
pub compress: bool,
pub verify: bool,
pub backup_type: BackupType,
}
impl Default for BackupConfig {
fn default() -> Self {
Self {
compress: true,
verify: true,
backup_type: BackupType::Full,
}
}
}
pub struct BackupManager {
config: BackupConfig,
}
impl BackupManager {
pub fn new(config: BackupConfig) -> Self {
Self { config }
}
pub fn create_backup(
&self,
source_dir: impl AsRef<Path>,
backup_dir: impl AsRef<Path>,
) -> Result<BackupMetadata> {
let source_dir = source_dir.as_ref();
let backup_dir = backup_dir.as_ref();
if !source_dir.exists() {
return Err(TdbError::Other(format!(
"Source directory does not exist: {:?}",
source_dir
)));
}
fs::create_dir_all(backup_dir).map_err(TdbError::Io)?;
let duration = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|e| TdbError::Other(format!("Time error: {}", e)))?;
let backup_name = format!(
"tdb_backup_{}_{}",
duration.as_secs(),
duration.subsec_nanos()
);
let backup_path = backup_dir.join(&backup_name);
self.copy_database_files(source_dir, &backup_path)?;
let size_bytes = self.calculate_directory_size(&backup_path)?;
let checksum = self.calculate_checksum(&backup_path)?;
let file_manifest = self.build_file_manifest(&backup_path)?;
let parent_backup = if self.config.backup_type == BackupType::Incremental {
self.find_latest_full_backup(backup_dir)?.and_then(|path| {
path.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string())
})
} else {
None
};
let metadata = BackupMetadata {
version: crate::VERSION.to_string(),
backup_type: self.config.backup_type,
created_at: SystemTime::now(),
source_path: source_dir.to_string_lossy().to_string(),
triple_count: 0, dictionary_size: 0, size_bytes,
compressed: self.config.compress,
checksum,
parent_backup,
wal_lsn: None, file_manifest,
};
self.save_metadata(&backup_path, &metadata)?;
if self.config.verify {
self.verify_backup(&backup_path, &metadata)?;
}
Ok(metadata)
}
pub fn restore_backup(
&self,
backup_dir: impl AsRef<Path>,
target_dir: impl AsRef<Path>,
) -> Result<()> {
let backup_dir = backup_dir.as_ref();
let target_dir = target_dir.as_ref();
let metadata = self.load_metadata(backup_dir)?;
self.verify_backup(backup_dir, &metadata)?;
fs::create_dir_all(target_dir).map_err(TdbError::Io)?;
self.copy_database_files(backup_dir, target_dir)?;
Ok(())
}
pub fn list_backups(&self, backup_dir: impl AsRef<Path>) -> Result<Vec<BackupMetadata>> {
let backup_dir = backup_dir.as_ref();
let mut backups = Vec::new();
if !backup_dir.exists() {
return Ok(backups);
}
for entry in fs::read_dir(backup_dir).map_err(TdbError::Io)? {
let entry = entry.map_err(TdbError::Io)?;
let path = entry.path();
if path.is_dir() {
if let Ok(metadata) = self.load_metadata(&path) {
backups.push(metadata);
}
}
}
backups.sort_by_key(|b| std::cmp::Reverse(b.created_at));
Ok(backups)
}
pub fn verify_backup(
&self,
backup_dir: impl AsRef<Path>,
metadata: &BackupMetadata,
) -> Result<bool> {
let backup_dir = backup_dir.as_ref();
let current_checksum = self.calculate_checksum(backup_dir)?;
if current_checksum != metadata.checksum {
return Err(TdbError::Other(format!(
"Backup verification failed: checksum mismatch (expected: {}, got: {})",
metadata.checksum, current_checksum
)));
}
Ok(true)
}
pub fn cleanup_old_backups(
&self,
backup_dir: impl AsRef<Path>,
keep_count: usize,
) -> Result<usize> {
let backup_dir = backup_dir.as_ref();
let backups = self.list_backups(backup_dir)?;
if backups.len() <= keep_count {
return Ok(0);
}
let keep_times: std::collections::HashSet<_> = backups
.iter()
.take(keep_count)
.map(|b| b.created_at)
.collect();
let mut removed = 0;
for entry in fs::read_dir(backup_dir).map_err(TdbError::Io)? {
let entry = entry.map_err(TdbError::Io)?;
let path = entry.path();
if path.is_dir() {
if let Ok(metadata) = self.load_metadata(&path) {
if !keep_times.contains(&metadata.created_at) {
fs::remove_dir_all(&path).map_err(TdbError::Io)?;
removed += 1;
}
}
}
}
Ok(removed)
}
#[allow(clippy::only_used_in_recursion)]
fn copy_database_files(&self, source: &Path, dest: &Path) -> Result<()> {
fs::create_dir_all(dest).map_err(TdbError::Io)?;
for entry in fs::read_dir(source).map_err(TdbError::Io)? {
let entry = entry.map_err(TdbError::Io)?;
let path = entry.path();
let file_name = path
.file_name()
.ok_or_else(|| TdbError::Other("Invalid file name".to_string()))?;
let dest_path = dest.join(file_name);
if path.is_file() {
fs::copy(&path, &dest_path).map_err(TdbError::Io)?;
} else if path.is_dir() {
self.copy_database_files(&path, &dest_path)?;
}
}
Ok(())
}
#[allow(clippy::only_used_in_recursion)]
fn calculate_directory_size(&self, dir: &Path) -> Result<u64> {
let mut total_size = 0u64;
for entry in fs::read_dir(dir).map_err(TdbError::Io)? {
let entry = entry.map_err(TdbError::Io)?;
let path = entry.path();
if path.is_file() {
let metadata = fs::metadata(&path).map_err(TdbError::Io)?;
total_size += metadata.len();
} else if path.is_dir() {
total_size += self.calculate_directory_size(&path)?;
}
}
Ok(total_size)
}
fn calculate_checksum(&self, dir: &Path) -> Result<String> {
let mut files = BTreeMap::new();
self.collect_files(dir, dir, &mut files)?;
let mut checksum_data = Vec::new();
for (rel_path, full_path) in files {
checksum_data.extend_from_slice(rel_path.as_bytes());
if let Ok(metadata) = fs::metadata(&full_path) {
checksum_data.extend_from_slice(&metadata.len().to_le_bytes());
}
}
let checksum = crc32fast::hash(&checksum_data);
Ok(format!("{:08x}", checksum))
}
#[allow(clippy::only_used_in_recursion)]
fn collect_files(
&self,
base_dir: &Path,
current_dir: &Path,
files: &mut BTreeMap<String, PathBuf>,
) -> Result<()> {
for entry in fs::read_dir(current_dir).map_err(TdbError::Io)? {
let entry = entry.map_err(TdbError::Io)?;
let path = entry.path();
if path.is_file() {
if path.file_name() == Some(std::ffi::OsStr::new("metadata.json")) {
continue;
}
if let Ok(rel_path) = path.strip_prefix(base_dir) {
files.insert(rel_path.to_string_lossy().to_string(), path.clone());
}
} else if path.is_dir() {
self.collect_files(base_dir, &path, files)?;
}
}
Ok(())
}
fn save_metadata(&self, backup_dir: &Path, metadata: &BackupMetadata) -> Result<()> {
let metadata_path = backup_dir.join("metadata.json");
let json = serde_json::to_string_pretty(metadata)
.map_err(|e| TdbError::Serialization(e.to_string()))?;
fs::write(metadata_path, json).map_err(TdbError::Io)?;
Ok(())
}
fn load_metadata(&self, backup_dir: &Path) -> Result<BackupMetadata> {
let metadata_path = backup_dir.join("metadata.json");
let json = fs::read_to_string(metadata_path).map_err(TdbError::Io)?;
serde_json::from_str(&json).map_err(|e| TdbError::Deserialization(e.to_string()))
}
fn build_file_manifest(&self, backup_dir: &Path) -> Result<FileManifest> {
let mut manifest = FileManifest {
files: HashMap::new(),
};
self.collect_file_metadata(backup_dir, backup_dir, &mut manifest)?;
Ok(manifest)
}
#[allow(clippy::only_used_in_recursion)]
fn collect_file_metadata(
&self,
base_dir: &Path,
current_dir: &Path,
manifest: &mut FileManifest,
) -> Result<()> {
for entry in fs::read_dir(current_dir).map_err(TdbError::Io)? {
let entry = entry.map_err(TdbError::Io)?;
let path = entry.path();
if path.is_file() {
if path.file_name() == Some(std::ffi::OsStr::new("metadata.json")) {
continue;
}
let metadata = fs::metadata(&path).map_err(TdbError::Io)?;
let rel_path = path
.strip_prefix(base_dir)
.map_err(|e| TdbError::Other(format!("Path strip error: {}", e)))?
.to_string_lossy()
.to_string();
let file_data = std::fs::read(&path).map_err(TdbError::Io)?;
let checksum = crc32fast::hash(&file_data);
manifest.files.insert(
rel_path,
FileMetadata {
size: metadata.len(),
modified: metadata
.modified()
.map_err(|e| TdbError::Other(format!("Modified time error: {}", e)))?,
checksum,
compressed: false, },
);
} else if path.is_dir() {
self.collect_file_metadata(base_dir, &path, manifest)?;
}
}
Ok(())
}
fn find_latest_full_backup(&self, backup_dir: impl AsRef<Path>) -> Result<Option<PathBuf>> {
let backup_dir = backup_dir.as_ref();
if !backup_dir.exists() {
return Ok(None);
}
for entry in fs::read_dir(backup_dir).map_err(TdbError::Io)? {
let entry = entry.map_err(TdbError::Io)?;
let path = entry.path();
if path.is_dir() {
if let Ok(metadata) = self.load_metadata(&path) {
if metadata.backup_type == BackupType::Full {
return Ok(Some(path));
}
}
}
}
Ok(None)
}
pub fn create_incremental_backup(
&mut self,
source_dir: impl AsRef<Path>,
backup_dir: impl AsRef<Path>,
) -> Result<BackupMetadata> {
self.config.backup_type = BackupType::Incremental;
let parent_backup_path = self.find_latest_full_backup(&backup_dir)?.ok_or_else(|| {
TdbError::Other("No full backup found. Create a full backup first.".to_string())
})?;
let parent_metadata = self.load_metadata(&parent_backup_path)?;
let current_manifest = self.build_file_manifest(source_dir.as_ref())?;
let changed_files =
self.find_changed_files(&parent_metadata.file_manifest, ¤t_manifest);
if changed_files.is_empty() {
return Err(TdbError::Other(
"No changes detected since last backup".to_string(),
));
}
let parent_backup_name = parent_backup_path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| TdbError::Other("Invalid parent backup path".to_string()))?
.to_string();
self.create_incremental_backup_from_changes(
source_dir.as_ref(),
backup_dir.as_ref(),
&changed_files,
Some(parent_backup_name),
current_manifest,
)
}
fn find_changed_files(
&self,
old_manifest: &FileManifest,
new_manifest: &FileManifest,
) -> Vec<String> {
let mut changed = Vec::new();
for (path, new_metadata) in &new_manifest.files {
match old_manifest.files.get(path) {
None => {
changed.push(path.clone());
}
Some(old_metadata) => {
if old_metadata.size != new_metadata.size
|| old_metadata.checksum != new_metadata.checksum
{
changed.push(path.clone());
}
}
}
}
changed
}
fn create_incremental_backup_from_changes(
&self,
source_dir: &Path,
backup_dir: &Path,
changed_files: &[String],
parent_backup: Option<String>,
file_manifest: FileManifest,
) -> Result<BackupMetadata> {
let duration = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|e| TdbError::Other(format!("Time error: {}", e)))?;
let backup_name = format!(
"tdb_backup_{}_{}",
duration.as_secs(),
duration.subsec_nanos()
);
let backup_path = backup_dir.join(&backup_name);
fs::create_dir_all(&backup_path).map_err(TdbError::Io)?;
for rel_path in changed_files {
let source_file = source_dir.join(rel_path);
let dest_file = backup_path.join(rel_path);
if let Some(parent) = dest_file.parent() {
fs::create_dir_all(parent).map_err(TdbError::Io)?;
}
fs::copy(&source_file, &dest_file).map_err(TdbError::Io)?;
}
let size_bytes = self.calculate_directory_size(&backup_path)?;
let checksum = self.calculate_checksum(&backup_path)?;
let metadata = BackupMetadata {
version: crate::VERSION.to_string(),
backup_type: BackupType::Incremental,
created_at: SystemTime::now(),
source_path: source_dir.to_string_lossy().to_string(),
triple_count: 0,
dictionary_size: 0,
size_bytes,
compressed: self.config.compress,
checksum,
parent_backup,
wal_lsn: None,
file_manifest,
};
self.save_metadata(&backup_path, &metadata)?;
Ok(metadata)
}
pub fn restore_incremental_backup(
&self,
backup_dir: impl AsRef<Path>,
target_dir: impl AsRef<Path>,
) -> Result<()> {
let backup_dir = backup_dir.as_ref();
let target_dir = target_dir.as_ref();
let backups_root = backup_dir
.parent()
.ok_or_else(|| TdbError::Other("Invalid backup directory".to_string()))?;
let backup_chain = self.build_backup_chain(backup_dir, backups_root)?;
fs::create_dir_all(target_dir).map_err(TdbError::Io)?;
for backup_path in backup_chain {
self.copy_database_files(&backup_path, target_dir)?;
}
Ok(())
}
fn build_backup_chain(
&self,
starting_backup_path: &Path,
backups_root: &Path,
) -> Result<Vec<PathBuf>> {
let mut chain = Vec::new();
let metadata = self.load_metadata(starting_backup_path)?;
if metadata.backup_type == BackupType::Full {
chain.push(starting_backup_path.to_path_buf());
return Ok(chain);
}
chain.push(starting_backup_path.to_path_buf());
let mut current = metadata;
let mut visited = std::collections::HashSet::new();
while let Some(parent_name) = ¤t.parent_backup {
if visited.contains(parent_name) {
return Err(TdbError::Other(
"Circular backup chain detected".to_string(),
));
}
visited.insert(parent_name.clone());
let parent_path = backups_root.join(parent_name);
current = self.load_metadata(&parent_path)?;
chain.push(parent_path);
if current.backup_type == BackupType::Full {
break;
}
}
chain.reverse();
Ok(chain)
}
}
impl Default for BackupManager {
fn default() -> Self {
Self::new(BackupConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_backup_creation() {
let temp_dir = env::temp_dir().join("oxirs_tdb_backup_test");
fs::remove_dir_all(&temp_dir).ok();
let source_dir = temp_dir.join("source");
let backup_dir = temp_dir.join("backups");
fs::create_dir_all(&source_dir).unwrap();
fs::write(source_dir.join("data.tdb"), b"test data").unwrap();
let manager = BackupManager::default();
let metadata = manager.create_backup(&source_dir, &backup_dir).unwrap();
assert_eq!(metadata.backup_type, BackupType::Full);
assert!(metadata.size_bytes > 0);
assert!(!metadata.checksum.is_empty());
fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_backup_restore() {
let temp_dir = env::temp_dir().join("oxirs_tdb_restore_test");
fs::remove_dir_all(&temp_dir).ok();
let source_dir = temp_dir.join("source");
let backup_dir = temp_dir.join("backups");
let restore_dir = temp_dir.join("restored");
fs::create_dir_all(&source_dir).unwrap();
fs::write(source_dir.join("data.tdb"), b"important data").unwrap();
let manager = BackupManager::default();
let metadata = manager.create_backup(&source_dir, &backup_dir).unwrap();
let backups = manager.list_backups(&backup_dir).unwrap();
assert_eq!(backups.len(), 1);
let backup_entries: Vec<_> = fs::read_dir(&backup_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.collect();
assert_eq!(backup_entries.len(), 1);
manager
.restore_backup(backup_entries[0].path(), &restore_dir)
.unwrap();
assert!(restore_dir.join("data.tdb").exists());
let restored_data = fs::read_to_string(restore_dir.join("data.tdb")).unwrap();
assert_eq!(restored_data, "important data");
fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_backup_verification() {
let temp_dir = env::temp_dir().join("oxirs_tdb_verify_test");
fs::remove_dir_all(&temp_dir).ok();
let source_dir = temp_dir.join("source");
let backup_dir = temp_dir.join("backups");
fs::create_dir_all(&source_dir).unwrap();
fs::write(source_dir.join("data.tdb"), b"verify me").unwrap();
let manager = BackupManager::default();
let metadata = manager.create_backup(&source_dir, &backup_dir).unwrap();
let backup_entries: Vec<_> = fs::read_dir(&backup_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.collect();
assert!(manager
.verify_backup(backup_entries[0].path(), &metadata)
.unwrap());
fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_list_backups() {
let temp_dir = env::temp_dir().join("oxirs_tdb_list_test");
fs::remove_dir_all(&temp_dir).ok();
let source_dir = temp_dir.join("source");
let backup_dir = temp_dir.join("backups");
fs::create_dir_all(&source_dir).unwrap();
fs::write(source_dir.join("data.tdb"), b"data").unwrap();
let manager = BackupManager::default();
manager.create_backup(&source_dir, &backup_dir).unwrap();
std::thread::sleep(std::time::Duration::from_millis(100));
manager.create_backup(&source_dir, &backup_dir).unwrap();
let backups = manager.list_backups(&backup_dir).unwrap();
assert_eq!(backups.len(), 2);
assert!(backups[0].created_at >= backups[1].created_at);
fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_cleanup_old_backups() {
let temp_dir = env::temp_dir().join("oxirs_tdb_cleanup_test");
fs::remove_dir_all(&temp_dir).ok();
let source_dir = temp_dir.join("source");
let backup_dir = temp_dir.join("backups");
fs::create_dir_all(&source_dir).unwrap();
fs::create_dir_all(&backup_dir).unwrap();
fs::write(source_dir.join("data.tdb"), b"data").unwrap();
let manager = BackupManager::default();
let _ = manager.cleanup_old_backups(&backup_dir, 0);
for _ in 0..5 {
manager.create_backup(&source_dir, &backup_dir).unwrap();
std::thread::sleep(std::time::Duration::from_millis(100));
}
let backups = manager.list_backups(&backup_dir).unwrap();
assert_eq!(backups.len(), 5);
let removed = manager.cleanup_old_backups(&backup_dir, 3).unwrap();
assert_eq!(removed, 2);
let backups_after = manager.list_backups(&backup_dir).unwrap();
assert_eq!(backups_after.len(), 3);
fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_incremental_backup() {
let temp_dir = env::temp_dir().join("oxirs_tdb_incremental_test");
fs::remove_dir_all(&temp_dir).ok();
let source_dir = temp_dir.join("source");
let backup_dir = temp_dir.join("backups");
fs::create_dir_all(&source_dir).unwrap();
let large_data = vec![0u8; 10_000]; fs::write(source_dir.join("data1.tdb"), &large_data).unwrap();
fs::write(source_dir.join("data2.tdb"), &large_data).unwrap();
fs::write(source_dir.join("data3.tdb"), &large_data).unwrap();
let mut manager = BackupManager::default();
let full_backup = manager.create_backup(&source_dir, &backup_dir).unwrap();
assert_eq!(full_backup.backup_type, BackupType::Full);
std::thread::sleep(std::time::Duration::from_millis(10));
fs::write(source_dir.join("data4.tdb"), b"new data").unwrap();
let inc_backup = manager
.create_incremental_backup(&source_dir, &backup_dir)
.unwrap();
assert_eq!(inc_backup.backup_type, BackupType::Incremental);
assert!(inc_backup.parent_backup.is_some());
assert!(inc_backup.size_bytes < full_backup.size_bytes);
fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_incremental_backup_no_changes() {
let temp_dir = env::temp_dir().join("oxirs_tdb_incremental_nochanges_test");
fs::remove_dir_all(&temp_dir).ok();
let source_dir = temp_dir.join("source");
let backup_dir = temp_dir.join("backups");
fs::create_dir_all(&source_dir).unwrap();
fs::write(source_dir.join("data.tdb"), b"static data").unwrap();
let mut manager = BackupManager::default();
manager.create_backup(&source_dir, &backup_dir).unwrap();
let result = manager.create_incremental_backup(&source_dir, &backup_dir);
assert!(result.is_err());
fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_incremental_backup_restore() {
let temp_dir = env::temp_dir().join("oxirs_tdb_incremental_restore_test");
fs::remove_dir_all(&temp_dir).ok();
let source_dir = temp_dir.join("source");
let backup_dir = temp_dir.join("backups");
let restore_dir = temp_dir.join("restored");
fs::create_dir_all(&source_dir).unwrap();
fs::write(source_dir.join("file1.tdb"), b"version 1").unwrap();
fs::write(source_dir.join("file2.tdb"), b"unchanged").unwrap();
let mut manager = BackupManager::default();
manager.create_backup(&source_dir, &backup_dir).unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
fs::write(source_dir.join("file1.tdb"), b"version 2").unwrap();
fs::write(source_dir.join("file3.tdb"), b"new file").unwrap();
manager
.create_incremental_backup(&source_dir, &backup_dir)
.unwrap();
let backups = manager.list_backups(&backup_dir).unwrap();
let inc_backup = backups
.iter()
.find(|b| b.backup_type == BackupType::Incremental)
.unwrap();
let backup_entries: Vec<_> = fs::read_dir(&backup_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.collect();
let inc_backup_path = backup_entries
.iter()
.find(|e| {
if let Ok(meta) = manager.load_metadata(&e.path()) {
meta.backup_type == BackupType::Incremental
} else {
false
}
})
.unwrap();
manager
.restore_incremental_backup(inc_backup_path.path(), &restore_dir)
.unwrap();
assert!(restore_dir.join("file1.tdb").exists());
assert!(restore_dir.join("file2.tdb").exists());
assert!(restore_dir.join("file3.tdb").exists());
let file1_data = fs::read_to_string(restore_dir.join("file1.tdb")).unwrap();
assert_eq!(file1_data, "version 2");
let file3_data = fs::read_to_string(restore_dir.join("file3.tdb")).unwrap();
assert_eq!(file3_data, "new file");
fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_file_manifest_tracking() {
let temp_dir = env::temp_dir().join("oxirs_tdb_manifest_test");
fs::remove_dir_all(&temp_dir).ok();
let test_dir = temp_dir.join("test_data");
fs::create_dir_all(&test_dir).unwrap();
fs::write(test_dir.join("file1.tdb"), b"test data").unwrap();
fs::write(test_dir.join("file2.tdb"), b"more data").unwrap();
let manager = BackupManager::default();
let manifest = manager.build_file_manifest(&test_dir).unwrap();
assert_eq!(manifest.files.len(), 2);
assert!(manifest.files.contains_key("file1.tdb"));
assert!(manifest.files.contains_key("file2.tdb"));
let file1_meta = manifest.files.get("file1.tdb").unwrap();
assert_eq!(file1_meta.size, 9);
fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_find_changed_files() {
let manager = BackupManager::default();
let mut old_manifest = FileManifest {
files: HashMap::new(),
};
old_manifest.files.insert(
"file1.tdb".to_string(),
FileMetadata {
size: 100,
modified: SystemTime::UNIX_EPOCH,
checksum: 0x12345678,
compressed: false,
},
);
old_manifest.files.insert(
"file2.tdb".to_string(),
FileMetadata {
size: 200,
modified: SystemTime::UNIX_EPOCH,
checksum: 0xabcdef01,
compressed: false,
},
);
let mut new_manifest = FileManifest {
files: HashMap::new(),
};
new_manifest.files.insert(
"file1.tdb".to_string(),
FileMetadata {
size: 150, modified: SystemTime::UNIX_EPOCH,
checksum: 0x87654321, compressed: false,
},
);
new_manifest.files.insert(
"file2.tdb".to_string(),
FileMetadata {
size: 200, modified: SystemTime::UNIX_EPOCH,
checksum: 0xabcdef01,
compressed: false,
},
);
new_manifest.files.insert(
"file3.tdb".to_string(),
FileMetadata {
size: 50,
modified: SystemTime::UNIX_EPOCH,
checksum: 0x11111111,
compressed: false,
},
);
let changed = manager.find_changed_files(&old_manifest, &new_manifest);
assert_eq!(changed.len(), 2);
assert!(changed.contains(&"file1.tdb".to_string()));
assert!(changed.contains(&"file3.tdb".to_string()));
}
#[test]
fn test_backup_chain_building() {
let temp_dir = env::temp_dir().join("oxirs_tdb_chain_test");
fs::remove_dir_all(&temp_dir).ok();
let source_dir = temp_dir.join("source");
let backup_dir = temp_dir.join("backups");
fs::create_dir_all(&source_dir).unwrap();
fs::write(source_dir.join("data.tdb"), b"v1").unwrap();
let mut manager = BackupManager::default();
let full_meta = manager.create_backup(&source_dir, &backup_dir).unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
fs::write(source_dir.join("data.tdb"), b"v2").unwrap();
manager
.create_incremental_backup(&source_dir, &backup_dir)
.unwrap();
let backup_entries: Vec<_> = fs::read_dir(&backup_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.collect();
let inc_backup_path = backup_entries
.iter()
.find(|e| {
if let Ok(meta) = manager.load_metadata(&e.path()) {
meta.backup_type == BackupType::Incremental
} else {
false
}
})
.unwrap();
let chain = manager
.build_backup_chain(&inc_backup_path.path(), &backup_dir)
.unwrap();
assert_eq!(chain.len(), 2);
let full_backup_meta = manager.load_metadata(&chain[0]).unwrap();
let inc_backup_meta = manager.load_metadata(&chain[1]).unwrap();
assert_eq!(full_backup_meta.backup_type, BackupType::Full);
assert_eq!(inc_backup_meta.backup_type, BackupType::Incremental);
fs::remove_dir_all(&temp_dir).ok();
}
}