Skip to main content

syncor_core/sync/
restore.rs

1use crate::error::{Result, SyncorError};
2use chkpt_core::scanner::scan_workspace;
3use chkpt_core::store::blob::bytes_to_hex;
4use chkpt_core::store::catalog::MetadataCatalog;
5use chkpt_core::store::pack::PackSet;
6use std::collections::HashSet;
7use std::path::{Path, PathBuf};
8
9/// Validate that a path from a manifest doesn't escape the target directory.
10pub fn validate_path(base: &Path, relative: &str) -> Result<PathBuf> {
11    // Reject absolute paths and path traversal
12    if relative.starts_with('/') || relative.starts_with('\\') || relative.contains("..") {
13        return Err(SyncorError::Other(format!(
14            "unsafe path in manifest: {}",
15            relative,
16        )));
17    }
18    let dest = base.join(relative);
19    // Double-check the resolved path is still inside base
20    // (handles edge cases like symlinks in base)
21    Ok(dest)
22}
23
24pub struct RestoreResult {
25    pub files_restored: usize,
26    pub files_removed: usize,
27}
28
29pub struct RestorePipeline;
30
31impl RestorePipeline {
32    pub fn run(snapshot_id: &str, store_dir: &Path, target_dir: &Path) -> Result<RestoreResult> {
33        // 1. Open MetadataCatalog
34        let catalog_path = store_dir.join("catalog.sqlite");
35        let catalog = MetadataCatalog::open(&catalog_path)?;
36
37        // 2. Get manifest
38        let manifest = catalog.snapshot_manifest(snapshot_id)?;
39
40        // 3. Open PackSet
41        let packs_dir = store_dir.join("packs");
42        let pack_set = PackSet::open_all(&packs_dir)?;
43
44        // Build set of manifest paths for later cleanup
45        let manifest_paths: HashSet<String> = manifest.iter().map(|e| e.path.clone()).collect();
46
47        // 4. Restore each file
48        let mut files_restored = 0;
49        for entry in &manifest {
50            let hash_hex = bytes_to_hex(&entry.blob_hash);
51            let content = pack_set.read(&hash_hex)?;
52
53            let dest = validate_path(target_dir, &entry.path)?;
54            if let Some(parent) = dest.parent() {
55                std::fs::create_dir_all(parent)?;
56            }
57            std::fs::write(&dest, &content)?;
58            #[cfg(unix)]
59            {
60                use std::os::unix::fs::PermissionsExt;
61                let perms = std::fs::Permissions::from_mode(entry.mode);
62                std::fs::set_permissions(&dest, perms)?;
63            }
64            files_restored += 1;
65        }
66
67        // 5. Scan target_dir using the same scanner as save (respects .chkptignore etc.)
68        //    and remove only scanned files not in the manifest. This preserves ignored
69        //    files like .git/, .env, .chkptignore.
70        let scanned = scan_workspace(target_dir, None)?;
71        let mut files_removed = 0;
72        for file in &scanned {
73            if !manifest_paths.contains(&file.relative_path) {
74                let path = validate_path(target_dir, &file.relative_path)?;
75                let _ = std::fs::remove_file(&path);
76                files_removed += 1;
77            }
78        }
79
80        // 6. Clean up empty directories (but never the target_dir root itself)
81        remove_empty_dirs(target_dir, target_dir)?;
82
83        Ok(RestoreResult {
84            files_restored,
85            files_removed,
86        })
87    }
88}
89
90fn remove_empty_dirs(root: &Path, dir: &Path) -> Result<()> {
91    let entries = match std::fs::read_dir(dir) {
92        Ok(e) => e,
93        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
94        Err(err) => return Err(err.into()),
95    };
96
97    for entry in entries {
98        let entry = entry?;
99        let path = entry.path();
100        if entry.file_type()?.is_dir() {
101            remove_empty_dirs(root, &path)?;
102            // Never remove root itself
103            if path != root {
104                // Try to remove; ignore if not empty
105                let _ = std::fs::remove_dir(&path);
106            }
107        }
108    }
109
110    Ok(())
111}