1use super::backup::CompressionType;
12use crate::client::Client;
13use crate::commands::backup;
14use anyhow::{Context, Result};
15use colored::*;
16use indicatif::{ProgressBar, ProgressStyle};
17use serde::{Deserialize, Serialize};
18use std::collections::HashSet;
19use std::fs;
20use std::path::Path;
21use tabled::Tabled;
22
23#[derive(Debug, Serialize, Deserialize, Clone)]
24pub struct RestoreOptions {
25 pub backup_path: String,
26 pub encryption_key: Option<String>,
27 pub password_file: Option<String>,
28 pub point_in_time: Option<String>,
29 pub restore_messages: bool,
30 pub restore_mailboxes: bool,
31 pub restore_config: bool,
32 pub restore_metadata: bool,
33 pub target_users: Option<Vec<String>>,
34 pub target_mailboxes: Option<Vec<String>>,
35 pub before_timestamp: Option<String>,
36 pub dry_run: bool,
37 pub verify: bool,
38 pub target_backend: Option<String>,
39}
40
41#[allow(clippy::too_many_arguments)]
43pub async fn restore(
44 client: &Client,
45 backup_path: &str,
46 encryption_key: Option<&str>,
47 password_file: Option<&str>,
48 point_in_time: Option<&str>,
49 dry_run: bool,
50 verify: bool,
51 json: bool,
52) -> Result<()> {
53 let key = if let Some(pwd_file) = password_file {
54 Some(read_password_file(pwd_file)?)
55 } else {
56 encryption_key.map(String::from)
57 };
58
59 let options = RestoreOptions {
60 backup_path: backup_path.to_string(),
61 encryption_key: key,
62 password_file: password_file.map(String::from),
63 point_in_time: point_in_time.map(|s| s.to_string()),
64 restore_messages: true,
65 restore_mailboxes: true,
66 restore_config: true,
67 restore_metadata: true,
68 target_users: None,
69 target_mailboxes: None,
70 before_timestamp: None,
71 dry_run,
72 verify,
73 target_backend: None,
74 };
75
76 perform_restore(client, &options, json).await
77}
78
79#[allow(clippy::too_many_arguments)]
81pub async fn restore_user(
82 client: &Client,
83 backup_path: &str,
84 user: &str,
85 encryption_key: Option<&str>,
86 password_file: Option<&str>,
87 dry_run: bool,
88 verify: bool,
89 json: bool,
90) -> Result<()> {
91 let key = if let Some(pwd_file) = password_file {
92 Some(read_password_file(pwd_file)?)
93 } else {
94 encryption_key.map(String::from)
95 };
96
97 let options = RestoreOptions {
98 backup_path: backup_path.to_string(),
99 encryption_key: key,
100 password_file: password_file.map(String::from),
101 point_in_time: None,
102 restore_messages: true,
103 restore_mailboxes: true,
104 restore_config: false,
105 restore_metadata: true,
106 target_users: Some(vec![user.to_string()]),
107 target_mailboxes: None,
108 before_timestamp: None,
109 dry_run,
110 verify,
111 target_backend: None,
112 };
113
114 perform_restore(client, &options, json).await
115}
116
117#[allow(clippy::too_many_arguments)]
119pub async fn restore_mailboxes(
120 client: &Client,
121 backup_path: &str,
122 mailboxes: &[String],
123 encryption_key: Option<&str>,
124 password_file: Option<&str>,
125 dry_run: bool,
126 verify: bool,
127 json: bool,
128) -> Result<()> {
129 let key = if let Some(pwd_file) = password_file {
130 Some(read_password_file(pwd_file)?)
131 } else {
132 encryption_key.map(String::from)
133 };
134
135 let options = RestoreOptions {
136 backup_path: backup_path.to_string(),
137 encryption_key: key,
138 password_file: password_file.map(String::from),
139 point_in_time: None,
140 restore_messages: true,
141 restore_mailboxes: true,
142 restore_config: false,
143 restore_metadata: true,
144 target_users: None,
145 target_mailboxes: Some(mailboxes.to_vec()),
146 before_timestamp: None,
147 dry_run,
148 verify,
149 target_backend: None,
150 };
151
152 perform_restore(client, &options, json).await
153}
154
155async fn perform_restore(client: &Client, options: &RestoreOptions, json: bool) -> Result<()> {
156 #[derive(Deserialize, Serialize)]
157 struct RestoreResponse {
158 restore_id: String,
159 messages_restored: u64,
160 mailboxes_restored: u32,
161 users_restored: u32,
162 duration_secs: f64,
163 errors: Vec<String>,
164 warnings: Vec<String>,
165 }
166
167 if !json {
168 if options.dry_run {
169 println!("{}", "DRY RUN: No changes will be made".yellow().bold());
170 }
171 println!("{}", "Restoring from backup...".blue().bold());
172 println!(" Backup: {}", options.backup_path);
173 if let Some(users) = &options.target_users {
174 println!(" Target users: {}", users.join(", "));
175 }
176 if let Some(mailboxes) = &options.target_mailboxes {
177 println!(" Target mailboxes: {}", mailboxes.join(", "));
178 }
179 if let Some(pit) = &options.point_in_time {
180 println!(" Point-in-time: {}", pit);
181 }
182 println!(
183 " Messages: {}",
184 if options.restore_messages {
185 "Yes"
186 } else {
187 "No"
188 }
189 );
190 println!(
191 " Mailboxes: {}",
192 if options.restore_mailboxes {
193 "Yes"
194 } else {
195 "No"
196 }
197 );
198 println!(
199 " Config: {}",
200 if options.restore_config { "Yes" } else { "No" }
201 );
202 println!(
203 " Metadata: {}",
204 if options.restore_metadata {
205 "Yes"
206 } else {
207 "No"
208 }
209 );
210 }
211
212 let response: RestoreResponse = client.post("/api/restore", options).await?;
213
214 if json {
215 println!("{}", serde_json::to_string_pretty(&response)?);
216 } else {
217 if options.dry_run {
218 println!("{}", "✓ Dry run completed".green().bold());
219 } else {
220 println!("{}", "✓ Restore completed successfully".green().bold());
221 }
222 println!(" Restore ID: {}", response.restore_id);
223 println!(" Messages restored: {}", response.messages_restored);
224 println!(" Mailboxes restored: {}", response.mailboxes_restored);
225 println!(" Users restored: {}", response.users_restored);
226 println!(" Duration: {:.2}s", response.duration_secs);
227
228 if !response.errors.is_empty() {
229 println!("\n{}", "Errors:".red().bold());
230 for error in &response.errors {
231 println!(" - {}", error);
232 }
233 }
234
235 if !response.warnings.is_empty() {
236 println!("\n{}", "Warnings:".yellow().bold());
237 for warning in &response.warnings {
238 println!(" - {}", warning);
239 }
240 }
241 }
242
243 Ok(())
244}
245
246fn read_backup_manifest_compression(backup_path: &Path) -> Result<CompressionType> {
249 let manifest_path = backup_path.with_extension(
251 backup_path
252 .extension()
253 .and_then(|e| e.to_str())
254 .map(|e| format!("{}.manifest.json", e))
255 .unwrap_or_else(|| "manifest.json".to_string()),
256 );
257
258 if manifest_path.exists() {
259 let data = fs::read(&manifest_path)?;
260 if let Ok(manifest) = serde_json::from_slice::<backup::BackupManifest>(&data) {
261 return Ok(manifest.compression);
262 }
263 }
264
265 let name = backup_path
267 .file_name()
268 .and_then(|n| n.to_str())
269 .unwrap_or("");
270 if name.contains(".tar.gz") || name.ends_with(".tgz") {
271 Ok(CompressionType::Gzip)
272 } else if name.contains(".tar.zst") || name.ends_with(".tzst") {
273 Ok(CompressionType::Zstd)
274 } else {
275 Ok(CompressionType::None)
277 }
278}
279
280#[allow(clippy::too_many_arguments)]
282pub fn restore_local(
283 backup_path: &Path,
284 target_dir: &Path,
285 password: Option<&str>,
286 filter_users: Option<&HashSet<String>>,
287 filter_mailboxes: Option<&HashSet<String>>,
288 dry_run: bool,
289) -> Result<RestoreStats> {
290 let pb = ProgressBar::new(100);
291 pb.set_style(
292 ProgressStyle::default_bar()
293 .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}")
294 .expect("invalid template")
295 .progress_chars("##-"),
296 );
297
298 pb.set_message("Reading backup file...");
299 let data = fs::read(backup_path)?;
300
301 let decrypted_data = if let Some(pwd) = password {
303 pb.set_message("Decrypting...");
304 backup::decrypt_data(&data, pwd)?
305 } else {
306 data
307 };
308
309 pb.set_message("Reading manifest...");
311 let compression = read_backup_manifest_compression(backup_path)?;
312 pb.set_message("Decompressing...");
313 let decompressed_data = backup::decompress_data(&decrypted_data, compression)?;
314
315 pb.set_message("Extracting files...");
317 let cursor = std::io::Cursor::new(decompressed_data);
318 let mut tar_reader = oxiarc_archive::TarReader::new(cursor)
319 .map_err(|e| anyhow::anyhow!("Failed to read tar archive: {}", e))?;
320
321 let mut stats = RestoreStats {
322 files_restored: 0,
323 bytes_restored: 0,
324 messages_restored: 0,
325 mailboxes_restored: 0,
326 users_restored: 0,
327 skipped: 0,
328 };
329
330 let entries = tar_reader.entries().to_vec();
331 for entry in &entries {
332 let path = Path::new(&entry.name);
333
334 let should_restore = should_restore_file(path, filter_users, filter_mailboxes);
336
337 if !should_restore {
338 stats.skipped += 1;
339 continue;
340 }
341
342 if !dry_run && entry.is_file() {
343 let target_path = target_dir.join(path);
344 if let Some(parent) = target_path.parent() {
345 fs::create_dir_all(parent)?;
346 }
347 let data = tar_reader
348 .extract_to_vec(entry)
349 .map_err(|e| anyhow::anyhow!("Failed to extract entry: {}", e))?;
350 fs::write(&target_path, &data)?;
351 } else if !dry_run && entry.is_dir() {
352 let target_path = target_dir.join(path);
353 fs::create_dir_all(&target_path)?;
354 }
355
356 stats.files_restored += 1;
357 stats.bytes_restored += entry.size;
358 pb.inc(1);
359 }
360
361 pb.finish_with_message("Restore completed!");
362
363 Ok(stats)
364}
365
366fn should_restore_file(
367 path: &Path,
368 filter_users: Option<&HashSet<String>>,
369 filter_mailboxes: Option<&HashSet<String>>,
370) -> bool {
371 if let Some(users) = filter_users {
373 if let Some(user_component) = path.components().nth(1) {
374 let user_str = user_component.as_os_str().to_string_lossy();
375 if !users.contains(user_str.as_ref()) {
376 return false;
377 }
378 }
379 }
380
381 if let Some(mailboxes) = filter_mailboxes {
383 if let Some(mailbox_component) = path.components().nth(2) {
384 let mailbox_str = mailbox_component.as_os_str().to_string_lossy();
385 if !mailboxes.contains(mailbox_str.as_ref()) {
386 return false;
387 }
388 }
389 }
390
391 true
392}
393
394#[derive(Debug, Clone)]
395pub struct RestoreStats {
396 pub files_restored: u64,
397 pub bytes_restored: u64,
398 pub messages_restored: u64,
399 pub mailboxes_restored: u32,
400 pub users_restored: u32,
401 pub skipped: u64,
402}
403
404#[allow(clippy::too_many_arguments)]
406pub async fn restore_from_s3(
407 client: &Client,
408 s3_url: &str,
409 bucket: &str,
410 region: &str,
411 access_key: &str,
412 secret_key: &str,
413 encryption_key: Option<&str>,
414 json: bool,
415) -> Result<()> {
416 #[derive(Serialize)]
417 struct S3RestoreRequest {
418 s3_url: String,
419 bucket: String,
420 region: String,
421 access_key: String,
422 secret_key: String,
423 encryption_key: Option<String>,
424 }
425
426 #[derive(Deserialize, Serialize)]
427 struct S3RestoreResponse {
428 restore_id: String,
429 downloaded_size_bytes: u64,
430 messages_restored: u64,
431 mailboxes_restored: u32,
432 duration_secs: f64,
433 }
434
435 let request = S3RestoreRequest {
436 s3_url: s3_url.to_string(),
437 bucket: bucket.to_string(),
438 region: region.to_string(),
439 access_key: access_key.to_string(),
440 secret_key: secret_key.to_string(),
441 encryption_key: encryption_key.map(|s| s.to_string()),
442 };
443
444 if !json {
445 println!("{}", "Downloading and restoring from S3...".blue().bold());
446 }
447
448 let response: S3RestoreResponse = client.post("/api/restore/from-s3", &request).await?;
449
450 if json {
451 println!("{}", serde_json::to_string_pretty(&response)?);
452 } else {
453 println!("{}", "✓ Restore completed successfully".green().bold());
454 println!(" Restore ID: {}", response.restore_id);
455 println!(
456 " Downloaded: {} MB",
457 response.downloaded_size_bytes / (1024 * 1024)
458 );
459 println!(" Messages: {}", response.messages_restored);
460 println!(" Mailboxes: {}", response.mailboxes_restored);
461 println!(" Duration: {:.2}s", response.duration_secs);
462 }
463
464 Ok(())
465}
466
467#[allow(clippy::too_many_arguments)]
469#[allow(dead_code)]
470pub async fn download_from_s3(
471 bucket: &str,
472 key: &str,
473 region: &str,
474 endpoint: Option<&str>,
475 #[allow(unused_variables)] access_key: &str,
476 #[allow(unused_variables)] secret_key: &str,
477 output_path: &Path,
478) -> Result<()> {
479 use aws_config::BehaviorVersion;
480 use aws_sdk_s3::Client as S3Client;
481
482 let config = if let Some(ep) = endpoint {
483 aws_config::defaults(BehaviorVersion::latest())
484 .region(aws_config::Region::new(region.to_string()))
485 .endpoint_url(ep)
486 .load()
487 .await
488 } else {
489 aws_config::defaults(BehaviorVersion::latest())
490 .region(aws_config::Region::new(region.to_string()))
491 .load()
492 .await
493 };
494
495 let s3_client = S3Client::new(&config);
496
497 let pb = ProgressBar::new_spinner();
498 pb.set_message("Downloading from S3...");
499
500 let response = s3_client
501 .get_object()
502 .bucket(bucket)
503 .key(key)
504 .send()
505 .await?;
506
507 let data = response.body.collect().await?;
508 fs::write(output_path, data.into_bytes())?;
509
510 pb.finish_with_message("Download completed!");
511
512 Ok(())
513}
514
515pub async fn history(client: &Client, json: bool) -> Result<()> {
517 #[derive(Deserialize, Serialize, Tabled)]
518 struct RestoreHistoryItem {
519 restore_id: String,
520 backup_path: String,
521 restored_at: String,
522 messages: u64,
523 mailboxes: u32,
524 status: String,
525 }
526
527 let history: Vec<RestoreHistoryItem> = client.get("/api/restore/history").await?;
528
529 if json {
530 println!("{}", serde_json::to_string_pretty(&history)?);
531 } else {
532 if history.is_empty() {
533 println!("{}", "No restore history found".yellow());
534 return Ok(());
535 }
536
537 use tabled::Table;
538 let table = Table::new(&history).to_string();
539 println!("{}", table);
540 println!("\n{} restores", history.len().to_string().bold());
541 }
542
543 Ok(())
544}
545
546pub async fn show_restore(client: &Client, restore_id: &str, json: bool) -> Result<()> {
548 #[derive(Deserialize, Serialize)]
549 struct RestoreDetails {
550 restore_id: String,
551 backup_path: String,
552 restored_at: String,
553 completed_at: Option<String>,
554 status: String,
555 messages_restored: u64,
556 mailboxes_restored: u32,
557 users_restored: u32,
558 duration_secs: f64,
559 errors: Vec<String>,
560 warnings: Vec<String>,
561 }
562
563 let details: RestoreDetails = client.get(&format!("/api/restore/{}", restore_id)).await?;
564
565 if json {
566 println!("{}", serde_json::to_string_pretty(&details)?);
567 } else {
568 println!("{}", format!("Restore: {}", restore_id).bold());
569 println!(" Backup: {}", details.backup_path);
570 println!(" Started: {}", details.restored_at);
571 if let Some(completed) = &details.completed_at {
572 println!(" Completed: {}", completed);
573 }
574 println!(
575 " Status: {}",
576 match details.status.as_str() {
577 "completed" => details.status.green(),
578 "failed" => details.status.red(),
579 "running" => details.status.blue(),
580 _ => details.status.normal(),
581 }
582 );
583 println!(" Messages: {}", details.messages_restored);
584 println!(" Mailboxes: {}", details.mailboxes_restored);
585 println!(" Users: {}", details.users_restored);
586 println!(" Duration: {:.2}s", details.duration_secs);
587
588 if !details.errors.is_empty() {
589 println!("\n{}", "Errors:".red().bold());
590 for error in &details.errors {
591 println!(" - {}", error);
592 }
593 }
594
595 if !details.warnings.is_empty() {
596 println!("\n{}", "Warnings:".yellow().bold());
597 for warning in &details.warnings {
598 println!(" - {}", warning);
599 }
600 }
601 }
602
603 Ok(())
604}
605
606fn read_password_file(path: &str) -> Result<String> {
607 let content = fs::read_to_string(path)
608 .with_context(|| format!("Failed to read password file: {}", path))?;
609 Ok(content.trim().to_string())
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615 use crate::commands::backup;
616 use tempfile::TempDir;
617
618 #[test]
619 fn test_restore_options_serialization() {
620 let options = RestoreOptions {
621 backup_path: "/tmp/backup.tar.gz".to_string(),
622 encryption_key: Some("key123".to_string()),
623 password_file: None,
624 point_in_time: None,
625 restore_messages: true,
626 restore_mailboxes: true,
627 restore_config: false,
628 restore_metadata: true,
629 target_users: Some(vec!["user@example.com".to_string()]),
630 target_mailboxes: None,
631 before_timestamp: None,
632 dry_run: false,
633 verify: true,
634 target_backend: None,
635 };
636
637 let json = serde_json::to_string(&options).unwrap();
638 assert!(json.contains("backup.tar.gz"));
639 assert!(json.contains("user@example.com"));
640 }
641
642 #[test]
643 fn test_restore_options_defaults() {
644 let options = RestoreOptions {
645 backup_path: "/tmp/backup.tar.gz".to_string(),
646 encryption_key: None,
647 password_file: None,
648 point_in_time: None,
649 restore_messages: true,
650 restore_mailboxes: true,
651 restore_config: true,
652 restore_metadata: true,
653 target_users: None,
654 target_mailboxes: None,
655 before_timestamp: None,
656 dry_run: true,
657 verify: false,
658 target_backend: None,
659 };
660
661 assert!(options.dry_run);
662 assert!(options.restore_messages);
663 assert!(options.encryption_key.is_none());
664 }
665
666 #[test]
667 fn test_restore_options_selective() {
668 let options = RestoreOptions {
669 backup_path: "/tmp/backup.tar.gz".to_string(),
670 encryption_key: None,
671 password_file: None,
672 point_in_time: Some("2024-02-15T10:00:00Z".to_string()),
673 restore_messages: true,
674 restore_mailboxes: false,
675 restore_config: false,
676 restore_metadata: true,
677 target_users: Some(vec![
678 "alice@example.com".to_string(),
679 "bob@example.com".to_string(),
680 ]),
681 target_mailboxes: Some(vec!["INBOX".to_string(), "Sent".to_string()]),
682 before_timestamp: Some("2024-02-16T00:00:00Z".to_string()),
683 dry_run: false,
684 verify: true,
685 target_backend: Some("postgres".to_string()),
686 };
687
688 assert!(options.point_in_time.is_some());
689 assert_eq!(options.target_users.as_ref().unwrap().len(), 2);
690 assert_eq!(options.target_mailboxes.as_ref().unwrap().len(), 2);
691 assert!(options.verify);
692 }
693
694 #[test]
695 fn test_should_restore_file_no_filter() {
696 let path = Path::new("users/alice@example.com/INBOX/msg1.eml");
697 assert!(should_restore_file(path, None, None));
698 }
699
700 #[test]
701 fn test_should_restore_file_user_filter() {
702 let path = Path::new("users/alice@example.com/INBOX/msg1.eml");
703 let mut users = HashSet::new();
704 users.insert("alice@example.com".to_string());
705
706 assert!(should_restore_file(path, Some(&users), None));
707
708 users.clear();
709 users.insert("bob@example.com".to_string());
710 assert!(!should_restore_file(path, Some(&users), None));
711 }
712
713 #[test]
714 fn test_should_restore_file_mailbox_filter() {
715 let path = Path::new("users/alice@example.com/INBOX/msg1.eml");
716 let mut mailboxes = HashSet::new();
717 mailboxes.insert("INBOX".to_string());
718
719 assert!(should_restore_file(path, None, Some(&mailboxes)));
720
721 mailboxes.clear();
722 mailboxes.insert("Sent".to_string());
723 assert!(!should_restore_file(path, None, Some(&mailboxes)));
724 }
725
726 #[test]
727 fn test_restore_stats() {
728 let stats = RestoreStats {
729 files_restored: 100,
730 bytes_restored: 1024 * 1024 * 10,
731 messages_restored: 80,
732 mailboxes_restored: 5,
733 users_restored: 2,
734 skipped: 20,
735 };
736
737 assert_eq!(stats.files_restored, 100);
738 assert_eq!(stats.bytes_restored, 1024 * 1024 * 10);
739 assert_eq!(stats.messages_restored, 80);
740 assert_eq!(stats.skipped, 20);
741 }
742
743 #[test]
744 fn test_restore_local_dry_run() {
745 let temp_dir = TempDir::new().unwrap();
746 let backup_path = temp_dir.path().join("backup.tar.zst");
747 let target_dir = temp_dir.path().join("restore");
748
749 let source_dir = temp_dir.path().join("source");
751 fs::create_dir(&source_dir).unwrap();
752 fs::write(source_dir.join("test.txt"), b"Test content").unwrap();
753
754 let _manifest = backup::create_local_backup(
755 &source_dir,
756 &backup_path,
757 CompressionType::Zstd,
758 false,
759 None,
760 false,
761 None,
762 )
763 .unwrap();
764
765 let stats = restore_local(
767 &backup_path,
768 &target_dir,
769 None,
770 None,
771 None,
772 true, )
774 .unwrap();
775
776 assert!(stats.files_restored > 0);
777 assert!(!target_dir.exists()); }
779
780 #[test]
781 fn test_restore_local_full() {
782 let temp_dir = TempDir::new().unwrap();
783 let backup_path = temp_dir.path().join("backup.tar.zst");
784 let target_dir = temp_dir.path().join("restore");
785
786 let source_dir = temp_dir.path().join("source");
788 fs::create_dir(&source_dir).unwrap();
789 fs::write(source_dir.join("test.txt"), b"Test content").unwrap();
790
791 let _manifest = backup::create_local_backup(
792 &source_dir,
793 &backup_path,
794 CompressionType::Zstd,
795 false,
796 None,
797 false,
798 None,
799 )
800 .unwrap();
801
802 let stats = restore_local(
804 &backup_path,
805 &target_dir,
806 None,
807 None,
808 None,
809 false, )
811 .unwrap();
812
813 assert!(stats.files_restored > 0);
814 assert!(target_dir.join("test.txt").exists());
815 }
816
817 #[test]
818 fn test_restore_local_encrypted() {
819 let temp_dir = TempDir::new().unwrap();
820 let backup_path = temp_dir.path().join("backup.tar.zst.enc");
821 let target_dir = temp_dir.path().join("restore");
822 let password = "TestPassword123";
823
824 let source_dir = temp_dir.path().join("source");
826 fs::create_dir(&source_dir).unwrap();
827 fs::write(source_dir.join("secret.txt"), b"Secret data").unwrap();
828
829 let _manifest = backup::create_local_backup(
830 &source_dir,
831 &backup_path,
832 CompressionType::Zstd,
833 true,
834 Some(password),
835 false,
836 None,
837 )
838 .unwrap();
839
840 let stats =
842 restore_local(&backup_path, &target_dir, Some(password), None, None, false).unwrap();
843
844 assert!(stats.files_restored > 0);
845 assert!(target_dir.join("secret.txt").exists());
846 }
847
848 #[test]
849 fn test_restore_local_wrong_password() {
850 let temp_dir = TempDir::new().unwrap();
851 let backup_path = temp_dir.path().join("backup.tar.zst.enc");
852 let target_dir = temp_dir.path().join("restore");
853 let password = "CorrectPassword";
854 let wrong_password = "WrongPassword";
855
856 let source_dir = temp_dir.path().join("source");
858 fs::create_dir(&source_dir).unwrap();
859 fs::write(source_dir.join("secret.txt"), b"Secret data").unwrap();
860
861 let _manifest = backup::create_local_backup(
862 &source_dir,
863 &backup_path,
864 CompressionType::Zstd,
865 true,
866 Some(password),
867 false,
868 None,
869 )
870 .unwrap();
871
872 let result = restore_local(
874 &backup_path,
875 &target_dir,
876 Some(wrong_password),
877 None,
878 None,
879 false,
880 );
881
882 assert!(result.is_err());
883 }
884
885 #[test]
886 fn test_restore_local_selective_users() {
887 let temp_dir = TempDir::new().unwrap();
888 let backup_path = temp_dir.path().join("backup.tar.zst");
889 let target_dir = temp_dir.path().join("restore");
890
891 let source_dir = temp_dir.path().join("source");
893 fs::create_dir_all(source_dir.join("users/alice@example.com")).unwrap();
894 fs::create_dir_all(source_dir.join("users/bob@example.com")).unwrap();
895 fs::write(
896 source_dir.join("users/alice@example.com/msg1.eml"),
897 b"Alice's message",
898 )
899 .unwrap();
900 fs::write(
901 source_dir.join("users/bob@example.com/msg1.eml"),
902 b"Bob's message",
903 )
904 .unwrap();
905
906 let _manifest = backup::create_local_backup(
907 &source_dir,
908 &backup_path,
909 CompressionType::Zstd,
910 false,
911 None,
912 false,
913 None,
914 )
915 .unwrap();
916
917 let mut users = HashSet::new();
919 users.insert("alice@example.com".to_string());
920
921 let stats =
922 restore_local(&backup_path, &target_dir, None, Some(&users), None, false).unwrap();
923
924 assert!(stats.files_restored > 0);
925 assert!(stats.skipped > 0);
926 }
927
928 #[test]
929 fn test_restore_options_with_backend() {
930 let options = RestoreOptions {
931 backup_path: "/tmp/backup.tar.gz".to_string(),
932 encryption_key: None,
933 password_file: None,
934 point_in_time: None,
935 restore_messages: true,
936 restore_mailboxes: true,
937 restore_config: true,
938 restore_metadata: true,
939 target_users: None,
940 target_mailboxes: None,
941 before_timestamp: None,
942 dry_run: false,
943 verify: false,
944 target_backend: Some("postgres".to_string()),
945 };
946
947 assert_eq!(options.target_backend.as_ref().unwrap(), "postgres");
948 }
949}