syncor_core/sync/
restore.rs1use 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
9pub fn validate_path(base: &Path, relative: &str) -> Result<PathBuf> {
11 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 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 let catalog_path = store_dir.join("catalog.sqlite");
35 let catalog = MetadataCatalog::open(&catalog_path)?;
36
37 let manifest = catalog.snapshot_manifest(snapshot_id)?;
39
40 let packs_dir = store_dir.join("packs");
42 let pack_set = PackSet::open_all(&packs_dir)?;
43
44 let manifest_paths: HashSet<String> = manifest.iter().map(|e| e.path.clone()).collect();
46
47 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 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 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 if path != root {
104 let _ = std::fs::remove_dir(&path);
106 }
107 }
108 }
109
110 Ok(())
111}