use std::fs;
use std::io;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SnapshotOutcome {
Reflinked,
Copied,
}
pub fn clone_tree(src: &Path, dst: &Path) -> io::Result<SnapshotOutcome> {
if dst.exists() {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("snapshot destination already exists: {}", dst.display()),
));
}
let mut state = WalkState::default();
walk(src, dst, &mut state)?;
Ok(if state.reflinked > 0 {
SnapshotOutcome::Reflinked
} else {
SnapshotOutcome::Copied
})
}
#[derive(Default)]
struct WalkState {
reflinked: u64,
copied: u64,
}
fn snapshots_disabled() -> bool {
crate::env::embedder_env("DISABLE_SNAPSHOTS").is_some()
}
fn walk(src: &Path, dst: &Path, state: &mut WalkState) -> io::Result<()> {
let metadata = fs::symlink_metadata(src)?;
let file_type = metadata.file_type();
if file_type.is_symlink() {
let target = fs::read_link(src)?;
symlink(&target, dst)?;
return Ok(());
}
if file_type.is_dir() {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
walk(&entry.path(), &dst.join(entry.file_name()), state)?;
}
return Ok(());
}
if snapshots_disabled() {
fs::copy(src, dst)?;
state.copied += 1;
return Ok(());
}
match reflink_copy::reflink_or_copy(src, dst) {
Ok(Some(_)) => state.copied += 1,
Ok(None) => state.reflinked += 1,
Err(e) => return Err(e),
}
Ok(())
}
#[cfg(unix)]
fn symlink(target: &Path, link: &Path) -> io::Result<()> {
std::os::unix::fs::symlink(target, link)
}
#[cfg(windows)]
fn symlink(target: &Path, link: &Path) -> io::Result<()> {
let resolved = link
.parent()
.map(|p| p.join(target))
.unwrap_or_else(|| target.to_path_buf());
let is_dir = std::fs::metadata(&resolved)
.map(|m| m.is_dir())
.unwrap_or(false);
if is_dir {
std::os::windows::fs::symlink_dir(target, link)
} else {
std::os::windows::fs::symlink_file(target, link)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn clone_tree_reproduces_files() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
let dst = dir.path().join("dst");
fs::create_dir_all(src.join("nested")).unwrap();
let mut a = fs::File::create(src.join("a.txt")).unwrap();
a.write_all(b"alpha").unwrap();
let mut b = fs::File::create(src.join("nested").join("b.txt")).unwrap();
b.write_all(b"beta").unwrap();
let outcome = clone_tree(&src, &dst).unwrap();
assert!(matches!(
outcome,
SnapshotOutcome::Reflinked | SnapshotOutcome::Copied
));
assert_eq!(fs::read(dst.join("a.txt")).unwrap(), b"alpha");
assert_eq!(fs::read(dst.join("nested").join("b.txt")).unwrap(), b"beta");
}
#[test]
fn clone_tree_refuses_existing_destination() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
let dst = dir.path().join("dst");
fs::create_dir_all(&src).unwrap();
fs::create_dir_all(&dst).unwrap();
let err = clone_tree(&src, &dst).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
}
#[test]
fn clone_tree_handles_empty_source() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
let dst = dir.path().join("dst");
fs::create_dir_all(&src).unwrap();
let outcome = clone_tree(&src, &dst).unwrap();
assert_eq!(outcome, SnapshotOutcome::Copied);
assert!(dst.exists());
}
}