Skip to main content

aegis_server/
backup.rs

1//! Aegis Backup Module
2//!
3//! Provides backup and restore functionality for Aegis-DB.
4//! Creates timestamped backups of all data including block files,
5//! WAL files, and metadata.
6//!
7//! Supports encrypted backups for HIPAA compliance using AES-256-GCM.
8//! Set AEGIS_ENCRYPTION_KEY environment variable (64 hex chars = 32 bytes).
9//!
10//! @version 0.1.0
11//! @author AutomataNexus Development Team
12
13use crate::activity::ActivityType;
14use crate::state::AppState;
15use aes_gcm::{
16    aead::{Aead, KeyInit},
17    Aes256Gcm, Nonce,
18};
19use axum::{
20    extract::{Path, State},
21    http::StatusCode,
22    response::IntoResponse,
23    Json,
24};
25use chrono::Utc;
26use parking_lot::Mutex;
27use serde::{Deserialize, Serialize};
28use sha2::{Digest, Sha256};
29use std::fs::{self, File};
30use std::io::{Read, Write};
31use std::path::{Path as StdPath, PathBuf};
32use std::sync::OnceLock;
33
34// =============================================================================
35// Encryption Constants and Key Management
36// =============================================================================
37
38const AES_GCM_NONCE_SIZE: usize = 12;
39const AES_256_KEY_SIZE: usize = 32;
40const ENCRYPTION_ALGORITHM: &str = "AES-256-GCM";
41
42/// Global encryption key loaded from environment variable.
43static ENCRYPTION_KEY: OnceLock<[u8; AES_256_KEY_SIZE]> = OnceLock::new();
44static ENCRYPTION_KEY_INIT: Mutex<bool> = Mutex::new(false);
45
46/// Get the encryption key from environment variable AEGIS_ENCRYPTION_KEY.
47/// The key must be 32 bytes (64 hex characters).
48fn get_encryption_key() -> Result<&'static [u8; AES_256_KEY_SIZE], String> {
49    // Fast path: key already initialized
50    if let Some(key) = ENCRYPTION_KEY.get() {
51        return Ok(key);
52    }
53
54    // Slow path: initialize the key with mutex protection
55    let _guard = ENCRYPTION_KEY_INIT.lock();
56
57    // Double-check after acquiring lock
58    if let Some(key) = ENCRYPTION_KEY.get() {
59        return Ok(key);
60    }
61
62    let hex_key = std::env::var("AEGIS_ENCRYPTION_KEY").map_err(|_| {
63        "AEGIS_ENCRYPTION_KEY environment variable not set. Required for encrypted backups."
64            .to_string()
65    })?;
66
67    let key_bytes = hex::decode(&hex_key)
68        .map_err(|e| format!("Invalid hex encoding in AEGIS_ENCRYPTION_KEY: {}", e))?;
69
70    if key_bytes.len() != AES_256_KEY_SIZE {
71        return Err(format!(
72            "AEGIS_ENCRYPTION_KEY must be {} bytes ({} hex chars), got {} bytes",
73            AES_256_KEY_SIZE,
74            AES_256_KEY_SIZE * 2,
75            key_bytes.len()
76        ));
77    }
78
79    let mut key = [0u8; AES_256_KEY_SIZE];
80    key.copy_from_slice(&key_bytes);
81
82    // Store the key
83    let _ = ENCRYPTION_KEY.set(key);
84
85    Ok(ENCRYPTION_KEY.get().unwrap())
86}
87
88/// Encrypt data using AES-256-GCM.
89/// Returns encrypted data with 12-byte nonce prepended.
90fn encrypt_aes256gcm(plaintext: &[u8]) -> Result<Vec<u8>, String> {
91    let key = get_encryption_key()?;
92    let cipher =
93        Aes256Gcm::new_from_slice(key).map_err(|e| format!("Failed to create cipher: {}", e))?;
94
95    // Generate a random nonce (12 bytes for AES-GCM)
96    let mut nonce_bytes = [0u8; AES_GCM_NONCE_SIZE];
97    getrandom::getrandom(&mut nonce_bytes)
98        .map_err(|e| format!("Failed to generate nonce: {}", e))?;
99    let nonce = Nonce::from_slice(&nonce_bytes);
100
101    let ciphertext = cipher
102        .encrypt(nonce, plaintext)
103        .map_err(|e| format!("Encryption failed: {}", e))?;
104
105    // Prepend nonce to ciphertext for storage
106    let mut result = Vec::with_capacity(AES_GCM_NONCE_SIZE + ciphertext.len());
107    result.extend_from_slice(&nonce_bytes);
108    result.extend(ciphertext);
109
110    Ok(result)
111}
112
113/// Decrypt data using AES-256-GCM.
114/// Expects 12-byte nonce prepended to ciphertext.
115fn decrypt_aes256gcm(encrypted_data: &[u8]) -> Result<Vec<u8>, String> {
116    if encrypted_data.len() < AES_GCM_NONCE_SIZE {
117        return Err("Encrypted data too short: missing nonce".to_string());
118    }
119
120    let key = get_encryption_key()?;
121    let cipher =
122        Aes256Gcm::new_from_slice(key).map_err(|e| format!("Failed to create cipher: {}", e))?;
123
124    let nonce = Nonce::from_slice(&encrypted_data[..AES_GCM_NONCE_SIZE]);
125    let ciphertext = &encrypted_data[AES_GCM_NONCE_SIZE..];
126
127    let plaintext = cipher.decrypt(nonce, ciphertext).map_err(|e| {
128        format!("Decryption failed: {}. Ensure AEGIS_ENCRYPTION_KEY matches the key used during backup.", e)
129    })?;
130
131    Ok(plaintext)
132}
133
134// =============================================================================
135// Backup Types
136// =============================================================================
137
138/// Information about a backup.
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct BackupInfo {
141    pub id: String,
142    pub timestamp: String,
143    pub version: String,
144    pub size_bytes: u64,
145    pub checksum: String,
146    pub compressed: bool,
147    pub status: BackupStatus,
148    pub files_count: usize,
149    pub created_by: Option<String>,
150    /// Whether the backup is encrypted (HIPAA compliance).
151    #[serde(default)]
152    pub encrypted: bool,
153    /// Encryption algorithm used.
154    #[serde(default)]
155    pub encryption_algorithm: Option<String>,
156}
157
158/// Status of a backup operation.
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "lowercase")]
161pub enum BackupStatus {
162    InProgress,
163    Completed,
164    Failed,
165    Corrupted,
166}
167
168/// Backup metadata stored with each backup.
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct BackupMetadata {
171    pub id: String,
172    pub timestamp: String,
173    pub version: String,
174    pub checksum: String,
175    pub compressed: bool,
176    pub files: Vec<BackupFile>,
177    pub created_by: Option<String>,
178    /// Whether the backup is encrypted (HIPAA compliance).
179    #[serde(default)]
180    pub encrypted: bool,
181    /// Encryption algorithm used (e.g., "AES-256-GCM").
182    #[serde(default)]
183    pub encryption_algorithm: Option<String>,
184    /// Key ID for key rotation support.
185    #[serde(default)]
186    pub key_id: Option<String>,
187}
188
189/// Information about a file in the backup.
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct BackupFile {
192    pub path: String,
193    pub size_bytes: u64,
194    pub checksum: String,
195}
196
197/// Request to create a backup.
198#[derive(Debug, Clone, Deserialize)]
199pub struct CreateBackupRequest {
200    #[serde(default)]
201    pub compress: bool,
202    pub description: Option<String>,
203    /// Enable encryption for HIPAA compliance (default: true).
204    #[serde(default = "default_encrypt")]
205    pub encrypt: bool,
206    /// Key ID for key rotation support.
207    pub encryption_key_id: Option<String>,
208}
209
210fn default_encrypt() -> bool {
211    true
212}
213
214/// Response from creating a backup.
215#[derive(Debug, Serialize)]
216pub struct CreateBackupResponse {
217    pub success: bool,
218    pub backup: Option<BackupInfo>,
219    pub error: Option<String>,
220}
221
222/// Request to restore from a backup.
223#[derive(Debug, Clone, Deserialize)]
224pub struct RestoreRequest {
225    pub backup_id: String,
226    #[serde(default)]
227    pub force: bool,
228}
229
230/// Response from restoring a backup.
231#[derive(Debug, Serialize)]
232pub struct RestoreResponse {
233    pub success: bool,
234    pub message: String,
235    pub files_restored: usize,
236}
237
238/// List backups response.
239#[derive(Debug, Serialize)]
240pub struct ListBackupsResponse {
241    pub backups: Vec<BackupInfo>,
242    pub total: usize,
243}
244
245/// Delete backup response.
246#[derive(Debug, Serialize)]
247pub struct DeleteBackupResponse {
248    pub success: bool,
249    pub message: String,
250}
251
252// =============================================================================
253// Backup Manager
254// =============================================================================
255
256/// Manages backup and restore operations.
257pub struct BackupManager {
258    backup_dir: PathBuf,
259    data_dir: PathBuf,
260}
261
262impl BackupManager {
263    /// Create a new backup manager.
264    pub fn new(data_dir: PathBuf) -> Self {
265        let backup_dir = data_dir.join("backups");
266        // Ensure backup directory exists
267        if let Err(e) = fs::create_dir_all(&backup_dir) {
268            tracing::error!("Failed to create backup directory: {}", e);
269        }
270        Self {
271            backup_dir,
272            data_dir,
273        }
274    }
275
276    /// Create a new backup.
277    ///
278    /// # Arguments
279    /// * `compress` - Whether to compress the backup
280    /// * `created_by` - Optional user who created the backup
281    /// * `encrypt` - Whether to encrypt files (HIPAA compliance)
282    /// * `key_id` - Optional key ID for key rotation support
283    pub fn create_backup(
284        &self,
285        compress: bool,
286        created_by: Option<&str>,
287        encrypt: bool,
288        key_id: Option<&str>,
289    ) -> Result<BackupInfo, String> {
290        // Validate encryption key is available if encryption is requested
291        if encrypt {
292            get_encryption_key()?;
293        }
294
295        let timestamp = Utc::now();
296        let backup_id = format!("backup_{}", timestamp.format("%Y%m%d_%H%M%S"));
297        let backup_path = self.backup_dir.join(&backup_id);
298
299        // Create backup directory
300        fs::create_dir_all(&backup_path)
301            .map_err(|e| format!("Failed to create backup directory: {}", e))?;
302
303        let mut files = Vec::new();
304        let mut total_size: u64 = 0;
305        let mut hasher = Sha256::new();
306
307        // Copy data files
308        let dirs_to_backup = vec![
309            ("blocks", self.data_dir.join("blocks")),
310            ("wal", self.data_dir.join("wal")),
311            ("documents", self.data_dir.join("documents")),
312        ];
313
314        // Also copy individual data files
315        let files_to_backup = vec!["kv_store.json", "sql_tables.json"];
316
317        // Copy directory contents
318        for (name, source_dir) in dirs_to_backup {
319            if source_dir.exists() && source_dir.is_dir() {
320                let target_dir = backup_path.join(name);
321                fs::create_dir_all(&target_dir)
322                    .map_err(|e| format!("Failed to create backup subdirectory {}: {}", name, e))?;
323
324                self.copy_directory(
325                    &source_dir,
326                    &target_dir,
327                    &mut files,
328                    &mut total_size,
329                    &mut hasher,
330                    encrypt,
331                )?;
332            }
333        }
334
335        // Copy individual files
336        for filename in files_to_backup {
337            let source_file = self.data_dir.join(filename);
338            if source_file.exists() && source_file.is_file() {
339                let target_file = backup_path.join(filename);
340                self.copy_file(
341                    &source_file,
342                    &target_file,
343                    &mut files,
344                    &mut total_size,
345                    &mut hasher,
346                    encrypt,
347                )?;
348            }
349        }
350
351        let checksum = format!("{:x}", hasher.finalize());
352
353        // Create metadata
354        let metadata = BackupMetadata {
355            id: backup_id.clone(),
356            timestamp: timestamp.to_rfc3339(),
357            version: env!("CARGO_PKG_VERSION").to_string(),
358            checksum: checksum.clone(),
359            compressed: compress,
360            files: files.clone(),
361            created_by: created_by.map(String::from),
362            encrypted: encrypt,
363            encryption_algorithm: if encrypt {
364                Some(ENCRYPTION_ALGORITHM.to_string())
365            } else {
366                None
367            },
368            key_id: key_id.map(String::from),
369        };
370
371        // Save metadata
372        let metadata_path = backup_path.join("metadata.json");
373        let metadata_json = serde_json::to_string_pretty(&metadata)
374            .map_err(|e| format!("Failed to serialize metadata: {}", e))?;
375        fs::write(&metadata_path, metadata_json)
376            .map_err(|e| format!("Failed to write metadata: {}", e))?;
377
378        // Optionally compress
379        if compress {
380            self.compress_backup(&backup_path)?;
381        }
382
383        let backup_info = BackupInfo {
384            id: backup_id,
385            timestamp: timestamp.to_rfc3339(),
386            version: env!("CARGO_PKG_VERSION").to_string(),
387            size_bytes: total_size,
388            checksum,
389            compressed: compress,
390            status: BackupStatus::Completed,
391            files_count: files.len(),
392            created_by: created_by.map(String::from),
393            encrypted: encrypt,
394            encryption_algorithm: if encrypt {
395                Some(ENCRYPTION_ALGORITHM.to_string())
396            } else {
397                None
398            },
399        };
400
401        tracing::info!(
402            "Backup created: {} ({} files, {} bytes, encrypted: {})",
403            backup_info.id,
404            backup_info.files_count,
405            backup_info.size_bytes,
406            backup_info.encrypted
407        );
408
409        Ok(backup_info)
410    }
411
412    /// Copy a directory recursively, optionally encrypting files.
413    fn copy_directory(
414        &self,
415        source: &StdPath,
416        target: &StdPath,
417        files: &mut Vec<BackupFile>,
418        total_size: &mut u64,
419        hasher: &mut Sha256,
420        encrypt: bool,
421    ) -> Result<(), String> {
422        let entries = fs::read_dir(source)
423            .map_err(|e| format!("Failed to read directory {:?}: {}", source, e))?;
424
425        for entry in entries {
426            let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
427            let path = entry.path();
428            let file_name = entry.file_name();
429            let target_path = target.join(&file_name);
430
431            if path.is_dir() {
432                fs::create_dir_all(&target_path)
433                    .map_err(|e| format!("Failed to create directory {:?}: {}", target_path, e))?;
434                self.copy_directory(&path, &target_path, files, total_size, hasher, encrypt)?;
435            } else if path.is_file() {
436                self.copy_file(&path, &target_path, files, total_size, hasher, encrypt)?;
437            }
438        }
439
440        Ok(())
441    }
442
443    /// Copy a single file, optionally encrypting it.
444    fn copy_file(
445        &self,
446        source: &StdPath,
447        target: &StdPath,
448        files: &mut Vec<BackupFile>,
449        total_size: &mut u64,
450        hasher: &mut Sha256,
451        encrypt: bool,
452    ) -> Result<(), String> {
453        // Read source file
454        let mut source_file =
455            File::open(source).map_err(|e| format!("Failed to open {:?}: {}", source, e))?;
456        let mut contents = Vec::new();
457        source_file
458            .read_to_end(&mut contents)
459            .map_err(|e| format!("Failed to read {:?}: {}", source, e))?;
460
461        // Calculate file checksum (of original plaintext for integrity verification)
462        let mut file_hasher = Sha256::new();
463        file_hasher.update(&contents);
464        let file_checksum = format!("{:x}", file_hasher.finalize());
465
466        // Update global hasher (using original plaintext)
467        hasher.update(&contents);
468
469        // Encrypt if requested
470        let data_to_write = if encrypt {
471            encrypt_aes256gcm(&contents)?
472        } else {
473            contents.clone()
474        };
475
476        // Write target file
477        let mut target_file =
478            File::create(target).map_err(|e| format!("Failed to create {:?}: {}", target, e))?;
479        target_file
480            .write_all(&data_to_write)
481            .map_err(|e| format!("Failed to write {:?}: {}", target, e))?;
482
483        let size = contents.len() as u64;
484        *total_size += size;
485
486        // Get relative path for metadata
487        let relative_path = source
488            .strip_prefix(&self.data_dir)
489            .unwrap_or(source)
490            .to_string_lossy()
491            .to_string();
492
493        files.push(BackupFile {
494            path: relative_path,
495            size_bytes: size,
496            checksum: file_checksum,
497        });
498
499        Ok(())
500    }
501
502    /// Compress a backup directory (simplified - just creates a marker).
503    fn compress_backup(&self, _backup_path: &StdPath) -> Result<(), String> {
504        // In a full implementation, this would create a .tar.gz or .zip archive
505        // For now, we just mark the backup as compressed in metadata
506        tracing::info!("Compression requested but not fully implemented yet");
507        Ok(())
508    }
509
510    /// List all available backups.
511    pub fn list_backups(&self) -> Result<Vec<BackupInfo>, String> {
512        let mut backups = Vec::new();
513
514        if !self.backup_dir.exists() {
515            return Ok(backups);
516        }
517
518        let entries = fs::read_dir(&self.backup_dir)
519            .map_err(|e| format!("Failed to read backup directory: {}", e))?;
520
521        for entry in entries {
522            let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
523            let path = entry.path();
524
525            if path.is_dir() {
526                let metadata_path = path.join("metadata.json");
527                if metadata_path.exists() {
528                    match self.read_backup_metadata(&metadata_path) {
529                        Ok(metadata) => {
530                            // Calculate actual size on disk
531                            let size = self.calculate_directory_size(&path);
532                            let status = if self.verify_backup_integrity(&path, &metadata) {
533                                BackupStatus::Completed
534                            } else {
535                                BackupStatus::Corrupted
536                            };
537
538                            backups.push(BackupInfo {
539                                id: metadata.id,
540                                timestamp: metadata.timestamp,
541                                version: metadata.version,
542                                size_bytes: size,
543                                checksum: metadata.checksum,
544                                compressed: metadata.compressed,
545                                status,
546                                files_count: metadata.files.len(),
547                                created_by: metadata.created_by,
548                                encrypted: metadata.encrypted,
549                                encryption_algorithm: metadata.encryption_algorithm,
550                            });
551                        }
552                        Err(e) => {
553                            tracing::warn!("Failed to read backup metadata from {:?}: {}", path, e);
554                        }
555                    }
556                }
557            }
558        }
559
560        // Sort by timestamp (newest first)
561        backups.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
562
563        Ok(backups)
564    }
565
566    /// Get information about a specific backup.
567    pub fn get_backup(&self, backup_id: &str) -> Result<BackupInfo, String> {
568        let backup_path = self.backup_dir.join(backup_id);
569        if !backup_path.exists() {
570            return Err(format!("Backup '{}' not found", backup_id));
571        }
572
573        let metadata_path = backup_path.join("metadata.json");
574        let metadata = self.read_backup_metadata(&metadata_path)?;
575
576        let size = self.calculate_directory_size(&backup_path);
577        let status = if self.verify_backup_integrity(&backup_path, &metadata) {
578            BackupStatus::Completed
579        } else {
580            BackupStatus::Corrupted
581        };
582
583        Ok(BackupInfo {
584            id: metadata.id,
585            timestamp: metadata.timestamp,
586            version: metadata.version,
587            size_bytes: size,
588            checksum: metadata.checksum,
589            compressed: metadata.compressed,
590            status,
591            files_count: metadata.files.len(),
592            created_by: metadata.created_by,
593            encrypted: metadata.encrypted,
594            encryption_algorithm: metadata.encryption_algorithm,
595        })
596    }
597
598    /// Read backup metadata from file.
599    fn read_backup_metadata(&self, path: &StdPath) -> Result<BackupMetadata, String> {
600        let content =
601            fs::read_to_string(path).map_err(|e| format!("Failed to read metadata file: {}", e))?;
602        serde_json::from_str(&content).map_err(|e| format!("Failed to parse metadata: {}", e))
603    }
604
605    /// Calculate total size of a directory.
606    fn calculate_directory_size(&self, path: &StdPath) -> u64 {
607        let mut size = 0;
608        if let Ok(entries) = fs::read_dir(path) {
609            for entry in entries.flatten() {
610                let entry_path = entry.path();
611                if entry_path.is_dir() {
612                    size += self.calculate_directory_size(&entry_path);
613                } else if entry_path.is_file() {
614                    size += entry.metadata().map(|m| m.len()).unwrap_or(0);
615                }
616            }
617        }
618        size
619    }
620
621    /// Verify backup integrity by checking file checksums.
622    /// For encrypted backups, files must be decrypted before checksum verification.
623    fn verify_backup_integrity(&self, backup_path: &StdPath, metadata: &BackupMetadata) -> bool {
624        // For encrypted backups, skip verification if key is not available
625        // (we'll fail later during restore with a clear error message)
626        if metadata.encrypted && get_encryption_key().is_err() {
627            tracing::warn!(
628                "Cannot verify encrypted backup integrity: encryption key not available"
629            );
630            return true; // Assume valid, will fail during restore if key is wrong
631        }
632
633        // Verify a sample of files (or all for small backups)
634        let files_to_check = if metadata.files.len() > 10 {
635            // Check first, last, and some random files
636            let mut indices: Vec<usize> = vec![0, metadata.files.len() - 1];
637            indices.push(metadata.files.len() / 2);
638            indices.push(metadata.files.len() / 4);
639            indices.push(3 * metadata.files.len() / 4);
640            indices
641        } else {
642            (0..metadata.files.len()).collect()
643        };
644
645        for idx in files_to_check {
646            if let Some(file_info) = metadata.files.get(idx) {
647                // Reconstruct the path - files are stored relative to backup_path
648                let file_path = backup_path.join(&file_info.path);
649                if file_path.exists() {
650                    if let Ok(mut file) = File::open(&file_path) {
651                        let mut contents = Vec::new();
652                        if file.read_to_end(&mut contents).is_ok() {
653                            // If encrypted, decrypt before checksum verification
654                            // (checksums are stored for original plaintext)
655                            let data_to_verify = if metadata.encrypted {
656                                match decrypt_aes256gcm(&contents) {
657                                    Ok(decrypted) => decrypted,
658                                    Err(e) => {
659                                        tracing::warn!(
660                                            "Failed to decrypt {:?} for integrity check: {}",
661                                            file_path,
662                                            e
663                                        );
664                                        return false;
665                                    }
666                                }
667                            } else {
668                                contents
669                            };
670
671                            let mut hasher = Sha256::new();
672                            hasher.update(&data_to_verify);
673                            let checksum = format!("{:x}", hasher.finalize());
674                            if checksum != file_info.checksum {
675                                tracing::warn!(
676                                    "Checksum mismatch for {:?}: expected {}, got {}",
677                                    file_path,
678                                    file_info.checksum,
679                                    checksum
680                                );
681                                return false;
682                            }
683                        }
684                    }
685                }
686            }
687        }
688        true
689    }
690
691    /// Restore from a backup.
692    pub fn restore_backup(&self, backup_id: &str, force: bool) -> Result<usize, String> {
693        let backup_path = self.backup_dir.join(backup_id);
694        if !backup_path.exists() {
695            return Err(format!("Backup '{}' not found", backup_id));
696        }
697
698        let metadata_path = backup_path.join("metadata.json");
699        let metadata = self.read_backup_metadata(&metadata_path)?;
700
701        // If backup is encrypted, ensure we have the decryption key
702        if metadata.encrypted {
703            get_encryption_key().map_err(|e| {
704                format!(
705                    "Cannot restore encrypted backup: {}. Set AEGIS_ENCRYPTION_KEY environment variable.",
706                    e
707                )
708            })?;
709        }
710
711        // Verify integrity before restore (skips checksum for encrypted files)
712        if !self.verify_backup_integrity(&backup_path, &metadata) && !force {
713            return Err(
714                "Backup integrity check failed. Use force=true to restore anyway.".to_string(),
715            );
716        }
717
718        let mut files_restored = 0;
719
720        // Restore directories
721        let dirs_to_restore = vec!["blocks", "wal", "documents"];
722        for dir_name in dirs_to_restore {
723            let source_dir = backup_path.join(dir_name);
724            if source_dir.exists() && source_dir.is_dir() {
725                let target_dir = self.data_dir.join(dir_name);
726                // Create target directory if it doesn't exist
727                fs::create_dir_all(&target_dir)
728                    .map_err(|e| format!("Failed to create directory {:?}: {}", target_dir, e))?;
729                files_restored +=
730                    self.restore_directory(&source_dir, &target_dir, metadata.encrypted)?;
731            }
732        }
733
734        // Restore individual files
735        let files_to_restore = vec!["kv_store.json", "sql_tables.json"];
736        for filename in files_to_restore {
737            let source_file = backup_path.join(filename);
738            if source_file.exists() && source_file.is_file() {
739                let target_file = self.data_dir.join(filename);
740                self.restore_file(&source_file, &target_file, metadata.encrypted)?;
741                files_restored += 1;
742            }
743        }
744
745        tracing::info!(
746            "Restored backup '{}': {} files restored (encrypted: {})",
747            backup_id,
748            files_restored,
749            metadata.encrypted
750        );
751
752        Ok(files_restored)
753    }
754
755    /// Restore a directory recursively, optionally decrypting files.
756    fn restore_directory(
757        &self,
758        source: &StdPath,
759        target: &StdPath,
760        encrypted: bool,
761    ) -> Result<usize, String> {
762        let mut count = 0;
763
764        let entries = fs::read_dir(source)
765            .map_err(|e| format!("Failed to read directory {:?}: {}", source, e))?;
766
767        for entry in entries {
768            let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
769            let path = entry.path();
770            let file_name = entry.file_name();
771            let target_path = target.join(&file_name);
772
773            if path.is_dir() {
774                fs::create_dir_all(&target_path)
775                    .map_err(|e| format!("Failed to create directory {:?}: {}", target_path, e))?;
776                count += self.restore_directory(&path, &target_path, encrypted)?;
777            } else if path.is_file() {
778                self.restore_file(&path, &target_path, encrypted)?;
779                count += 1;
780            }
781        }
782
783        Ok(count)
784    }
785
786    /// Restore a single file, optionally decrypting it.
787    fn restore_file(
788        &self,
789        source: &StdPath,
790        target: &StdPath,
791        encrypted: bool,
792    ) -> Result<(), String> {
793        // Read source file
794        let mut source_file =
795            File::open(source).map_err(|e| format!("Failed to open {:?}: {}", source, e))?;
796        let mut contents = Vec::new();
797        source_file
798            .read_to_end(&mut contents)
799            .map_err(|e| format!("Failed to read {:?}: {}", source, e))?;
800
801        // Decrypt if the backup was encrypted
802        let data_to_write = if encrypted {
803            decrypt_aes256gcm(&contents)?
804        } else {
805            contents
806        };
807
808        // Write target file
809        let mut target_file =
810            File::create(target).map_err(|e| format!("Failed to create {:?}: {}", target, e))?;
811        target_file
812            .write_all(&data_to_write)
813            .map_err(|e| format!("Failed to write {:?}: {}", target, e))?;
814
815        Ok(())
816    }
817
818    /// Delete a backup.
819    pub fn delete_backup(&self, backup_id: &str) -> Result<(), String> {
820        let backup_path = self.backup_dir.join(backup_id);
821        if !backup_path.exists() {
822            return Err(format!("Backup '{}' not found", backup_id));
823        }
824
825        fs::remove_dir_all(&backup_path).map_err(|e| format!("Failed to delete backup: {}", e))?;
826
827        tracing::info!("Deleted backup: {}", backup_id);
828        Ok(())
829    }
830}
831
832// =============================================================================
833// HTTP Handlers
834// =============================================================================
835
836/// Create a new backup.
837pub async fn create_backup(
838    State(state): State<AppState>,
839    Json(request): Json<CreateBackupRequest>,
840) -> impl IntoResponse {
841    state.activity.log(ActivityType::System, "Creating backup");
842
843    // Get data directory from config
844    let data_dir = match &state.config.data_dir {
845        Some(dir) => PathBuf::from(dir),
846        None => {
847            return (
848                StatusCode::BAD_REQUEST,
849                Json(CreateBackupResponse {
850                    success: false,
851                    backup: None,
852                    error: Some(
853                        "No data directory configured. Set data_dir in server configuration."
854                            .to_string(),
855                    ),
856                }),
857            );
858        }
859    };
860
861    // First, save all in-memory data to disk
862    if let Err(e) = state.save_to_disk() {
863        tracing::warn!("Failed to save data to disk before backup: {}", e);
864    }
865
866    let manager = BackupManager::new(data_dir);
867
868    match manager.create_backup(
869        request.compress,
870        None,
871        request.encrypt,
872        request.encryption_key_id.as_deref(),
873    ) {
874        Ok(backup) => {
875            state.activity.log(
876                ActivityType::System,
877                &format!("Backup created: {}", backup.id),
878            );
879            (
880                StatusCode::CREATED,
881                Json(CreateBackupResponse {
882                    success: true,
883                    backup: Some(backup),
884                    error: None,
885                }),
886            )
887        }
888        Err(e) => {
889            state
890                .activity
891                .log(ActivityType::System, &format!("Backup failed: {}", e));
892            (
893                StatusCode::INTERNAL_SERVER_ERROR,
894                Json(CreateBackupResponse {
895                    success: false,
896                    backup: None,
897                    error: Some(e),
898                }),
899            )
900        }
901    }
902}
903
904/// List all available backups.
905pub async fn list_backups(State(state): State<AppState>) -> impl IntoResponse {
906    state.activity.log(ActivityType::Query, "Listing backups");
907
908    // Get data directory from config
909    let data_dir = match &state.config.data_dir {
910        Some(dir) => PathBuf::from(dir),
911        None => {
912            return (
913                StatusCode::OK,
914                Json(ListBackupsResponse {
915                    backups: vec![],
916                    total: 0,
917                }),
918            );
919        }
920    };
921
922    let manager = BackupManager::new(data_dir);
923
924    match manager.list_backups() {
925        Ok(backups) => {
926            let total = backups.len();
927            (StatusCode::OK, Json(ListBackupsResponse { backups, total }))
928        }
929        Err(e) => {
930            tracing::error!("Failed to list backups: {}", e);
931            (
932                StatusCode::OK,
933                Json(ListBackupsResponse {
934                    backups: vec![],
935                    total: 0,
936                }),
937            )
938        }
939    }
940}
941
942/// Restore from a backup.
943pub async fn restore_backup(
944    State(state): State<AppState>,
945    Json(request): Json<RestoreRequest>,
946) -> impl IntoResponse {
947    state.activity.log(
948        ActivityType::System,
949        &format!("Restoring from backup: {}", request.backup_id),
950    );
951
952    // Get data directory from config
953    let data_dir = match &state.config.data_dir {
954        Some(dir) => PathBuf::from(dir),
955        None => {
956            return (
957                StatusCode::BAD_REQUEST,
958                Json(RestoreResponse {
959                    success: false,
960                    message: "No data directory configured. Set data_dir in server configuration."
961                        .to_string(),
962                    files_restored: 0,
963                }),
964            );
965        }
966    };
967
968    let manager = BackupManager::new(data_dir);
969
970    match manager.restore_backup(&request.backup_id, request.force) {
971        Ok(files_restored) => {
972            state.activity.log(
973                ActivityType::System,
974                &format!(
975                    "Backup restored: {} ({} files)",
976                    request.backup_id, files_restored
977                ),
978            );
979            (
980                StatusCode::OK,
981                Json(RestoreResponse {
982                    success: true,
983                    message: format!(
984                        "Successfully restored backup '{}'. Please restart the server to load restored data.",
985                        request.backup_id
986                    ),
987                    files_restored,
988                }),
989            )
990        }
991        Err(e) => {
992            state
993                .activity
994                .log(ActivityType::System, &format!("Restore failed: {}", e));
995            (
996                StatusCode::INTERNAL_SERVER_ERROR,
997                Json(RestoreResponse {
998                    success: false,
999                    message: e,
1000                    files_restored: 0,
1001                }),
1002            )
1003        }
1004    }
1005}
1006
1007/// Delete a backup.
1008pub async fn delete_backup(
1009    State(state): State<AppState>,
1010    Path(backup_id): Path<String>,
1011) -> impl IntoResponse {
1012    state.activity.log(
1013        ActivityType::Delete,
1014        &format!("Deleting backup: {}", backup_id),
1015    );
1016
1017    // Get data directory from config
1018    let data_dir = match &state.config.data_dir {
1019        Some(dir) => PathBuf::from(dir),
1020        None => {
1021            return (
1022                StatusCode::BAD_REQUEST,
1023                Json(DeleteBackupResponse {
1024                    success: false,
1025                    message: "No data directory configured.".to_string(),
1026                }),
1027            );
1028        }
1029    };
1030
1031    let manager = BackupManager::new(data_dir);
1032
1033    match manager.delete_backup(&backup_id) {
1034        Ok(()) => {
1035            state.activity.log(
1036                ActivityType::Delete,
1037                &format!("Backup deleted: {}", backup_id),
1038            );
1039            (
1040                StatusCode::OK,
1041                Json(DeleteBackupResponse {
1042                    success: true,
1043                    message: format!("Backup '{}' deleted successfully", backup_id),
1044                }),
1045            )
1046        }
1047        Err(e) => (
1048            StatusCode::NOT_FOUND,
1049            Json(DeleteBackupResponse {
1050                success: false,
1051                message: e,
1052            }),
1053        ),
1054    }
1055}
1056
1057// =============================================================================
1058// Tests
1059// =============================================================================
1060
1061#[cfg(test)]
1062mod tests {
1063    use super::*;
1064    use tempfile::tempdir;
1065
1066    #[test]
1067    fn test_backup_manager_creation() {
1068        let temp_dir = tempdir().expect("Failed to create temp dir");
1069        let manager = BackupManager::new(temp_dir.path().to_path_buf());
1070        assert!(manager.backup_dir.exists());
1071    }
1072
1073    #[test]
1074    fn test_create_and_list_backup() {
1075        let temp_dir = tempdir().expect("Failed to create temp dir");
1076        let data_dir = temp_dir.path().to_path_buf();
1077
1078        // Create some test data
1079        let kv_path = data_dir.join("kv_store.json");
1080        fs::write(&kv_path, r#"[{"key": "test", "value": "data"}]"#)
1081            .expect("Failed to write test data");
1082
1083        let manager = BackupManager::new(data_dir);
1084
1085        // Create backup
1086        let backup = manager
1087            .create_backup(false, Some("test_user"), false, None)
1088            .expect("Failed to create backup");
1089        assert!(!backup.id.is_empty());
1090        assert_eq!(backup.status, BackupStatus::Completed);
1091
1092        // List backups
1093        let backups = manager.list_backups().expect("Failed to list backups");
1094        assert_eq!(backups.len(), 1);
1095        assert_eq!(backups[0].id, backup.id);
1096    }
1097
1098    #[test]
1099    fn test_backup_and_restore() {
1100        let temp_dir = tempdir().expect("Failed to create temp dir");
1101        let data_dir = temp_dir.path().to_path_buf();
1102
1103        // Create test data
1104        let kv_path = data_dir.join("kv_store.json");
1105        let test_data = r#"[{"key": "test_key", "value": "test_value"}]"#;
1106        fs::write(&kv_path, test_data).expect("Failed to write test data");
1107
1108        let manager = BackupManager::new(data_dir.clone());
1109
1110        // Create backup
1111        let backup = manager
1112            .create_backup(false, None, false, None)
1113            .expect("Failed to create backup");
1114
1115        // Modify original data
1116        fs::write(&kv_path, r#"[{"key": "modified"}]"#).expect("Failed to modify data");
1117
1118        // Restore
1119        let files_restored = manager
1120            .restore_backup(&backup.id, false)
1121            .expect("Failed to restore");
1122        assert!(files_restored > 0);
1123
1124        // Verify data was restored
1125        let restored_data = fs::read_to_string(&kv_path).expect("Failed to read restored data");
1126        assert_eq!(restored_data, test_data);
1127    }
1128
1129    #[test]
1130    fn test_delete_backup() {
1131        let temp_dir = tempdir().expect("Failed to create temp dir");
1132        let data_dir = temp_dir.path().to_path_buf();
1133
1134        // Create test data
1135        let kv_path = data_dir.join("kv_store.json");
1136        fs::write(&kv_path, "{}").expect("Failed to write test data");
1137
1138        let manager = BackupManager::new(data_dir);
1139
1140        // Create and delete backup
1141        let backup = manager
1142            .create_backup(false, None, false, None)
1143            .expect("Failed to create backup");
1144        manager
1145            .delete_backup(&backup.id)
1146            .expect("Failed to delete backup");
1147
1148        // Verify backup is gone
1149        let backups = manager.list_backups().expect("Failed to list backups");
1150        assert!(backups.is_empty());
1151    }
1152
1153    #[test]
1154    fn test_backup_not_found() {
1155        let temp_dir = tempdir().expect("Failed to create temp dir");
1156        let manager = BackupManager::new(temp_dir.path().to_path_buf());
1157
1158        let result = manager.restore_backup("nonexistent_backup", false);
1159        assert!(result.is_err());
1160        assert!(result.unwrap_err().contains("not found"));
1161    }
1162
1163    #[test]
1164    fn test_encrypted_backup_and_restore() {
1165        // Set up test encryption key (32 bytes = 64 hex chars)
1166        std::env::set_var(
1167            "AEGIS_ENCRYPTION_KEY",
1168            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
1169        );
1170
1171        let temp_dir = tempdir().expect("Failed to create temp dir");
1172        let data_dir = temp_dir.path().to_path_buf();
1173
1174        // Create test data files
1175        let kv_path = data_dir.join("kv_store.json");
1176        let test_data = r#"{"key": "sensitive_patient_data", "value": "HIPAA protected info"}"#;
1177        fs::write(&kv_path, test_data).expect("Failed to write test data");
1178
1179        // Create a blocks directory with test data
1180        let blocks_dir = data_dir.join("blocks");
1181        fs::create_dir_all(&blocks_dir).expect("Failed to create blocks dir");
1182        let block_data = b"Encrypted block data for HIPAA compliance";
1183        fs::write(blocks_dir.join("block_001.dat"), block_data).expect("Failed to write block");
1184
1185        let manager = BackupManager::new(data_dir.clone());
1186
1187        // Create encrypted backup
1188        let backup = manager
1189            .create_backup(false, Some("hipaa_admin"), true, Some("key_v1"))
1190            .expect("Failed to create encrypted backup");
1191
1192        assert!(!backup.id.is_empty());
1193        assert_eq!(backup.status, BackupStatus::Completed);
1194        assert!(backup.encrypted);
1195        assert_eq!(backup.encryption_algorithm, Some("AES-256-GCM".to_string()));
1196        assert!(backup.files_count > 0);
1197
1198        // Verify the backup files are actually encrypted (not plaintext)
1199        let backup_kv_path = manager.backup_dir.join(&backup.id).join("kv_store.json");
1200        let encrypted_contents = fs::read(&backup_kv_path).expect("Failed to read backup file");
1201        // Encrypted data should be different from plaintext and include nonce (12 bytes)
1202        assert_ne!(encrypted_contents.as_slice(), test_data.as_bytes());
1203        assert!(encrypted_contents.len() > test_data.len());
1204
1205        // Modify original data to verify restore works
1206        fs::write(&kv_path, r#"{"modified": true}"#).expect("Failed to modify data");
1207        fs::write(blocks_dir.join("block_001.dat"), b"modified").expect("Failed to modify block");
1208
1209        // Restore encrypted backup
1210        let files_restored = manager
1211            .restore_backup(&backup.id, false)
1212            .expect("Failed to restore encrypted backup");
1213        assert!(files_restored > 0);
1214
1215        // Verify data was restored correctly (decrypted)
1216        let restored_data = fs::read_to_string(&kv_path).expect("Failed to read restored data");
1217        assert_eq!(restored_data, test_data);
1218
1219        let restored_block =
1220            fs::read(blocks_dir.join("block_001.dat")).expect("Failed to read restored block");
1221        assert_eq!(restored_block.as_slice(), block_data);
1222    }
1223
1224    #[test]
1225    fn test_encrypted_backup_metadata() {
1226        std::env::set_var(
1227            "AEGIS_ENCRYPTION_KEY",
1228            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
1229        );
1230
1231        let temp_dir = tempdir().expect("Failed to create temp dir");
1232        let data_dir = temp_dir.path().to_path_buf();
1233
1234        // Create minimal test data
1235        fs::write(data_dir.join("kv_store.json"), "{}").expect("Failed to write test data");
1236
1237        let manager = BackupManager::new(data_dir);
1238
1239        // Create encrypted backup
1240        let backup = manager
1241            .create_backup(false, None, true, None)
1242            .expect("Failed to create backup");
1243
1244        // Read and verify metadata
1245        let metadata_path = manager.backup_dir.join(&backup.id).join("metadata.json");
1246        let metadata_content = fs::read_to_string(&metadata_path).expect("Failed to read metadata");
1247        let metadata: BackupMetadata =
1248            serde_json::from_str(&metadata_content).expect("Failed to parse metadata");
1249
1250        assert!(metadata.encrypted);
1251        assert_eq!(
1252            metadata.encryption_algorithm,
1253            Some("AES-256-GCM".to_string())
1254        );
1255    }
1256
1257    #[test]
1258    fn test_unencrypted_backup_still_works() {
1259        let temp_dir = tempdir().expect("Failed to create temp dir");
1260        let data_dir = temp_dir.path().to_path_buf();
1261
1262        // Create test data
1263        let test_data = r#"{"unencrypted": true}"#;
1264        fs::write(data_dir.join("kv_store.json"), test_data).expect("Failed to write test data");
1265
1266        let manager = BackupManager::new(data_dir.clone());
1267
1268        // Create unencrypted backup (encrypt=false)
1269        let backup = manager
1270            .create_backup(false, None, false, None)
1271            .expect("Failed to create unencrypted backup");
1272
1273        assert!(!backup.encrypted);
1274        assert!(backup.encryption_algorithm.is_none());
1275
1276        // Verify the backup file is plaintext
1277        let backup_kv_path = manager.backup_dir.join(&backup.id).join("kv_store.json");
1278        let backup_contents = fs::read_to_string(&backup_kv_path).expect("Failed to read backup");
1279        assert_eq!(backup_contents, test_data);
1280
1281        // Modify and restore
1282        fs::write(data_dir.join("kv_store.json"), "modified").expect("Failed to modify");
1283        manager
1284            .restore_backup(&backup.id, false)
1285            .expect("Failed to restore");
1286
1287        let restored = fs::read_to_string(data_dir.join("kv_store.json")).expect("Failed to read");
1288        assert_eq!(restored, test_data);
1289    }
1290
1291    #[test]
1292    fn test_encrypted_backup_requires_key() {
1293        // Temporarily unset the encryption key by setting an invalid one
1294        // Note: Due to OnceLock caching, this test may not work in all scenarios
1295        // In production, the key check happens before backup creation
1296
1297        let temp_dir = tempdir().expect("Failed to create temp dir");
1298        let data_dir = temp_dir.path().to_path_buf();
1299        fs::write(data_dir.join("kv_store.json"), "{}").expect("Failed to write test data");
1300
1301        let manager = BackupManager::new(data_dir);
1302
1303        // If key is set from previous tests, backup will succeed
1304        // This test verifies the encrypt flag is properly stored
1305        let result = manager.create_backup(false, None, true, None);
1306
1307        // Either succeeds (key from previous test) or fails (no key)
1308        match result {
1309            Ok(backup) => {
1310                assert!(backup.encrypted);
1311            }
1312            Err(e) => {
1313                assert!(e.contains("AEGIS_ENCRYPTION_KEY"));
1314            }
1315        }
1316    }
1317}