use crate::error::{Result, SyncorError};
use chkpt_core::scanner::scan_workspace;
use chkpt_core::store::blob::bytes_to_hex;
use chkpt_core::store::catalog::MetadataCatalog;
use chkpt_core::store::pack::PackSet;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
pub fn validate_path(base: &Path, relative: &str) -> Result<PathBuf> {
if relative.starts_with('/') || relative.starts_with('\\') || relative.contains("..") {
return Err(SyncorError::Other(format!(
"unsafe path in manifest: {}",
relative,
)));
}
let dest = base.join(relative);
Ok(dest)
}
pub struct RestoreResult {
pub files_restored: usize,
pub files_removed: usize,
}
pub struct RestorePipeline;
impl RestorePipeline {
pub fn run(snapshot_id: &str, store_dir: &Path, target_dir: &Path) -> Result<RestoreResult> {
let catalog_path = store_dir.join("catalog.sqlite");
let catalog = MetadataCatalog::open(&catalog_path)?;
let manifest = catalog.snapshot_manifest(snapshot_id)?;
let packs_dir = store_dir.join("packs");
let pack_set = PackSet::open_all(&packs_dir)?;
let manifest_paths: HashSet<String> = manifest.iter().map(|e| e.path.clone()).collect();
let mut files_restored = 0;
for entry in &manifest {
let hash_hex = bytes_to_hex(&entry.blob_hash);
let content = pack_set.read(&hash_hex)?;
let dest = validate_path(target_dir, &entry.path)?;
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&dest, &content)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(entry.mode);
std::fs::set_permissions(&dest, perms)?;
}
files_restored += 1;
}
let scanned = scan_workspace(target_dir, None)?;
let mut files_removed = 0;
for file in &scanned {
if !manifest_paths.contains(&file.relative_path) {
let path = validate_path(target_dir, &file.relative_path)?;
let _ = std::fs::remove_file(&path);
files_removed += 1;
}
}
remove_empty_dirs(target_dir, target_dir)?;
Ok(RestoreResult {
files_restored,
files_removed,
})
}
}
fn remove_empty_dirs(root: &Path, dir: &Path) -> Result<()> {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(err) => return Err(err.into()),
};
for entry in entries {
let entry = entry?;
let path = entry.path();
if entry.file_type()?.is_dir() {
remove_empty_dirs(root, &path)?;
if path != root {
let _ = std::fs::remove_dir(&path);
}
}
}
Ok(())
}