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 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/// Restore from a backup
42#[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/// Restore for a specific user
80#[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/// Restore specific mailboxes
118#[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
246/// Read compression type from backup companion manifest file.
247/// Falls back to detecting from the file extension if manifest not found.
248fn read_backup_manifest_compression(backup_path: &Path) -> Result<CompressionType> {
249    // Try companion manifest file: {backup}.manifest.json
250    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    // Fall back to file extension detection
266    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        // Default to no compression
276        Ok(CompressionType::None)
277    }
278}
279
280/// Perform local restore (standalone implementation)
281#[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    // Decrypt if needed
302    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    // Determine compression from companion manifest file or file extension fallback
310    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    // Extract tar archive
316    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        // 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 && 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    // Check user filter
372    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    // Check mailbox filter
382    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/// Download backup from S3 and restore
405#[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/// Download from S3 to local file
468#[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
515/// Show restore history
516pub 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
546/// Show details of a specific restore
547pub 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        // Create a simple backup for testing
750        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        // Dry run restore
766        let stats = restore_local(
767            &backup_path,
768            &target_dir,
769            None,
770            None,
771            None,
772            true, // dry_run
773        )
774        .unwrap();
775
776        assert!(stats.files_restored > 0);
777        assert!(!target_dir.exists()); // Should not create directory in dry run
778    }
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        // Create a simple backup
787        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        // Full restore
803        let stats = restore_local(
804            &backup_path,
805            &target_dir,
806            None,
807            None,
808            None,
809            false, // not dry_run
810        )
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        // Create encrypted backup
825        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        // Restore with password
841        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        // Create encrypted backup
857        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        // Try to restore with wrong password
873        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        // Create backup with multiple users
892        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        // Restore only alice
918        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}