use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use std::sync::atomic::{AtomicUsize, Ordering};
use anyhow::Context;
use rayon::prelude::*;
static COPY_POOL: LazyLock<rayon::ThreadPool> = LazyLock::new(|| {
rayon::ThreadPoolBuilder::new()
.num_threads(4)
.build()
.expect("failed to build copy thread pool")
});
pub fn copy_leaf(src: &Path, dest: &Path, force: bool) -> anyhow::Result<bool> {
if force {
remove_if_exists(dest)?;
}
if dest.symlink_metadata().is_ok() {
return Ok(false);
}
let is_symlink = src
.symlink_metadata()
.with_context(|| format!("reading metadata for {}", src.display()))?
.file_type()
.is_symlink();
if is_symlink {
let target =
fs::read_link(src).with_context(|| format!("reading symlink {}", src.display()))?;
create_symlink(&target, src, dest)?;
} else {
match reflink_copy::reflink_or_copy(src, dest) {
Ok(_) => {
#[cfg(unix)]
{
let perms = fs::metadata(src)
.context("reading source file permissions")?
.permissions();
fs::set_permissions(dest, perms)
.context("setting destination file permissions")?;
}
}
Err(e) if e.kind() == ErrorKind::AlreadyExists => return Ok(false),
Err(e) => {
return Err(anyhow::Error::from(e).context(format!("copying {}", src.display())));
}
}
}
Ok(true)
}
struct CopyLeaf {
src: PathBuf,
dest: PathBuf,
}
pub fn copy_dir_recursive(src: &Path, dest: &Path, force: bool) -> anyhow::Result<usize> {
let mut leaves = Vec::new();
let mut dir_stack = vec![(src.to_path_buf(), dest.to_path_buf())];
#[cfg(unix)]
let mut dirs_for_perms: Vec<(PathBuf, PathBuf)> = Vec::new();
while let Some((src_dir, dest_dir)) = dir_stack.pop() {
fs::create_dir_all(&dest_dir)
.with_context(|| format!("creating directory {}", dest_dir.display()))?;
#[cfg(unix)]
dirs_for_perms.push((src_dir.clone(), dest_dir.clone()));
let entries: Vec<_> = fs::read_dir(&src_dir)?.collect::<Result<Vec<_>, _>>()?;
for entry in entries {
let file_type = entry.file_type()?;
let src_path = entry.path();
let dest_path = dest_dir.join(entry.file_name());
if file_type.is_dir() {
dir_stack.push((src_path, dest_path));
} else if file_type.is_file() || file_type.is_symlink() {
leaves.push(CopyLeaf {
src: src_path,
dest: dest_path,
});
} else {
log::debug!("skipping non-regular file: {}", src_path.display());
}
}
}
let copied = AtomicUsize::new(0);
COPY_POOL.install(|| {
leaves
.par_iter()
.try_for_each(|leaf| -> anyhow::Result<()> {
if copy_leaf(&leaf.src, &leaf.dest, force)? {
copied.fetch_add(1, Ordering::Relaxed);
}
Ok(())
})
})?;
#[cfg(unix)]
for (src_dir, dest_dir) in &dirs_for_perms {
let src_perms = fs::metadata(src_dir)
.with_context(|| format!("reading permissions for {}", src_dir.display()))?
.permissions();
fs::set_permissions(dest_dir, src_perms)
.with_context(|| format!("setting permissions on {}", dest_dir.display()))?;
}
Ok(copied.into_inner())
}
fn remove_if_exists(path: &Path) -> anyhow::Result<()> {
if let Err(e) = fs::remove_file(path) {
anyhow::ensure!(e.kind() == ErrorKind::NotFound, e);
}
Ok(())
}
fn create_symlink(target: &Path, src_path: &Path, dest_path: &Path) -> anyhow::Result<()> {
#[cfg(unix)]
{
let _ = src_path; std::os::unix::fs::symlink(target, dest_path)
.with_context(|| format!("creating symlink {}", dest_path.display()))?;
}
#[cfg(windows)]
{
let is_dir = src_path.metadata().map(|m| m.is_dir()).unwrap_or(false);
if is_dir {
std::os::windows::fs::symlink_dir(target, dest_path)
.with_context(|| format!("creating symlink {}", dest_path.display()))?;
} else {
std::os::windows::fs::symlink_file(target, dest_path)
.with_context(|| format!("creating symlink {}", dest_path.display()))?;
}
}
#[cfg(not(any(unix, windows)))]
{
let _ = (target, src_path, dest_path);
anyhow::bail!("symlink creation not supported on this platform");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_remove_if_exists_nonexistent() {
assert!(remove_if_exists(Path::new("/nonexistent/file")).is_ok());
}
#[test]
fn test_remove_if_exists_not_a_file() {
let dir = std::env::temp_dir();
assert!(remove_if_exists(&dir).is_err());
}
}