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