1use common::{DakeraError, Result};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::time::{Duration, SystemTime, UNIX_EPOCH};
15
16use crate::object::{ObjectStorage, ObjectStorageConfig};
17use crate::snapshot::{SnapshotConfig, SnapshotManager, SnapshotMetadata};
18use crate::traits::VectorStorage;
19
20#[derive(Debug, Clone)]
22pub struct BackupConfig {
23 pub snapshot_config: SnapshotConfig,
25 pub remote_config: Option<ObjectStorageConfig>,
27 pub retention: RetentionPolicy,
29 pub verify_backups: bool,
31 pub compression: CompressionConfig,
33 pub encryption: Option<EncryptionConfig>,
35}
36
37impl Default for BackupConfig {
38 fn default() -> Self {
39 Self {
40 snapshot_config: SnapshotConfig::default(),
41 remote_config: None,
42 retention: RetentionPolicy::default(),
43 verify_backups: true,
44 compression: CompressionConfig::default(),
45 encryption: None,
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
52pub struct RetentionPolicy {
53 pub daily_retention_days: u32,
55 pub weekly_retention_weeks: u32,
57 pub monthly_retention_months: u32,
59 pub max_backups: usize,
61}
62
63impl Default for RetentionPolicy {
64 fn default() -> Self {
65 Self {
66 daily_retention_days: 7,
67 weekly_retention_weeks: 4,
68 monthly_retention_months: 12,
69 max_backups: 50,
70 }
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct CompressionConfig {
77 pub enabled: bool,
79 pub level: u32,
81}
82
83impl Default for CompressionConfig {
84 fn default() -> Self {
85 Self {
86 enabled: true,
87 level: 3, }
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct EncryptionConfig {
95 pub key: Vec<u8>,
97 pub salt: Vec<u8>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct BackupMetadata {
104 pub snapshot: SnapshotMetadata,
106 pub backup_type: BackupType,
108 pub remote_location: Option<String>,
110 pub compressed: bool,
112 pub encrypted: bool,
114 pub checksum: String,
116 pub duration_ms: u64,
118 pub tags: HashMap<String, String>,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124pub enum BackupType {
125 Manual,
127 Scheduled,
129 PreOperation,
131 Continuous,
133}
134
135#[derive(Debug, Clone)]
137pub struct VerificationResult {
138 pub backup_id: String,
140 pub valid: bool,
142 pub checksum_valid: bool,
144 pub data_integrity: bool,
146 pub vectors_verified: u64,
148 pub errors: Vec<String>,
150}
151
152#[derive(Debug, Clone, Default, Serialize, Deserialize)]
154pub struct BackupStats {
155 pub total_backups: u64,
157 pub verified_backups: u64,
159 pub total_bytes_backed_up: u64,
161 pub total_bytes_compressed: u64,
163 pub avg_backup_duration_ms: u64,
165 pub last_backup_at: Option<u64>,
167 pub last_verification_at: Option<u64>,
169 pub backup_failures: u64,
171}
172
173pub struct BackupManager {
175 config: BackupConfig,
176 snapshot_manager: SnapshotManager,
177 remote_storage: Option<ObjectStorage>,
178 stats: BackupStats,
179}
180
181impl BackupManager {
182 pub fn new(config: BackupConfig) -> Result<Self> {
184 let snapshot_manager = SnapshotManager::new(config.snapshot_config.clone())?;
185
186 let remote_storage = if let Some(ref remote_config) = config.remote_config {
187 Some(ObjectStorage::new(remote_config.clone())?)
188 } else {
189 None
190 };
191
192 Ok(Self {
193 config,
194 snapshot_manager,
195 remote_storage,
196 stats: BackupStats::default(),
197 })
198 }
199
200 pub async fn create_backup<S: VectorStorage>(
202 &mut self,
203 storage: &S,
204 backup_type: BackupType,
205 description: Option<String>,
206 tags: HashMap<String, String>,
207 ) -> Result<BackupMetadata> {
208 let start = std::time::Instant::now();
209
210 let snapshot = self
212 .snapshot_manager
213 .create_snapshot(storage, description)
214 .await?;
215
216 let duration_ms = start.elapsed().as_millis() as u64;
217
218 let checksum = self.calculate_checksum(&snapshot.id)?;
220
221 let mut backup_metadata = BackupMetadata {
222 snapshot,
223 backup_type,
224 remote_location: None,
225 compressed: self.config.compression.enabled,
226 encrypted: self.config.encryption.is_some(),
227 checksum,
228 duration_ms,
229 tags,
230 };
231
232 if let Some(ref remote) = self.remote_storage {
234 let remote_path = self
235 .upload_to_remote(remote, &backup_metadata.snapshot.id)
236 .await?;
237 backup_metadata.remote_location = Some(remote_path);
238 }
239
240 self.save_backup_metadata(&backup_metadata)?;
242
243 if self.config.verify_backups {
245 let verification = self.verify_backup(&backup_metadata.snapshot.id)?;
246 if !verification.valid {
247 return Err(DakeraError::Storage(format!(
248 "Backup verification failed: {:?}",
249 verification.errors
250 )));
251 }
252 }
253
254 self.stats.total_backups += 1;
256 self.stats.total_bytes_backed_up += backup_metadata.snapshot.size_bytes;
257 self.stats.last_backup_at = Some(
258 SystemTime::now()
259 .duration_since(UNIX_EPOCH)
260 .unwrap_or(Duration::ZERO)
261 .as_secs(),
262 );
263
264 self.apply_retention_policy().await?;
266
267 Ok(backup_metadata)
268 }
269
270 pub async fn create_incremental_backup<S: VectorStorage>(
272 &mut self,
273 storage: &S,
274 parent_id: &str,
275 changed_namespaces: &[String],
276 description: Option<String>,
277 tags: HashMap<String, String>,
278 ) -> Result<BackupMetadata> {
279 let start = std::time::Instant::now();
280
281 let snapshot = self
282 .snapshot_manager
283 .create_incremental_snapshot(storage, parent_id, changed_namespaces, description)
284 .await?;
285
286 let duration_ms = start.elapsed().as_millis() as u64;
287 let checksum = self.calculate_checksum(&snapshot.id)?;
288
289 let mut backup_metadata = BackupMetadata {
290 snapshot,
291 backup_type: BackupType::Manual,
292 remote_location: None,
293 compressed: self.config.compression.enabled,
294 encrypted: self.config.encryption.is_some(),
295 checksum,
296 duration_ms,
297 tags,
298 };
299
300 if let Some(ref remote) = self.remote_storage {
301 let remote_path = self
302 .upload_to_remote(remote, &backup_metadata.snapshot.id)
303 .await?;
304 backup_metadata.remote_location = Some(remote_path);
305 }
306
307 self.save_backup_metadata(&backup_metadata)?;
309
310 self.stats.total_backups += 1;
311 self.stats.total_bytes_backed_up += backup_metadata.snapshot.size_bytes;
312
313 Ok(backup_metadata)
314 }
315
316 pub async fn restore_backup<S: VectorStorage>(
318 &mut self,
319 storage: &S,
320 backup_id: &str,
321 ) -> Result<RestoreStats> {
322 let start = std::time::Instant::now();
323
324 if !self.snapshot_manager.snapshot_exists(backup_id) {
326 if let Some(ref remote) = self.remote_storage {
327 self.download_from_remote(remote, backup_id).await?;
328 } else {
329 return Err(DakeraError::Storage(format!(
330 "Backup not found: {}",
331 backup_id
332 )));
333 }
334 }
335
336 if self.config.verify_backups {
338 let verification = self.verify_backup(backup_id)?;
339 if !verification.valid {
340 return Err(DakeraError::Storage(format!(
341 "Backup verification failed before restore: {:?}",
342 verification.errors
343 )));
344 }
345 }
346
347 let result = self
348 .snapshot_manager
349 .restore_snapshot(storage, backup_id)
350 .await?;
351
352 let duration_ms = start.elapsed().as_millis() as u64;
353
354 Ok(RestoreStats {
355 backup_id: backup_id.to_string(),
356 namespaces_restored: result.namespaces_restored,
357 vectors_restored: result.vectors_restored,
358 duration_ms,
359 })
360 }
361
362 pub fn verify_backup(&mut self, backup_id: &str) -> Result<VerificationResult> {
364 let mut errors = Vec::new();
365
366 if !self.snapshot_manager.snapshot_exists(backup_id) {
368 return Ok(VerificationResult {
369 backup_id: backup_id.to_string(),
370 valid: false,
371 checksum_valid: false,
372 data_integrity: false,
373 vectors_verified: 0,
374 errors: vec!["Backup file not found".to_string()],
375 });
376 }
377
378 let _current_checksum = match self.calculate_checksum(backup_id) {
380 Ok(cs) => cs,
381 Err(e) => {
382 errors.push(format!("Checksum calculation failed: {}", e));
383 return Ok(VerificationResult {
384 backup_id: backup_id.to_string(),
385 valid: false,
386 checksum_valid: false,
387 data_integrity: false,
388 vectors_verified: 0,
389 errors,
390 });
391 }
392 };
393
394 let metadata = match self.snapshot_manager.get_snapshot_metadata(backup_id) {
396 Ok(m) => m,
397 Err(e) => {
398 errors.push(format!("Failed to read metadata: {}", e));
399 return Ok(VerificationResult {
400 backup_id: backup_id.to_string(),
401 valid: false,
402 checksum_valid: false,
403 data_integrity: false,
404 vectors_verified: 0,
405 errors,
406 });
407 }
408 };
409
410 let checksum_valid = match self.load_backup_metadata(backup_id) {
413 Ok(stored) => _current_checksum == stored.checksum,
414 Err(e) => {
415 tracing::warn!(
418 backup_id = backup_id,
419 error = %e,
420 "No backup metadata sidecar found; skipping checksum comparison (legacy backup)"
421 );
422 !_current_checksum.is_empty()
423 }
424 };
425 let data_integrity = errors.is_empty();
426 let valid = checksum_valid && data_integrity;
427
428 if valid {
429 self.stats.verified_backups += 1;
430 self.stats.last_verification_at = Some(
431 SystemTime::now()
432 .duration_since(UNIX_EPOCH)
433 .unwrap_or(Duration::ZERO)
434 .as_secs(),
435 );
436 }
437
438 Ok(VerificationResult {
439 backup_id: backup_id.to_string(),
440 valid,
441 checksum_valid,
442 data_integrity,
443 vectors_verified: metadata.total_vectors,
444 errors,
445 })
446 }
447
448 pub fn list_backups(&self) -> Result<Vec<SnapshotMetadata>> {
450 self.snapshot_manager.list_snapshots()
451 }
452
453 pub async fn delete_backup(&mut self, backup_id: &str) -> Result<bool> {
455 let local_deleted = self.snapshot_manager.delete_snapshot(backup_id)?;
457
458 let bak_path = self.backup_metadata_path(backup_id);
460 if bak_path.exists() {
461 if let Err(e) = std::fs::remove_file(&bak_path) {
462 tracing::warn!(
463 path = %bak_path.display(),
464 error = %e,
465 "Failed to remove backup metadata sidecar"
466 );
467 }
468 }
469
470 if let Some(ref remote) = self.remote_storage {
472 let remote_path = format!("backups/{}.snap", backup_id);
473 let _ = remote.delete(&"backups".to_string(), &[remote_path]).await;
474 }
475
476 Ok(local_deleted)
477 }
478
479 pub fn get_stats(&self) -> &BackupStats {
481 &self.stats
482 }
483
484 async fn apply_retention_policy(&mut self) -> Result<()> {
486 let backups = self.snapshot_manager.list_snapshots()?;
487
488 if backups.len() <= self.config.retention.max_backups {
489 return Ok(());
490 }
491
492 let now = SystemTime::now()
493 .duration_since(UNIX_EPOCH)
494 .unwrap_or(Duration::ZERO)
495 .as_secs();
496
497 let daily_cutoff = now - (self.config.retention.daily_retention_days as u64 * 24 * 60 * 60);
498 let weekly_cutoff =
499 now - (self.config.retention.weekly_retention_weeks as u64 * 7 * 24 * 60 * 60);
500 let monthly_cutoff =
501 now - (self.config.retention.monthly_retention_months as u64 * 30 * 24 * 60 * 60);
502
503 let mut to_keep = Vec::new();
504 let mut to_delete = Vec::new();
505
506 for backup in backups {
507 if backup.created_at >= daily_cutoff {
509 to_keep.push(backup);
510 continue;
511 }
512
513 if backup.created_at >= weekly_cutoff {
515 let week_number = backup.created_at / (7 * 24 * 60 * 60);
517 let has_weekly = to_keep
518 .iter()
519 .any(|b: &SnapshotMetadata| b.created_at / (7 * 24 * 60 * 60) == week_number);
520 if !has_weekly {
521 to_keep.push(backup);
522 continue;
523 }
524 }
525
526 if backup.created_at >= monthly_cutoff {
528 let month_number = backup.created_at / (30 * 24 * 60 * 60);
529 let has_monthly = to_keep
530 .iter()
531 .any(|b: &SnapshotMetadata| b.created_at / (30 * 24 * 60 * 60) == month_number);
532 if !has_monthly {
533 to_keep.push(backup);
534 continue;
535 }
536 }
537
538 to_delete.push(backup);
540 }
541
542 while to_keep.len() > self.config.retention.max_backups && !to_keep.is_empty() {
544 if let Some(oldest) = to_keep.pop() {
545 to_delete.push(oldest);
546 }
547 }
548
549 for backup in to_delete {
551 let is_parent = to_keep
553 .iter()
554 .any(|b| b.parent_id.as_ref() == Some(&backup.id));
555
556 if !is_parent {
557 self.delete_backup(&backup.id).await?;
558 }
559 }
560
561 Ok(())
562 }
563
564 fn calculate_checksum(&self, backup_id: &str) -> Result<String> {
566 use sha2::{Digest, Sha256};
567 use std::fs::File;
568 use std::io::Read;
569
570 let path = self
571 .config
572 .snapshot_config
573 .snapshot_dir
574 .join(format!("{}.snap", backup_id));
575
576 let mut file = File::open(&path)
577 .map_err(|e| DakeraError::Storage(format!("Failed to open backup: {}", e)))?;
578
579 let mut hasher = Sha256::new();
580 let mut buffer = [0u8; 8192];
581
582 loop {
583 let bytes_read = file
584 .read(&mut buffer)
585 .map_err(|e| DakeraError::Storage(format!("Failed to read backup: {}", e)))?;
586 if bytes_read == 0 {
587 break;
588 }
589 hasher.update(&buffer[..bytes_read]);
590 }
591
592 let hash = hasher.finalize();
593 Ok(hash.iter().map(|b| format!("{:02x}", b)).collect())
594 }
595
596 async fn upload_to_remote(&self, remote: &ObjectStorage, backup_id: &str) -> Result<String> {
598 use std::fs;
599
600 let local_path = self
601 .config
602 .snapshot_config
603 .snapshot_dir
604 .join(format!("{}.snap", backup_id));
605
606 let data = fs::read(&local_path)
607 .map_err(|e| DakeraError::Storage(format!("Failed to read backup: {}", e)))?;
608
609 let remote_path = format!("backups/{}.snap", backup_id);
610 let size = data.len();
611
612 remote.put_object(&remote_path, data).await?;
615
616 tracing::info!(
617 backup_id = backup_id,
618 remote_path = remote_path,
619 size = size,
620 "Backup uploaded to remote storage"
621 );
622
623 Ok(remote_path)
624 }
625
626 async fn download_from_remote(&self, remote: &ObjectStorage, backup_id: &str) -> Result<()> {
629 use std::fs;
630
631 let remote_path = format!("backups/{}.snap", backup_id);
632
633 let data = remote.get_object(&remote_path).await?.ok_or_else(|| {
634 DakeraError::Storage(format!(
635 "Remote backup not found at '{}' for '{}'",
636 remote_path, backup_id
637 ))
638 })?;
639
640 let local_path = self
641 .config
642 .snapshot_config
643 .snapshot_dir
644 .join(format!("{}.snap", backup_id));
645
646 if let Some(parent) = local_path.parent() {
647 fs::create_dir_all(parent).map_err(|e| {
648 DakeraError::Storage(format!("Failed to create snapshot dir: {}", e))
649 })?;
650 }
651 fs::write(&local_path, &data).map_err(|e| {
652 DakeraError::Storage(format!("Failed to write downloaded backup: {}", e))
653 })?;
654
655 tracing::info!(
656 backup_id = backup_id,
657 remote_path = remote_path,
658 size = data.len(),
659 "Backup downloaded from remote storage"
660 );
661
662 Ok(())
663 }
664
665 fn backup_metadata_path(&self, backup_id: &str) -> std::path::PathBuf {
668 self.config
669 .snapshot_config
670 .snapshot_dir
671 .join(format!("{}.bak", backup_id))
672 }
673
674 fn save_backup_metadata(&self, metadata: &BackupMetadata) -> Result<()> {
675 use std::fs::File;
676 use std::io::BufWriter;
677
678 let path = self.backup_metadata_path(&metadata.snapshot.id);
679 let file = File::create(&path).map_err(|e| {
680 DakeraError::Storage(format!("Failed to create backup metadata: {}", e))
681 })?;
682 let writer = BufWriter::new(file);
683 serde_json::to_writer_pretty(writer, metadata)
684 .map_err(|e| DakeraError::Storage(format!("Backup metadata serialize error: {}", e)))?;
685 Ok(())
686 }
687
688 fn load_backup_metadata(&self, backup_id: &str) -> Result<BackupMetadata> {
689 use std::fs::File;
690 use std::io::BufReader;
691
692 let path = self.backup_metadata_path(backup_id);
693 let file = File::open(&path)
694 .map_err(|e| DakeraError::Storage(format!("Failed to open backup metadata: {}", e)))?;
695 let reader = BufReader::new(file);
696 serde_json::from_reader(reader)
697 .map_err(|e| DakeraError::Storage(format!("Backup metadata deserialize error: {}", e)))
698 }
699}
700
701#[derive(Debug, Clone)]
703pub struct RestoreStats {
704 pub backup_id: String,
706 pub namespaces_restored: usize,
708 pub vectors_restored: u64,
710 pub duration_ms: u64,
712}
713
714pub struct BackupScheduler {
716 pub interval: Duration,
718 pub next_backup: SystemTime,
720 pub backup_type: BackupType,
722 pub tags: HashMap<String, String>,
724}
725
726impl BackupScheduler {
727 pub fn daily() -> Self {
729 Self {
730 interval: Duration::from_secs(24 * 60 * 60),
731 next_backup: SystemTime::now() + Duration::from_secs(24 * 60 * 60),
732 backup_type: BackupType::Scheduled,
733 tags: {
734 let mut tags = HashMap::new();
735 tags.insert("schedule".to_string(), "daily".to_string());
736 tags
737 },
738 }
739 }
740
741 pub fn hourly() -> Self {
743 Self {
744 interval: Duration::from_secs(60 * 60),
745 next_backup: SystemTime::now() + Duration::from_secs(60 * 60),
746 backup_type: BackupType::Scheduled,
747 tags: {
748 let mut tags = HashMap::new();
749 tags.insert("schedule".to_string(), "hourly".to_string());
750 tags
751 },
752 }
753 }
754
755 pub fn custom(interval: Duration) -> Self {
757 Self {
758 interval,
759 next_backup: SystemTime::now() + interval,
760 backup_type: BackupType::Scheduled,
761 tags: HashMap::new(),
762 }
763 }
764
765 pub fn is_backup_due(&self) -> bool {
767 SystemTime::now() >= self.next_backup
768 }
769
770 pub fn mark_completed(&mut self) {
772 self.next_backup = SystemTime::now() + self.interval;
773 }
774
775 pub fn time_until_next(&self) -> Duration {
777 self.next_backup
778 .duration_since(SystemTime::now())
779 .unwrap_or(Duration::ZERO)
780 }
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786 use crate::memory::InMemoryStorage;
787 use common::Vector;
788 use std::path::Path;
789 use tempfile::TempDir;
790
791 fn test_config(dir: &Path) -> BackupConfig {
792 BackupConfig {
793 snapshot_config: SnapshotConfig {
794 snapshot_dir: dir.to_path_buf(),
795 max_snapshots: 10,
796 compression_enabled: false,
797 include_metadata: true,
798 },
799 remote_config: None,
800 retention: RetentionPolicy::default(),
801 verify_backups: true,
802 compression: CompressionConfig::default(),
803 encryption: None,
804 }
805 }
806
807 fn create_test_vector(id: &str, dim: usize) -> Vector {
808 Vector {
809 id: id.to_string(),
810 values: vec![1.0; dim],
811 metadata: None,
812 ttl_seconds: None,
813 expires_at: None,
814 }
815 }
816
817 #[tokio::test]
818 async fn test_create_backup() {
819 let temp_dir = TempDir::new().unwrap();
820 let config = test_config(temp_dir.path());
821 let mut manager = BackupManager::new(config).unwrap();
822
823 let storage = InMemoryStorage::new();
824 storage.ensure_namespace(&"test".to_string()).await.unwrap();
825 storage
826 .upsert(
827 &"test".to_string(),
828 vec![create_test_vector("v1", 4), create_test_vector("v2", 4)],
829 )
830 .await
831 .unwrap();
832
833 let backup = manager
834 .create_backup(
835 &storage,
836 BackupType::Manual,
837 Some("Test backup".to_string()),
838 HashMap::new(),
839 )
840 .await
841 .unwrap();
842
843 assert_eq!(backup.snapshot.total_vectors, 2);
844 assert_eq!(backup.backup_type, BackupType::Manual);
845 assert!(!backup.checksum.is_empty());
846 }
847
848 #[tokio::test]
849 async fn test_verify_backup() {
850 let temp_dir = TempDir::new().unwrap();
851 let config = test_config(temp_dir.path());
852 let mut manager = BackupManager::new(config).unwrap();
853
854 let storage = InMemoryStorage::new();
855 storage.ensure_namespace(&"test".to_string()).await.unwrap();
856 storage
857 .upsert(&"test".to_string(), vec![create_test_vector("v1", 4)])
858 .await
859 .unwrap();
860
861 let backup = manager
862 .create_backup(&storage, BackupType::Manual, None, HashMap::new())
863 .await
864 .unwrap();
865
866 let verification = manager.verify_backup(&backup.snapshot.id).unwrap();
867
868 assert!(verification.valid);
869 assert!(verification.checksum_valid);
870 assert!(verification.data_integrity);
871 }
872
873 #[tokio::test]
874 async fn test_restore_backup() {
875 let temp_dir = TempDir::new().unwrap();
876 let config = test_config(temp_dir.path());
877 let mut manager = BackupManager::new(config).unwrap();
878
879 let storage = InMemoryStorage::new();
880 storage.ensure_namespace(&"test".to_string()).await.unwrap();
881 storage
882 .upsert(&"test".to_string(), vec![create_test_vector("v1", 4)])
883 .await
884 .unwrap();
885
886 let backup = manager
887 .create_backup(&storage, BackupType::Manual, None, HashMap::new())
888 .await
889 .unwrap();
890
891 storage
893 .delete(&"test".to_string(), &["v1".to_string()])
894 .await
895 .unwrap();
896 assert_eq!(storage.count(&"test".to_string()).await.unwrap(), 0);
897
898 let stats = manager
900 .restore_backup(&storage, &backup.snapshot.id)
901 .await
902 .unwrap();
903
904 assert_eq!(stats.vectors_restored, 1);
905 assert_eq!(storage.count(&"test".to_string()).await.unwrap(), 1);
906 }
907
908 #[tokio::test]
909 async fn test_remote_backup_upload_download_roundtrip() {
910 let temp_dir = TempDir::new().unwrap();
915 let mut config = test_config(temp_dir.path());
916 config.remote_config = Some(ObjectStorageConfig::Memory);
917 let mut manager = BackupManager::new(config).unwrap();
918
919 let storage = InMemoryStorage::new();
920 storage.ensure_namespace(&"test".to_string()).await.unwrap();
921 storage
922 .upsert(&"test".to_string(), vec![create_test_vector("v1", 4)])
923 .await
924 .unwrap();
925
926 let backup = manager
927 .create_backup(&storage, BackupType::Manual, None, HashMap::new())
928 .await
929 .unwrap();
930 assert!(backup.remote_location.is_some());
932
933 let local_snap = temp_dir.path().join(format!("{}.snap", backup.snapshot.id));
936 std::fs::remove_file(&local_snap).unwrap();
937 assert!(!local_snap.exists());
938
939 storage
940 .delete(&"test".to_string(), &["v1".to_string()])
941 .await
942 .unwrap();
943 assert_eq!(storage.count(&"test".to_string()).await.unwrap(), 0);
944
945 let stats = manager
946 .restore_backup(&storage, &backup.snapshot.id)
947 .await
948 .unwrap();
949
950 assert_eq!(stats.vectors_restored, 1);
951 assert_eq!(storage.count(&"test".to_string()).await.unwrap(), 1);
952 assert!(local_snap.exists());
954 }
955
956 #[tokio::test]
957 async fn test_remote_download_missing_backup_errors() {
958 let temp_dir = TempDir::new().unwrap();
961 let mut config = test_config(temp_dir.path());
962 config.remote_config = Some(ObjectStorageConfig::Memory);
963 let mut manager = BackupManager::new(config).unwrap();
964
965 let storage = InMemoryStorage::new();
966 let result = manager.restore_backup(&storage, "does-not-exist").await;
967 assert!(result.is_err());
968 }
969
970 #[tokio::test]
971 async fn test_backup_stats() {
972 let temp_dir = TempDir::new().unwrap();
973 let config = test_config(temp_dir.path());
974 let mut manager = BackupManager::new(config).unwrap();
975
976 let storage = InMemoryStorage::new();
977 storage.ensure_namespace(&"test".to_string()).await.unwrap();
978 storage
979 .upsert(&"test".to_string(), vec![create_test_vector("v1", 4)])
980 .await
981 .unwrap();
982
983 for _ in 0..3 {
985 manager
986 .create_backup(&storage, BackupType::Manual, None, HashMap::new())
987 .await
988 .unwrap();
989 }
990
991 let stats = manager.get_stats();
992 assert_eq!(stats.total_backups, 3);
993 assert!(stats.last_backup_at.is_some());
994 }
995
996 #[tokio::test]
997 async fn test_verify_backup_detects_corruption() {
998 let temp_dir = TempDir::new().unwrap();
999 let config = test_config(temp_dir.path());
1000 let mut manager = BackupManager::new(config).unwrap();
1001
1002 let storage = InMemoryStorage::new();
1003 storage.ensure_namespace(&"test".to_string()).await.unwrap();
1004 storage
1005 .upsert(&"test".to_string(), vec![create_test_vector("v1", 4)])
1006 .await
1007 .unwrap();
1008
1009 let backup = manager
1010 .create_backup(&storage, BackupType::Manual, None, HashMap::new())
1011 .await
1012 .unwrap();
1013
1014 let snap_path = temp_dir.path().join(format!("{}.snap", backup.snapshot.id));
1016 use std::io::Write;
1017 let mut file = std::fs::OpenOptions::new()
1018 .append(true)
1019 .open(&snap_path)
1020 .unwrap();
1021 file.write_all(b"CORRUPTED_DATA").unwrap();
1022 drop(file);
1023
1024 let verification = manager.verify_backup(&backup.snapshot.id).unwrap();
1026 assert!(
1027 !verification.checksum_valid,
1028 "corrupted backup should fail checksum"
1029 );
1030 assert!(!verification.valid, "corrupted backup should not be valid");
1031 }
1032
1033 #[test]
1034 fn test_backup_scheduler() {
1035 let mut scheduler = BackupScheduler::hourly();
1036
1037 assert!(!scheduler.is_backup_due());
1039
1040 scheduler.next_backup = SystemTime::now() - Duration::from_secs(1);
1042 assert!(scheduler.is_backup_due());
1043
1044 scheduler.mark_completed();
1046 assert!(!scheduler.is_backup_due());
1047 }
1048}