Skip to main content

rusmes_cli/commands/
restore.rs

1//! Restore commands with full implementation
2//!
3//! Supports:
4//! - Full and selective restore
5//! - Point-in-time restore
6//! - Decryption (AES-256-GCM with Argon2)
7//! - Decompression (zstd, gzip, none)
8//! - S3/Object storage download
9//! - Verification and dry-run mode
10
11use 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/// Restore from a backup
44#[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/// Restore for a specific user
82#[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/// Restore specific mailboxes
120#[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
248/// Read compression type from backup companion manifest file.
249/// Falls back to detecting from the file extension if manifest not found.
250fn read_backup_manifest_compression(backup_path: &Path) -> Result<CompressionType> {
251    // Try companion manifest file: {backup}.manifest.json
252    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    // Fall back to file extension detection
268    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        // Default to no compression
278        Ok(CompressionType::None)
279    }
280}
281
282/// Perform local restore (standalone implementation)
283#[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    // Decrypt if needed
304    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    // Determine compression from companion manifest file or file extension fallback
312    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    // Extract tar archive
318    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        // Apply filters
335        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    // Check user filter
366    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    // Check mailbox filter
376    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/// Download backup from S3 and restore
399#[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/// Download from S3 to local file
462#[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
509/// Show restore history
510pub 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
540/// Show details of a specific restore
541pub 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        // Create a simple backup for testing
744        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        // Dry run restore
760        let stats = restore_local(
761            &backup_path,
762            &target_dir,
763            None,
764            None,
765            None,
766            true, // dry_run
767        )
768        .unwrap();
769
770        assert!(stats.files_restored > 0);
771        assert!(!target_dir.exists()); // Should not create directory in dry run
772    }
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        // Create a simple backup
781        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        // Full restore
797        let stats = restore_local(
798            &backup_path,
799            &target_dir,
800            None,
801            None,
802            None,
803            false, // not dry_run
804        )
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        // Create encrypted backup
819        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        // Restore with password
835        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        // Create encrypted backup
851        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        // Try to restore with wrong password
867        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        // Create backup with multiple users
886        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        // Restore only alice
912        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}