1use crate::error::{Result, TdbError};
15use std::collections::{BTreeMap, HashMap};
16use std::fs::{self, File};
17use std::io::{self, Read, Write};
18use std::path::{Path, PathBuf};
19use std::time::SystemTime;
20
21#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
23pub struct BackupMetadata {
24 pub version: String,
26 pub backup_type: BackupType,
28 pub created_at: SystemTime,
30 pub source_path: String,
32 pub triple_count: usize,
34 pub dictionary_size: usize,
36 pub size_bytes: u64,
38 pub compressed: bool,
40 pub checksum: String,
42 pub parent_backup: Option<String>,
44 pub wal_lsn: Option<u64>,
46 pub file_manifest: FileManifest,
48}
49
50#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
52pub struct FileManifest {
53 pub files: HashMap<String, FileMetadata>,
55}
56
57#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
59pub struct FileMetadata {
60 pub size: u64,
62 pub modified: SystemTime,
64 pub checksum: u32,
66 pub compressed: bool,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
72pub enum BackupType {
73 Full,
75 Incremental,
77}
78
79#[derive(Debug, Clone)]
81pub struct BackupConfig {
82 pub compress: bool,
84 pub verify: bool,
86 pub backup_type: BackupType,
88}
89
90impl Default for BackupConfig {
91 fn default() -> Self {
92 Self {
93 compress: true,
94 verify: true,
95 backup_type: BackupType::Full,
96 }
97 }
98}
99
100pub struct BackupManager {
102 config: BackupConfig,
104}
105
106impl BackupManager {
107 pub fn new(config: BackupConfig) -> Self {
109 Self { config }
110 }
111
112 pub fn create_backup(
114 &self,
115 source_dir: impl AsRef<Path>,
116 backup_dir: impl AsRef<Path>,
117 ) -> Result<BackupMetadata> {
118 let source_dir = source_dir.as_ref();
119 let backup_dir = backup_dir.as_ref();
120
121 if !source_dir.exists() {
123 return Err(TdbError::Other(format!(
124 "Source directory does not exist: {:?}",
125 source_dir
126 )));
127 }
128
129 fs::create_dir_all(backup_dir).map_err(TdbError::Io)?;
131
132 let duration = SystemTime::now()
134 .duration_since(SystemTime::UNIX_EPOCH)
135 .map_err(|e| TdbError::Other(format!("Time error: {}", e)))?;
136
137 let backup_name = format!(
138 "tdb_backup_{}_{}",
139 duration.as_secs(),
140 duration.subsec_nanos()
141 );
142 let backup_path = backup_dir.join(&backup_name);
143
144 self.copy_database_files(source_dir, &backup_path)?;
146
147 let size_bytes = self.calculate_directory_size(&backup_path)?;
149 let checksum = self.calculate_checksum(&backup_path)?;
150
151 let file_manifest = self.build_file_manifest(&backup_path)?;
153
154 let parent_backup = if self.config.backup_type == BackupType::Incremental {
156 self.find_latest_full_backup(backup_dir)?.and_then(|path| {
157 path.file_name()
158 .and_then(|n| n.to_str())
159 .map(|s| s.to_string())
160 })
161 } else {
162 None
163 };
164
165 let metadata = BackupMetadata {
167 version: crate::VERSION.to_string(),
168 backup_type: self.config.backup_type,
169 created_at: SystemTime::now(),
170 source_path: source_dir.to_string_lossy().to_string(),
171 triple_count: 0, dictionary_size: 0, size_bytes,
174 compressed: self.config.compress,
175 checksum,
176 parent_backup,
177 wal_lsn: None, file_manifest,
179 };
180
181 self.save_metadata(&backup_path, &metadata)?;
183
184 if self.config.verify {
186 self.verify_backup(&backup_path, &metadata)?;
187 }
188
189 Ok(metadata)
190 }
191
192 pub fn restore_backup(
194 &self,
195 backup_dir: impl AsRef<Path>,
196 target_dir: impl AsRef<Path>,
197 ) -> Result<()> {
198 let backup_dir = backup_dir.as_ref();
199 let target_dir = target_dir.as_ref();
200
201 let metadata = self.load_metadata(backup_dir)?;
203 self.verify_backup(backup_dir, &metadata)?;
204
205 fs::create_dir_all(target_dir).map_err(TdbError::Io)?;
207
208 self.copy_database_files(backup_dir, target_dir)?;
210
211 Ok(())
212 }
213
214 pub fn list_backups(&self, backup_dir: impl AsRef<Path>) -> Result<Vec<BackupMetadata>> {
216 let backup_dir = backup_dir.as_ref();
217 let mut backups = Vec::new();
218
219 if !backup_dir.exists() {
220 return Ok(backups);
221 }
222
223 for entry in fs::read_dir(backup_dir).map_err(TdbError::Io)? {
224 let entry = entry.map_err(TdbError::Io)?;
225 let path = entry.path();
226
227 if path.is_dir() {
228 if let Ok(metadata) = self.load_metadata(&path) {
229 backups.push(metadata);
230 }
231 }
232 }
233
234 backups.sort_by_key(|b| std::cmp::Reverse(b.created_at));
236
237 Ok(backups)
238 }
239
240 pub fn verify_backup(
242 &self,
243 backup_dir: impl AsRef<Path>,
244 metadata: &BackupMetadata,
245 ) -> Result<bool> {
246 let backup_dir = backup_dir.as_ref();
247
248 let current_checksum = self.calculate_checksum(backup_dir)?;
250
251 if current_checksum != metadata.checksum {
252 return Err(TdbError::Other(format!(
253 "Backup verification failed: checksum mismatch (expected: {}, got: {})",
254 metadata.checksum, current_checksum
255 )));
256 }
257
258 Ok(true)
259 }
260
261 pub fn cleanup_old_backups(
263 &self,
264 backup_dir: impl AsRef<Path>,
265 keep_count: usize,
266 ) -> Result<usize> {
267 let backup_dir = backup_dir.as_ref();
268 let backups = self.list_backups(backup_dir)?;
269
270 if backups.len() <= keep_count {
271 return Ok(0);
272 }
273
274 let keep_times: std::collections::HashSet<_> = backups
276 .iter()
277 .take(keep_count)
278 .map(|b| b.created_at)
279 .collect();
280
281 let mut removed = 0;
283 for entry in fs::read_dir(backup_dir).map_err(TdbError::Io)? {
284 let entry = entry.map_err(TdbError::Io)?;
285 let path = entry.path();
286
287 if path.is_dir() {
288 if let Ok(metadata) = self.load_metadata(&path) {
289 if !keep_times.contains(&metadata.created_at) {
291 fs::remove_dir_all(&path).map_err(TdbError::Io)?;
292 removed += 1;
293 }
294 }
295 }
296 }
297
298 Ok(removed)
299 }
300
301 #[allow(clippy::only_used_in_recursion)]
305 fn copy_database_files(&self, source: &Path, dest: &Path) -> Result<()> {
306 fs::create_dir_all(dest).map_err(TdbError::Io)?;
307
308 for entry in fs::read_dir(source).map_err(TdbError::Io)? {
309 let entry = entry.map_err(TdbError::Io)?;
310 let path = entry.path();
311 let file_name = path
312 .file_name()
313 .ok_or_else(|| TdbError::Other("Invalid file name".to_string()))?;
314
315 let dest_path = dest.join(file_name);
316
317 if path.is_file() {
318 fs::copy(&path, &dest_path).map_err(TdbError::Io)?;
319 } else if path.is_dir() {
320 self.copy_database_files(&path, &dest_path)?;
321 }
322 }
323
324 Ok(())
325 }
326
327 #[allow(clippy::only_used_in_recursion)]
329 fn calculate_directory_size(&self, dir: &Path) -> Result<u64> {
330 let mut total_size = 0u64;
331
332 for entry in fs::read_dir(dir).map_err(TdbError::Io)? {
333 let entry = entry.map_err(TdbError::Io)?;
334 let path = entry.path();
335
336 if path.is_file() {
337 let metadata = fs::metadata(&path).map_err(TdbError::Io)?;
338 total_size += metadata.len();
339 } else if path.is_dir() {
340 total_size += self.calculate_directory_size(&path)?;
341 }
342 }
343
344 Ok(total_size)
345 }
346
347 fn calculate_checksum(&self, dir: &Path) -> Result<String> {
349 let mut files = BTreeMap::new();
351 self.collect_files(dir, dir, &mut files)?;
352
353 let mut checksum_data = Vec::new();
355 for (rel_path, full_path) in files {
356 checksum_data.extend_from_slice(rel_path.as_bytes());
358
359 if let Ok(metadata) = fs::metadata(&full_path) {
361 checksum_data.extend_from_slice(&metadata.len().to_le_bytes());
362 }
363 }
364
365 let checksum = crc32fast::hash(&checksum_data);
367 Ok(format!("{:08x}", checksum))
368 }
369
370 #[allow(clippy::only_used_in_recursion)]
372 fn collect_files(
373 &self,
374 base_dir: &Path,
375 current_dir: &Path,
376 files: &mut BTreeMap<String, PathBuf>,
377 ) -> Result<()> {
378 for entry in fs::read_dir(current_dir).map_err(TdbError::Io)? {
379 let entry = entry.map_err(TdbError::Io)?;
380 let path = entry.path();
381
382 if path.is_file() {
383 if path.file_name() == Some(std::ffi::OsStr::new("metadata.json")) {
385 continue;
386 }
387
388 if let Ok(rel_path) = path.strip_prefix(base_dir) {
390 files.insert(rel_path.to_string_lossy().to_string(), path.clone());
391 }
392 } else if path.is_dir() {
393 self.collect_files(base_dir, &path, files)?;
394 }
395 }
396
397 Ok(())
398 }
399
400 fn save_metadata(&self, backup_dir: &Path, metadata: &BackupMetadata) -> Result<()> {
402 let metadata_path = backup_dir.join("metadata.json");
403 let json = serde_json::to_string_pretty(metadata)
404 .map_err(|e| TdbError::Serialization(e.to_string()))?;
405
406 fs::write(metadata_path, json).map_err(TdbError::Io)?;
407 Ok(())
408 }
409
410 fn load_metadata(&self, backup_dir: &Path) -> Result<BackupMetadata> {
412 let metadata_path = backup_dir.join("metadata.json");
413 let json = fs::read_to_string(metadata_path).map_err(TdbError::Io)?;
414
415 serde_json::from_str(&json).map_err(|e| TdbError::Deserialization(e.to_string()))
416 }
417
418 fn build_file_manifest(&self, backup_dir: &Path) -> Result<FileManifest> {
420 let mut manifest = FileManifest {
421 files: HashMap::new(),
422 };
423
424 self.collect_file_metadata(backup_dir, backup_dir, &mut manifest)?;
425
426 Ok(manifest)
427 }
428
429 #[allow(clippy::only_used_in_recursion)]
431 fn collect_file_metadata(
432 &self,
433 base_dir: &Path,
434 current_dir: &Path,
435 manifest: &mut FileManifest,
436 ) -> Result<()> {
437 for entry in fs::read_dir(current_dir).map_err(TdbError::Io)? {
438 let entry = entry.map_err(TdbError::Io)?;
439 let path = entry.path();
440
441 if path.is_file() {
442 if path.file_name() == Some(std::ffi::OsStr::new("metadata.json")) {
444 continue;
445 }
446
447 let metadata = fs::metadata(&path).map_err(TdbError::Io)?;
448 let rel_path = path
449 .strip_prefix(base_dir)
450 .map_err(|e| TdbError::Other(format!("Path strip error: {}", e)))?
451 .to_string_lossy()
452 .to_string();
453
454 let file_data = std::fs::read(&path).map_err(TdbError::Io)?;
456 let checksum = crc32fast::hash(&file_data);
457
458 manifest.files.insert(
459 rel_path,
460 FileMetadata {
461 size: metadata.len(),
462 modified: metadata
463 .modified()
464 .map_err(|e| TdbError::Other(format!("Modified time error: {}", e)))?,
465 checksum,
466 compressed: false, },
468 );
469 } else if path.is_dir() {
470 self.collect_file_metadata(base_dir, &path, manifest)?;
471 }
472 }
473
474 Ok(())
475 }
476
477 fn find_latest_full_backup(&self, backup_dir: impl AsRef<Path>) -> Result<Option<PathBuf>> {
479 let backup_dir = backup_dir.as_ref();
480
481 if !backup_dir.exists() {
482 return Ok(None);
483 }
484
485 for entry in fs::read_dir(backup_dir).map_err(TdbError::Io)? {
487 let entry = entry.map_err(TdbError::Io)?;
488 let path = entry.path();
489
490 if path.is_dir() {
491 if let Ok(metadata) = self.load_metadata(&path) {
493 if metadata.backup_type == BackupType::Full {
494 return Ok(Some(path));
496 }
497 }
498 }
499 }
500
501 Ok(None)
502 }
503
504 pub fn create_incremental_backup(
506 &mut self,
507 source_dir: impl AsRef<Path>,
508 backup_dir: impl AsRef<Path>,
509 ) -> Result<BackupMetadata> {
510 self.config.backup_type = BackupType::Incremental;
512
513 let parent_backup_path = self.find_latest_full_backup(&backup_dir)?.ok_or_else(|| {
515 TdbError::Other("No full backup found. Create a full backup first.".to_string())
516 })?;
517
518 let parent_metadata = self.load_metadata(&parent_backup_path)?;
519
520 let current_manifest = self.build_file_manifest(source_dir.as_ref())?;
522
523 let changed_files =
525 self.find_changed_files(&parent_metadata.file_manifest, ¤t_manifest);
526
527 if changed_files.is_empty() {
528 return Err(TdbError::Other(
529 "No changes detected since last backup".to_string(),
530 ));
531 }
532
533 let parent_backup_name = parent_backup_path
535 .file_name()
536 .and_then(|n| n.to_str())
537 .ok_or_else(|| TdbError::Other("Invalid parent backup path".to_string()))?
538 .to_string();
539
540 self.create_incremental_backup_from_changes(
542 source_dir.as_ref(),
543 backup_dir.as_ref(),
544 &changed_files,
545 Some(parent_backup_name),
546 current_manifest,
547 )
548 }
549
550 fn find_changed_files(
552 &self,
553 old_manifest: &FileManifest,
554 new_manifest: &FileManifest,
555 ) -> Vec<String> {
556 let mut changed = Vec::new();
557
558 for (path, new_metadata) in &new_manifest.files {
559 match old_manifest.files.get(path) {
560 None => {
561 changed.push(path.clone());
563 }
564 Some(old_metadata) => {
565 if old_metadata.size != new_metadata.size
570 || old_metadata.checksum != new_metadata.checksum
571 {
572 changed.push(path.clone());
573 }
574 }
575 }
576 }
577
578 changed
579 }
580
581 fn create_incremental_backup_from_changes(
583 &self,
584 source_dir: &Path,
585 backup_dir: &Path,
586 changed_files: &[String],
587 parent_backup: Option<String>,
588 file_manifest: FileManifest,
589 ) -> Result<BackupMetadata> {
590 let duration = SystemTime::now()
592 .duration_since(SystemTime::UNIX_EPOCH)
593 .map_err(|e| TdbError::Other(format!("Time error: {}", e)))?;
594
595 let backup_name = format!(
596 "tdb_backup_{}_{}",
597 duration.as_secs(),
598 duration.subsec_nanos()
599 );
600 let backup_path = backup_dir.join(&backup_name);
601
602 fs::create_dir_all(&backup_path).map_err(TdbError::Io)?;
603
604 for rel_path in changed_files {
606 let source_file = source_dir.join(rel_path);
607 let dest_file = backup_path.join(rel_path);
608
609 if let Some(parent) = dest_file.parent() {
611 fs::create_dir_all(parent).map_err(TdbError::Io)?;
612 }
613
614 fs::copy(&source_file, &dest_file).map_err(TdbError::Io)?;
615 }
616
617 let size_bytes = self.calculate_directory_size(&backup_path)?;
619 let checksum = self.calculate_checksum(&backup_path)?;
620
621 let metadata = BackupMetadata {
623 version: crate::VERSION.to_string(),
624 backup_type: BackupType::Incremental,
625 created_at: SystemTime::now(),
626 source_path: source_dir.to_string_lossy().to_string(),
627 triple_count: 0,
628 dictionary_size: 0,
629 size_bytes,
630 compressed: self.config.compress,
631 checksum,
632 parent_backup,
633 wal_lsn: None,
634 file_manifest,
635 };
636
637 self.save_metadata(&backup_path, &metadata)?;
638
639 Ok(metadata)
640 }
641
642 pub fn restore_incremental_backup(
644 &self,
645 backup_dir: impl AsRef<Path>,
646 target_dir: impl AsRef<Path>,
647 ) -> Result<()> {
648 let backup_dir = backup_dir.as_ref();
649 let target_dir = target_dir.as_ref();
650
651 let backups_root = backup_dir
653 .parent()
654 .ok_or_else(|| TdbError::Other("Invalid backup directory".to_string()))?;
655
656 let backup_chain = self.build_backup_chain(backup_dir, backups_root)?;
657
658 fs::create_dir_all(target_dir).map_err(TdbError::Io)?;
660
661 for backup_path in backup_chain {
662 self.copy_database_files(&backup_path, target_dir)?;
663 }
664
665 Ok(())
666 }
667
668 fn build_backup_chain(
670 &self,
671 starting_backup_path: &Path,
672 backups_root: &Path,
673 ) -> Result<Vec<PathBuf>> {
674 let mut chain = Vec::new();
675 let metadata = self.load_metadata(starting_backup_path)?;
676
677 if metadata.backup_type == BackupType::Full {
679 chain.push(starting_backup_path.to_path_buf());
680 return Ok(chain);
681 }
682
683 chain.push(starting_backup_path.to_path_buf());
685
686 let mut current = metadata;
688 let mut visited = std::collections::HashSet::new();
689
690 while let Some(parent_name) = ¤t.parent_backup {
691 if visited.contains(parent_name) {
692 return Err(TdbError::Other(
693 "Circular backup chain detected".to_string(),
694 ));
695 }
696 visited.insert(parent_name.clone());
697
698 let parent_path = backups_root.join(parent_name);
699 current = self.load_metadata(&parent_path)?;
700
701 chain.push(parent_path);
702
703 if current.backup_type == BackupType::Full {
704 break;
705 }
706 }
707
708 chain.reverse();
710
711 Ok(chain)
712 }
713}
714
715impl Default for BackupManager {
716 fn default() -> Self {
717 Self::new(BackupConfig::default())
718 }
719}
720
721#[cfg(test)]
722mod tests {
723 use super::*;
724 use std::env;
725
726 #[test]
727 fn test_backup_creation() {
728 let temp_dir = env::temp_dir().join("oxirs_tdb_backup_test");
729 fs::remove_dir_all(&temp_dir).ok();
731
732 let source_dir = temp_dir.join("source");
733 let backup_dir = temp_dir.join("backups");
734
735 fs::create_dir_all(&source_dir).unwrap();
737 fs::write(source_dir.join("data.tdb"), b"test data").unwrap();
738
739 let manager = BackupManager::default();
740 let metadata = manager.create_backup(&source_dir, &backup_dir).unwrap();
741
742 assert_eq!(metadata.backup_type, BackupType::Full);
743 assert!(metadata.size_bytes > 0);
744 assert!(!metadata.checksum.is_empty());
745
746 fs::remove_dir_all(&temp_dir).ok();
747 }
748
749 #[test]
750 fn test_backup_restore() {
751 let temp_dir = env::temp_dir().join("oxirs_tdb_restore_test");
752 fs::remove_dir_all(&temp_dir).ok();
754
755 let source_dir = temp_dir.join("source");
756 let backup_dir = temp_dir.join("backups");
757 let restore_dir = temp_dir.join("restored");
758
759 fs::create_dir_all(&source_dir).unwrap();
761 fs::write(source_dir.join("data.tdb"), b"important data").unwrap();
762
763 let manager = BackupManager::default();
764
765 let metadata = manager.create_backup(&source_dir, &backup_dir).unwrap();
767
768 let backups = manager.list_backups(&backup_dir).unwrap();
770 assert_eq!(backups.len(), 1);
771
772 let backup_entries: Vec<_> = fs::read_dir(&backup_dir)
774 .unwrap()
775 .filter_map(|e| e.ok())
776 .filter(|e| e.path().is_dir())
777 .collect();
778 assert_eq!(backup_entries.len(), 1);
779
780 manager
781 .restore_backup(backup_entries[0].path(), &restore_dir)
782 .unwrap();
783
784 assert!(restore_dir.join("data.tdb").exists());
786 let restored_data = fs::read_to_string(restore_dir.join("data.tdb")).unwrap();
787 assert_eq!(restored_data, "important data");
788
789 fs::remove_dir_all(&temp_dir).ok();
790 }
791
792 #[test]
793 fn test_backup_verification() {
794 let temp_dir = env::temp_dir().join("oxirs_tdb_verify_test");
795 fs::remove_dir_all(&temp_dir).ok();
797
798 let source_dir = temp_dir.join("source");
799 let backup_dir = temp_dir.join("backups");
800
801 fs::create_dir_all(&source_dir).unwrap();
803 fs::write(source_dir.join("data.tdb"), b"verify me").unwrap();
804
805 let manager = BackupManager::default();
806 let metadata = manager.create_backup(&source_dir, &backup_dir).unwrap();
807
808 let backup_entries: Vec<_> = fs::read_dir(&backup_dir)
810 .unwrap()
811 .filter_map(|e| e.ok())
812 .filter(|e| e.path().is_dir())
813 .collect();
814
815 assert!(manager
817 .verify_backup(backup_entries[0].path(), &metadata)
818 .unwrap());
819
820 fs::remove_dir_all(&temp_dir).ok();
821 }
822
823 #[test]
824 fn test_list_backups() {
825 let temp_dir = env::temp_dir().join("oxirs_tdb_list_test");
826 fs::remove_dir_all(&temp_dir).ok();
828
829 let source_dir = temp_dir.join("source");
830 let backup_dir = temp_dir.join("backups");
831
832 fs::create_dir_all(&source_dir).unwrap();
834 fs::write(source_dir.join("data.tdb"), b"data").unwrap();
835
836 let manager = BackupManager::default();
837
838 manager.create_backup(&source_dir, &backup_dir).unwrap();
840 std::thread::sleep(std::time::Duration::from_millis(100));
841 manager.create_backup(&source_dir, &backup_dir).unwrap();
842
843 let backups = manager.list_backups(&backup_dir).unwrap();
845 assert_eq!(backups.len(), 2);
846
847 assert!(backups[0].created_at >= backups[1].created_at);
849
850 fs::remove_dir_all(&temp_dir).ok();
851 }
852
853 #[test]
854 fn test_cleanup_old_backups() {
855 let temp_dir = env::temp_dir().join("oxirs_tdb_cleanup_test");
856 fs::remove_dir_all(&temp_dir).ok();
858
859 let source_dir = temp_dir.join("source");
860 let backup_dir = temp_dir.join("backups");
861
862 fs::create_dir_all(&source_dir).unwrap();
864 fs::create_dir_all(&backup_dir).unwrap();
865 fs::write(source_dir.join("data.tdb"), b"data").unwrap();
866
867 let manager = BackupManager::default();
868
869 let _ = manager.cleanup_old_backups(&backup_dir, 0);
871
872 for _ in 0..5 {
874 manager.create_backup(&source_dir, &backup_dir).unwrap();
875 std::thread::sleep(std::time::Duration::from_millis(100));
876 }
877
878 let backups = manager.list_backups(&backup_dir).unwrap();
879 assert_eq!(backups.len(), 5);
880
881 let removed = manager.cleanup_old_backups(&backup_dir, 3).unwrap();
883 assert_eq!(removed, 2);
884
885 let backups_after = manager.list_backups(&backup_dir).unwrap();
886 assert_eq!(backups_after.len(), 3);
887
888 fs::remove_dir_all(&temp_dir).ok();
889 }
890
891 #[test]
892 fn test_incremental_backup() {
893 let temp_dir = env::temp_dir().join("oxirs_tdb_incremental_test");
894 fs::remove_dir_all(&temp_dir).ok();
895
896 let source_dir = temp_dir.join("source");
897 let backup_dir = temp_dir.join("backups");
898
899 fs::create_dir_all(&source_dir).unwrap();
901 let large_data = vec![0u8; 10_000]; fs::write(source_dir.join("data1.tdb"), &large_data).unwrap();
904 fs::write(source_dir.join("data2.tdb"), &large_data).unwrap();
905 fs::write(source_dir.join("data3.tdb"), &large_data).unwrap();
906
907 let mut manager = BackupManager::default();
908
909 let full_backup = manager.create_backup(&source_dir, &backup_dir).unwrap();
911 assert_eq!(full_backup.backup_type, BackupType::Full);
912
913 std::thread::sleep(std::time::Duration::from_millis(10));
915 fs::write(source_dir.join("data4.tdb"), b"new data").unwrap();
916
917 let inc_backup = manager
919 .create_incremental_backup(&source_dir, &backup_dir)
920 .unwrap();
921 assert_eq!(inc_backup.backup_type, BackupType::Incremental);
922 assert!(inc_backup.parent_backup.is_some());
923 assert!(inc_backup.size_bytes < full_backup.size_bytes);
925
926 fs::remove_dir_all(&temp_dir).ok();
927 }
928
929 #[test]
930 fn test_incremental_backup_no_changes() {
931 let temp_dir = env::temp_dir().join("oxirs_tdb_incremental_nochanges_test");
932 fs::remove_dir_all(&temp_dir).ok();
933
934 let source_dir = temp_dir.join("source");
935 let backup_dir = temp_dir.join("backups");
936
937 fs::create_dir_all(&source_dir).unwrap();
939 fs::write(source_dir.join("data.tdb"), b"static data").unwrap();
940
941 let mut manager = BackupManager::default();
942
943 manager.create_backup(&source_dir, &backup_dir).unwrap();
945
946 let result = manager.create_incremental_backup(&source_dir, &backup_dir);
948 assert!(result.is_err());
949
950 fs::remove_dir_all(&temp_dir).ok();
951 }
952
953 #[test]
954 fn test_incremental_backup_restore() {
955 let temp_dir = env::temp_dir().join("oxirs_tdb_incremental_restore_test");
956 fs::remove_dir_all(&temp_dir).ok();
957
958 let source_dir = temp_dir.join("source");
959 let backup_dir = temp_dir.join("backups");
960 let restore_dir = temp_dir.join("restored");
961
962 fs::create_dir_all(&source_dir).unwrap();
964 fs::write(source_dir.join("file1.tdb"), b"version 1").unwrap();
965 fs::write(source_dir.join("file2.tdb"), b"unchanged").unwrap();
966
967 let mut manager = BackupManager::default();
968
969 manager.create_backup(&source_dir, &backup_dir).unwrap();
971
972 std::thread::sleep(std::time::Duration::from_millis(10));
974 fs::write(source_dir.join("file1.tdb"), b"version 2").unwrap();
975 fs::write(source_dir.join("file3.tdb"), b"new file").unwrap();
976
977 manager
979 .create_incremental_backup(&source_dir, &backup_dir)
980 .unwrap();
981
982 let backups = manager.list_backups(&backup_dir).unwrap();
984 let inc_backup = backups
985 .iter()
986 .find(|b| b.backup_type == BackupType::Incremental)
987 .unwrap();
988
989 let backup_entries: Vec<_> = fs::read_dir(&backup_dir)
990 .unwrap()
991 .filter_map(|e| e.ok())
992 .filter(|e| e.path().is_dir())
993 .collect();
994
995 let inc_backup_path = backup_entries
997 .iter()
998 .find(|e| {
999 if let Ok(meta) = manager.load_metadata(&e.path()) {
1000 meta.backup_type == BackupType::Incremental
1001 } else {
1002 false
1003 }
1004 })
1005 .unwrap();
1006
1007 manager
1009 .restore_incremental_backup(inc_backup_path.path(), &restore_dir)
1010 .unwrap();
1011
1012 assert!(restore_dir.join("file1.tdb").exists());
1014 assert!(restore_dir.join("file2.tdb").exists());
1015 assert!(restore_dir.join("file3.tdb").exists());
1016
1017 let file1_data = fs::read_to_string(restore_dir.join("file1.tdb")).unwrap();
1018 assert_eq!(file1_data, "version 2");
1019
1020 let file3_data = fs::read_to_string(restore_dir.join("file3.tdb")).unwrap();
1021 assert_eq!(file3_data, "new file");
1022
1023 fs::remove_dir_all(&temp_dir).ok();
1024 }
1025
1026 #[test]
1027 fn test_file_manifest_tracking() {
1028 let temp_dir = env::temp_dir().join("oxirs_tdb_manifest_test");
1029 fs::remove_dir_all(&temp_dir).ok();
1030
1031 let test_dir = temp_dir.join("test_data");
1032 fs::create_dir_all(&test_dir).unwrap();
1033 fs::write(test_dir.join("file1.tdb"), b"test data").unwrap();
1034 fs::write(test_dir.join("file2.tdb"), b"more data").unwrap();
1035
1036 let manager = BackupManager::default();
1037 let manifest = manager.build_file_manifest(&test_dir).unwrap();
1038
1039 assert_eq!(manifest.files.len(), 2);
1040 assert!(manifest.files.contains_key("file1.tdb"));
1041 assert!(manifest.files.contains_key("file2.tdb"));
1042
1043 let file1_meta = manifest.files.get("file1.tdb").unwrap();
1044 assert_eq!(file1_meta.size, 9); fs::remove_dir_all(&temp_dir).ok();
1047 }
1048
1049 #[test]
1050 fn test_find_changed_files() {
1051 let manager = BackupManager::default();
1052
1053 let mut old_manifest = FileManifest {
1055 files: HashMap::new(),
1056 };
1057 old_manifest.files.insert(
1058 "file1.tdb".to_string(),
1059 FileMetadata {
1060 size: 100,
1061 modified: SystemTime::UNIX_EPOCH,
1062 checksum: 0x12345678,
1063 compressed: false,
1064 },
1065 );
1066 old_manifest.files.insert(
1067 "file2.tdb".to_string(),
1068 FileMetadata {
1069 size: 200,
1070 modified: SystemTime::UNIX_EPOCH,
1071 checksum: 0xabcdef01,
1072 compressed: false,
1073 },
1074 );
1075
1076 let mut new_manifest = FileManifest {
1078 files: HashMap::new(),
1079 };
1080 new_manifest.files.insert(
1081 "file1.tdb".to_string(),
1082 FileMetadata {
1083 size: 150, modified: SystemTime::UNIX_EPOCH,
1085 checksum: 0x87654321, compressed: false,
1087 },
1088 );
1089 new_manifest.files.insert(
1090 "file2.tdb".to_string(),
1091 FileMetadata {
1092 size: 200, modified: SystemTime::UNIX_EPOCH,
1094 checksum: 0xabcdef01,
1095 compressed: false,
1096 },
1097 );
1098 new_manifest.files.insert(
1099 "file3.tdb".to_string(),
1100 FileMetadata {
1101 size: 50,
1102 modified: SystemTime::UNIX_EPOCH,
1103 checksum: 0x11111111,
1104 compressed: false,
1105 },
1106 );
1107
1108 let changed = manager.find_changed_files(&old_manifest, &new_manifest);
1109
1110 assert_eq!(changed.len(), 2);
1112 assert!(changed.contains(&"file1.tdb".to_string()));
1113 assert!(changed.contains(&"file3.tdb".to_string()));
1114 }
1115
1116 #[test]
1117 fn test_backup_chain_building() {
1118 let temp_dir = env::temp_dir().join("oxirs_tdb_chain_test");
1119 fs::remove_dir_all(&temp_dir).ok();
1120
1121 let source_dir = temp_dir.join("source");
1122 let backup_dir = temp_dir.join("backups");
1123
1124 fs::create_dir_all(&source_dir).unwrap();
1125 fs::write(source_dir.join("data.tdb"), b"v1").unwrap();
1126
1127 let mut manager = BackupManager::default();
1128
1129 let full_meta = manager.create_backup(&source_dir, &backup_dir).unwrap();
1131
1132 std::thread::sleep(std::time::Duration::from_millis(10));
1134 fs::write(source_dir.join("data.tdb"), b"v2").unwrap();
1135 manager
1136 .create_incremental_backup(&source_dir, &backup_dir)
1137 .unwrap();
1138
1139 let backup_entries: Vec<_> = fs::read_dir(&backup_dir)
1141 .unwrap()
1142 .filter_map(|e| e.ok())
1143 .filter(|e| e.path().is_dir())
1144 .collect();
1145
1146 let inc_backup_path = backup_entries
1148 .iter()
1149 .find(|e| {
1150 if let Ok(meta) = manager.load_metadata(&e.path()) {
1151 meta.backup_type == BackupType::Incremental
1152 } else {
1153 false
1154 }
1155 })
1156 .unwrap();
1157
1158 let chain = manager
1160 .build_backup_chain(&inc_backup_path.path(), &backup_dir)
1161 .unwrap();
1162
1163 assert_eq!(chain.len(), 2);
1165
1166 let full_backup_meta = manager.load_metadata(&chain[0]).unwrap();
1168 let inc_backup_meta = manager.load_metadata(&chain[1]).unwrap();
1169
1170 assert_eq!(full_backup_meta.backup_type, BackupType::Full);
1171 assert_eq!(inc_backup_meta.backup_type, BackupType::Incremental);
1172
1173 fs::remove_dir_all(&temp_dir).ok();
1174 }
1175}