Skip to main content

storage/
backup.rs

1//! Backup Utilities for Buffer
2//!
3//! Enterprise-grade backup features building on the snapshot system:
4//! - Remote backup to object storage (S3, Azure, GCS)
5//! - Backup scheduling and retention policies
6//! - Backup verification and integrity checking
7//! - Backup encryption (AES-256)
8//! - Backup compression (zstd)
9//! - Backup statistics and monitoring
10
11use 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/// Backup configuration
21#[derive(Debug, Clone)]
22pub struct BackupConfig {
23    /// Local snapshot configuration
24    pub snapshot_config: SnapshotConfig,
25    /// Remote storage configuration (optional)
26    pub remote_config: Option<ObjectStorageConfig>,
27    /// Retention policy
28    pub retention: RetentionPolicy,
29    /// Enable backup verification
30    pub verify_backups: bool,
31    /// Enable compression
32    pub compression: CompressionConfig,
33    /// Enable encryption
34    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/// Retention policy for backups
51#[derive(Debug, Clone)]
52pub struct RetentionPolicy {
53    /// Keep daily backups for this many days
54    pub daily_retention_days: u32,
55    /// Keep weekly backups for this many weeks
56    pub weekly_retention_weeks: u32,
57    /// Keep monthly backups for this many months
58    pub monthly_retention_months: u32,
59    /// Maximum total backups to keep
60    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/// Compression configuration
75#[derive(Debug, Clone)]
76pub struct CompressionConfig {
77    /// Enable compression
78    pub enabled: bool,
79    /// Compression level (1-22 for zstd)
80    pub level: u32,
81}
82
83impl Default for CompressionConfig {
84    fn default() -> Self {
85        Self {
86            enabled: true,
87            level: 3, // Good balance of speed and ratio
88        }
89    }
90}
91
92/// Encryption configuration
93#[derive(Debug, Clone)]
94pub struct EncryptionConfig {
95    /// Encryption key (32 bytes for AES-256)
96    pub key: Vec<u8>,
97    /// Key derivation salt
98    pub salt: Vec<u8>,
99}
100
101/// Backup metadata with extended information
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct BackupMetadata {
104    /// Underlying snapshot metadata
105    pub snapshot: SnapshotMetadata,
106    /// Backup creation method
107    pub backup_type: BackupType,
108    /// Remote storage location (if uploaded)
109    pub remote_location: Option<String>,
110    /// Compression used
111    pub compressed: bool,
112    /// Encrypted
113    pub encrypted: bool,
114    /// Checksum for verification
115    pub checksum: String,
116    /// Backup duration in milliseconds
117    pub duration_ms: u64,
118    /// Tags for organization
119    pub tags: HashMap<String, String>,
120}
121
122/// Type of backup
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124pub enum BackupType {
125    /// Manual backup triggered by user
126    Manual,
127    /// Scheduled automatic backup
128    Scheduled,
129    /// Pre-operation backup (before risky operations)
130    PreOperation,
131    /// Continuous/streaming backup
132    Continuous,
133}
134
135/// Backup verification result
136#[derive(Debug, Clone)]
137pub struct VerificationResult {
138    /// Backup ID verified
139    pub backup_id: String,
140    /// Verification passed
141    pub valid: bool,
142    /// Checksum matches
143    pub checksum_valid: bool,
144    /// Data integrity verified
145    pub data_integrity: bool,
146    /// Number of vectors verified
147    pub vectors_verified: u64,
148    /// Errors found
149    pub errors: Vec<String>,
150}
151
152/// Backup statistics
153#[derive(Debug, Clone, Default, Serialize, Deserialize)]
154pub struct BackupStats {
155    /// Total backups created
156    pub total_backups: u64,
157    /// Total backups successfully verified
158    pub verified_backups: u64,
159    /// Total data backed up (bytes)
160    pub total_bytes_backed_up: u64,
161    /// Total data after compression (bytes)
162    pub total_bytes_compressed: u64,
163    /// Average backup duration (ms)
164    pub avg_backup_duration_ms: u64,
165    /// Last backup timestamp
166    pub last_backup_at: Option<u64>,
167    /// Last successful verification
168    pub last_verification_at: Option<u64>,
169    /// Backup failures
170    pub backup_failures: u64,
171}
172
173/// Backup manager providing enterprise backup features
174pub struct BackupManager {
175    config: BackupConfig,
176    snapshot_manager: SnapshotManager,
177    remote_storage: Option<ObjectStorage>,
178    stats: BackupStats,
179}
180
181impl BackupManager {
182    /// Create a new backup manager
183    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    /// Create a full backup
201    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        // Create local snapshot
211        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        // Calculate checksum
219        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        // Upload to remote storage if configured
233        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        // Persist BackupMetadata so verify_backup can compare checksums
241        self.save_backup_metadata(&backup_metadata)?;
242
243        // Verify if configured
244        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        // Update stats
255        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        // Apply retention policy
265        self.apply_retention_policy().await?;
266
267        Ok(backup_metadata)
268    }
269
270    /// Create an incremental backup
271    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        // Persist BackupMetadata so verify_backup can compare checksums
308        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    /// Restore from a backup
317    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        // Check if backup exists locally, if not download from remote
325        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        // Verify before restore
337        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    /// Verify a backup's integrity
363    pub fn verify_backup(&mut self, backup_id: &str) -> Result<VerificationResult> {
364        let mut errors = Vec::new();
365
366        // Check file exists
367        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        // Verify checksum
379        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        // Get stored metadata
395        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        // Compare recomputed checksum against the stored original.
411        // load_backup_metadata returns the sidecar written by create_backup.
412        let checksum_valid = match self.load_backup_metadata(backup_id) {
413            Ok(stored) => _current_checksum == stored.checksum,
414            Err(e) => {
415                // No sidecar present (legacy backup created before this fix).
416                // Fall back to "file is readable" check so old backups still pass.
417                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    /// List all available backups
449    pub fn list_backups(&self) -> Result<Vec<SnapshotMetadata>> {
450        self.snapshot_manager.list_snapshots()
451    }
452
453    /// Delete a backup
454    pub async fn delete_backup(&mut self, backup_id: &str) -> Result<bool> {
455        // Delete local snapshot + its .meta sidecar
456        let local_deleted = self.snapshot_manager.delete_snapshot(backup_id)?;
457
458        // Delete the BackupMetadata sidecar (.bak) if present
459        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        // Delete remote if exists
471        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    /// Get backup statistics
480    pub fn get_stats(&self) -> &BackupStats {
481        &self.stats
482    }
483
484    /// Apply retention policy and clean up old backups
485    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            // Keep recent daily backups
508            if backup.created_at >= daily_cutoff {
509                to_keep.push(backup);
510                continue;
511            }
512
513            // Keep weekly backups
514            if backup.created_at >= weekly_cutoff {
515                // Check if this is a "weekly" backup (keep one per week)
516                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            // Keep monthly backups
527            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            // Mark for deletion
539            to_delete.push(backup);
540        }
541
542        // Enforce max backups limit
543        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        // Delete marked backups
550        for backup in to_delete {
551            // Don't delete if it's a parent of an incremental backup
552            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    /// Calculate checksum for a backup
565    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    /// Upload backup to remote storage
597    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        // Use object storage to upload
612        // Note: This is a simplified implementation
613        // In production, would use streaming upload for large files
614        remote.ensure_namespace(&"backups".to_string()).await?;
615
616        // For now, we can't directly write bytes to object storage
617        // This would need the IndexStorage trait or a raw write method
618        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    /// Download backup from remote storage
629    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    // --- BackupMetadata sidecar helpers ---
645
646    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/// Restore statistics
681#[derive(Debug, Clone)]
682pub struct RestoreStats {
683    /// Backup ID restored
684    pub backup_id: String,
685    /// Number of namespaces restored
686    pub namespaces_restored: usize,
687    /// Number of vectors restored
688    pub vectors_restored: u64,
689    /// Duration in milliseconds
690    pub duration_ms: u64,
691}
692
693/// Backup scheduler for automatic backups
694pub struct BackupScheduler {
695    /// Backup interval
696    pub interval: Duration,
697    /// Next scheduled backup time
698    pub next_backup: SystemTime,
699    /// Backup type for scheduled backups
700    pub backup_type: BackupType,
701    /// Tags to apply to scheduled backups
702    pub tags: HashMap<String, String>,
703}
704
705impl BackupScheduler {
706    /// Create a new scheduler with daily backups
707    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    /// Create a new scheduler with hourly backups
721    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    /// Create a custom scheduler
735    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    /// Check if a backup is due
745    pub fn is_backup_due(&self) -> bool {
746        SystemTime::now() >= self.next_backup
747    }
748
749    /// Mark backup as completed and schedule next
750    pub fn mark_completed(&mut self) {
751        self.next_backup = SystemTime::now() + self.interval;
752    }
753
754    /// Get time until next backup
755    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        // Clear data
871        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        // Restore
878        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        // Create a few backups
901        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        // Corrupt the backup file by appending garbage bytes
932        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        // Checksum mismatch must be detected
942        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        // Initially not due (just created)
955        assert!(!scheduler.is_backup_due());
956
957        // Simulate time passing
958        scheduler.next_backup = SystemTime::now() - Duration::from_secs(1);
959        assert!(scheduler.is_backup_due());
960
961        // Mark completed
962        scheduler.mark_completed();
963        assert!(!scheduler.is_backup_due());
964    }
965}