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
611 remote.ensure_namespace(&"backups".to_string()).await?;
615
616 tracing::info!(
619 backup_id = backup_id,
620 remote_path = remote_path,
621 size = data.len(),
622 "Backup uploaded to remote storage"
623 );
624
625 Ok(remote_path)
626 }
627
628 async fn download_from_remote(&self, _remote: &ObjectStorage, backup_id: &str) -> Result<()> {
630 let remote_path = format!("backups/{}.snap", backup_id);
631
632 tracing::warn!(
633 backup_id = backup_id,
634 remote_path = remote_path,
635 "Remote backup download not yet implemented"
636 );
637
638 Err(DakeraError::Storage(format!(
639 "Remote backup download not yet implemented for '{}'",
640 backup_id
641 )))
642 }
643
644 fn backup_metadata_path(&self, backup_id: &str) -> std::path::PathBuf {
647 self.config
648 .snapshot_config
649 .snapshot_dir
650 .join(format!("{}.bak", backup_id))
651 }
652
653 fn save_backup_metadata(&self, metadata: &BackupMetadata) -> Result<()> {
654 use std::fs::File;
655 use std::io::BufWriter;
656
657 let path = self.backup_metadata_path(&metadata.snapshot.id);
658 let file = File::create(&path).map_err(|e| {
659 DakeraError::Storage(format!("Failed to create backup metadata: {}", e))
660 })?;
661 let writer = BufWriter::new(file);
662 serde_json::to_writer_pretty(writer, metadata)
663 .map_err(|e| DakeraError::Storage(format!("Backup metadata serialize error: {}", e)))?;
664 Ok(())
665 }
666
667 fn load_backup_metadata(&self, backup_id: &str) -> Result<BackupMetadata> {
668 use std::fs::File;
669 use std::io::BufReader;
670
671 let path = self.backup_metadata_path(backup_id);
672 let file = File::open(&path)
673 .map_err(|e| DakeraError::Storage(format!("Failed to open backup metadata: {}", e)))?;
674 let reader = BufReader::new(file);
675 serde_json::from_reader(reader)
676 .map_err(|e| DakeraError::Storage(format!("Backup metadata deserialize error: {}", e)))
677 }
678}
679
680#[derive(Debug, Clone)]
682pub struct RestoreStats {
683 pub backup_id: String,
685 pub namespaces_restored: usize,
687 pub vectors_restored: u64,
689 pub duration_ms: u64,
691}
692
693pub struct BackupScheduler {
695 pub interval: Duration,
697 pub next_backup: SystemTime,
699 pub backup_type: BackupType,
701 pub tags: HashMap<String, String>,
703}
704
705impl BackupScheduler {
706 pub fn daily() -> Self {
708 Self {
709 interval: Duration::from_secs(24 * 60 * 60),
710 next_backup: SystemTime::now() + Duration::from_secs(24 * 60 * 60),
711 backup_type: BackupType::Scheduled,
712 tags: {
713 let mut tags = HashMap::new();
714 tags.insert("schedule".to_string(), "daily".to_string());
715 tags
716 },
717 }
718 }
719
720 pub fn hourly() -> Self {
722 Self {
723 interval: Duration::from_secs(60 * 60),
724 next_backup: SystemTime::now() + Duration::from_secs(60 * 60),
725 backup_type: BackupType::Scheduled,
726 tags: {
727 let mut tags = HashMap::new();
728 tags.insert("schedule".to_string(), "hourly".to_string());
729 tags
730 },
731 }
732 }
733
734 pub fn custom(interval: Duration) -> Self {
736 Self {
737 interval,
738 next_backup: SystemTime::now() + interval,
739 backup_type: BackupType::Scheduled,
740 tags: HashMap::new(),
741 }
742 }
743
744 pub fn is_backup_due(&self) -> bool {
746 SystemTime::now() >= self.next_backup
747 }
748
749 pub fn mark_completed(&mut self) {
751 self.next_backup = SystemTime::now() + self.interval;
752 }
753
754 pub fn time_until_next(&self) -> Duration {
756 self.next_backup
757 .duration_since(SystemTime::now())
758 .unwrap_or(Duration::ZERO)
759 }
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765 use crate::memory::InMemoryStorage;
766 use common::Vector;
767 use std::path::Path;
768 use tempfile::TempDir;
769
770 fn test_config(dir: &Path) -> BackupConfig {
771 BackupConfig {
772 snapshot_config: SnapshotConfig {
773 snapshot_dir: dir.to_path_buf(),
774 max_snapshots: 10,
775 compression_enabled: false,
776 include_metadata: true,
777 },
778 remote_config: None,
779 retention: RetentionPolicy::default(),
780 verify_backups: true,
781 compression: CompressionConfig::default(),
782 encryption: None,
783 }
784 }
785
786 fn create_test_vector(id: &str, dim: usize) -> Vector {
787 Vector {
788 id: id.to_string(),
789 values: vec![1.0; dim],
790 metadata: None,
791 ttl_seconds: None,
792 expires_at: None,
793 }
794 }
795
796 #[tokio::test]
797 async fn test_create_backup() {
798 let temp_dir = TempDir::new().unwrap();
799 let config = test_config(temp_dir.path());
800 let mut manager = BackupManager::new(config).unwrap();
801
802 let storage = InMemoryStorage::new();
803 storage.ensure_namespace(&"test".to_string()).await.unwrap();
804 storage
805 .upsert(
806 &"test".to_string(),
807 vec![create_test_vector("v1", 4), create_test_vector("v2", 4)],
808 )
809 .await
810 .unwrap();
811
812 let backup = manager
813 .create_backup(
814 &storage,
815 BackupType::Manual,
816 Some("Test backup".to_string()),
817 HashMap::new(),
818 )
819 .await
820 .unwrap();
821
822 assert_eq!(backup.snapshot.total_vectors, 2);
823 assert_eq!(backup.backup_type, BackupType::Manual);
824 assert!(!backup.checksum.is_empty());
825 }
826
827 #[tokio::test]
828 async fn test_verify_backup() {
829 let temp_dir = TempDir::new().unwrap();
830 let config = test_config(temp_dir.path());
831 let mut manager = BackupManager::new(config).unwrap();
832
833 let storage = InMemoryStorage::new();
834 storage.ensure_namespace(&"test".to_string()).await.unwrap();
835 storage
836 .upsert(&"test".to_string(), vec![create_test_vector("v1", 4)])
837 .await
838 .unwrap();
839
840 let backup = manager
841 .create_backup(&storage, BackupType::Manual, None, HashMap::new())
842 .await
843 .unwrap();
844
845 let verification = manager.verify_backup(&backup.snapshot.id).unwrap();
846
847 assert!(verification.valid);
848 assert!(verification.checksum_valid);
849 assert!(verification.data_integrity);
850 }
851
852 #[tokio::test]
853 async fn test_restore_backup() {
854 let temp_dir = TempDir::new().unwrap();
855 let config = test_config(temp_dir.path());
856 let mut manager = BackupManager::new(config).unwrap();
857
858 let storage = InMemoryStorage::new();
859 storage.ensure_namespace(&"test".to_string()).await.unwrap();
860 storage
861 .upsert(&"test".to_string(), vec![create_test_vector("v1", 4)])
862 .await
863 .unwrap();
864
865 let backup = manager
866 .create_backup(&storage, BackupType::Manual, None, HashMap::new())
867 .await
868 .unwrap();
869
870 storage
872 .delete(&"test".to_string(), &["v1".to_string()])
873 .await
874 .unwrap();
875 assert_eq!(storage.count(&"test".to_string()).await.unwrap(), 0);
876
877 let stats = manager
879 .restore_backup(&storage, &backup.snapshot.id)
880 .await
881 .unwrap();
882
883 assert_eq!(stats.vectors_restored, 1);
884 assert_eq!(storage.count(&"test".to_string()).await.unwrap(), 1);
885 }
886
887 #[tokio::test]
888 async fn test_backup_stats() {
889 let temp_dir = TempDir::new().unwrap();
890 let config = test_config(temp_dir.path());
891 let mut manager = BackupManager::new(config).unwrap();
892
893 let storage = InMemoryStorage::new();
894 storage.ensure_namespace(&"test".to_string()).await.unwrap();
895 storage
896 .upsert(&"test".to_string(), vec![create_test_vector("v1", 4)])
897 .await
898 .unwrap();
899
900 for _ in 0..3 {
902 manager
903 .create_backup(&storage, BackupType::Manual, None, HashMap::new())
904 .await
905 .unwrap();
906 }
907
908 let stats = manager.get_stats();
909 assert_eq!(stats.total_backups, 3);
910 assert!(stats.last_backup_at.is_some());
911 }
912
913 #[tokio::test]
914 async fn test_verify_backup_detects_corruption() {
915 let temp_dir = TempDir::new().unwrap();
916 let config = test_config(temp_dir.path());
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
931 let snap_path = temp_dir.path().join(format!("{}.snap", backup.snapshot.id));
933 use std::io::Write;
934 let mut file = std::fs::OpenOptions::new()
935 .append(true)
936 .open(&snap_path)
937 .unwrap();
938 file.write_all(b"CORRUPTED_DATA").unwrap();
939 drop(file);
940
941 let verification = manager.verify_backup(&backup.snapshot.id).unwrap();
943 assert!(
944 !verification.checksum_valid,
945 "corrupted backup should fail checksum"
946 );
947 assert!(!verification.valid, "corrupted backup should not be valid");
948 }
949
950 #[test]
951 fn test_backup_scheduler() {
952 let mut scheduler = BackupScheduler::hourly();
953
954 assert!(!scheduler.is_backup_due());
956
957 scheduler.next_backup = SystemTime::now() - Duration::from_secs(1);
959 assert!(scheduler.is_backup_due());
960
961 scheduler.mark_completed();
963 assert!(!scheduler.is_backup_due());
964 }
965}