1use 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
34const AES_GCM_NONCE_SIZE: usize = 12;
39const AES_256_KEY_SIZE: usize = 32;
40const ENCRYPTION_ALGORITHM: &str = "AES-256-GCM";
41
42static ENCRYPTION_KEY: OnceLock<[u8; AES_256_KEY_SIZE]> = OnceLock::new();
44static ENCRYPTION_KEY_INIT: Mutex<bool> = Mutex::new(false);
45
46fn get_encryption_key() -> Result<&'static [u8; AES_256_KEY_SIZE], String> {
49 if let Some(key) = ENCRYPTION_KEY.get() {
51 return Ok(key);
52 }
53
54 let _guard = ENCRYPTION_KEY_INIT.lock();
56
57 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 let _ = ENCRYPTION_KEY.set(key);
84
85 Ok(ENCRYPTION_KEY.get().unwrap())
86}
87
88fn 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 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 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
113fn 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#[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 #[serde(default)]
152 pub encrypted: bool,
153 #[serde(default)]
155 pub encryption_algorithm: Option<String>,
156}
157
158#[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#[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 #[serde(default)]
180 pub encrypted: bool,
181 #[serde(default)]
183 pub encryption_algorithm: Option<String>,
184 #[serde(default)]
186 pub key_id: Option<String>,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct BackupFile {
192 pub path: String,
193 pub size_bytes: u64,
194 pub checksum: String,
195}
196
197#[derive(Debug, Clone, Deserialize)]
199pub struct CreateBackupRequest {
200 #[serde(default)]
201 pub compress: bool,
202 pub description: Option<String>,
203 #[serde(default = "default_encrypt")]
205 pub encrypt: bool,
206 pub encryption_key_id: Option<String>,
208}
209
210fn default_encrypt() -> bool {
211 true
212}
213
214#[derive(Debug, Serialize)]
216pub struct CreateBackupResponse {
217 pub success: bool,
218 pub backup: Option<BackupInfo>,
219 pub error: Option<String>,
220}
221
222#[derive(Debug, Clone, Deserialize)]
224pub struct RestoreRequest {
225 pub backup_id: String,
226 #[serde(default)]
227 pub force: bool,
228}
229
230#[derive(Debug, Serialize)]
232pub struct RestoreResponse {
233 pub success: bool,
234 pub message: String,
235 pub files_restored: usize,
236}
237
238#[derive(Debug, Serialize)]
240pub struct ListBackupsResponse {
241 pub backups: Vec<BackupInfo>,
242 pub total: usize,
243}
244
245#[derive(Debug, Serialize)]
247pub struct DeleteBackupResponse {
248 pub success: bool,
249 pub message: String,
250}
251
252pub struct BackupManager {
258 backup_dir: PathBuf,
259 data_dir: PathBuf,
260}
261
262impl BackupManager {
263 pub fn new(data_dir: PathBuf) -> Self {
265 let backup_dir = data_dir.join("backups");
266 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 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 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 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 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 let files_to_backup = vec!["kv_store.json", "sql_tables.json"];
316
317 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 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 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 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 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 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 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 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 let mut file_hasher = Sha256::new();
463 file_hasher.update(&contents);
464 let file_checksum = format!("{:x}", file_hasher.finalize());
465
466 hasher.update(&contents);
468
469 let data_to_write = if encrypt {
471 encrypt_aes256gcm(&contents)?
472 } else {
473 contents.clone()
474 };
475
476 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 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 fn compress_backup(&self, _backup_path: &StdPath) -> Result<(), String> {
504 tracing::info!("Compression requested but not fully implemented yet");
507 Ok(())
508 }
509
510 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 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 backups.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
562
563 Ok(backups)
564 }
565
566 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 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 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 fn verify_backup_integrity(&self, backup_path: &StdPath, metadata: &BackupMetadata) -> bool {
624 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; }
632
633 let files_to_check = if metadata.files.len() > 10 {
635 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 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 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 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 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 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 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 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 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 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 fn restore_file(
788 &self,
789 source: &StdPath,
790 target: &StdPath,
791 encrypted: bool,
792 ) -> Result<(), String> {
793 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 let data_to_write = if encrypted {
803 decrypt_aes256gcm(&contents)?
804 } else {
805 contents
806 };
807
808 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 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
832pub 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 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 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
904pub async fn list_backups(State(state): State<AppState>) -> impl IntoResponse {
906 state.activity.log(ActivityType::Query, "Listing backups");
907
908 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
942pub 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 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
1007pub 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 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#[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 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 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 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 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 let backup = manager
1112 .create_backup(false, None, false, None)
1113 .expect("Failed to create backup");
1114
1115 fs::write(&kv_path, r#"[{"key": "modified"}]"#).expect("Failed to modify data");
1117
1118 let files_restored = manager
1120 .restore_backup(&backup.id, false)
1121 .expect("Failed to restore");
1122 assert!(files_restored > 0);
1123
1124 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 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 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 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 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 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 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 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 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 assert_ne!(encrypted_contents.as_slice(), test_data.as_bytes());
1203 assert!(encrypted_contents.len() > test_data.len());
1204
1205 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 let files_restored = manager
1211 .restore_backup(&backup.id, false)
1212 .expect("Failed to restore encrypted backup");
1213 assert!(files_restored > 0);
1214
1215 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 fs::write(data_dir.join("kv_store.json"), "{}").expect("Failed to write test data");
1236
1237 let manager = BackupManager::new(data_dir);
1238
1239 let backup = manager
1241 .create_backup(false, None, true, None)
1242 .expect("Failed to create backup");
1243
1244 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 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 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 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 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 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 let result = manager.create_backup(false, None, true, None);
1306
1307 match result {
1309 Ok(backup) => {
1310 assert!(backup.encrypted);
1311 }
1312 Err(e) => {
1313 assert!(e.contains("AEGIS_ENCRYPTION_KEY"));
1314 }
1315 }
1316 }
1317}