use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use anyhow::Context;
use rayon::prelude::*;
use crate::path::{canonicalize_with_parents, format_path_for_display};
use crate::progress::Progress;
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,
root: Option<&Path>,
force: bool,
) -> anyhow::Result<Option<u64>> {
if let Some(root) = root {
ensure_path_within_root(dest.parent().unwrap_or(dest), root)?;
}
if force {
remove_if_exists(dest)?;
}
if dest.symlink_metadata().is_ok() {
return Ok(None);
}
let src_meta = src
.symlink_metadata()
.with_context(|| format!("reading metadata for {}", src.display()))?;
let is_symlink = src_meta.file_type().is_symlink();
let bytes = src_meta.len();
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)]
{
fs::set_permissions(dest, src_meta.permissions())
.context("setting destination file permissions")?;
}
}
Err(e) if e.kind() == ErrorKind::AlreadyExists => return Ok(None),
Err(e) => {
return Err(anyhow::Error::from(e).context(format!("copying {}", src.display())));
}
}
}
Ok(Some(bytes))
}
fn ensure_path_within_root(path: &Path, root: &Path) -> anyhow::Result<()> {
let canonical_root = canonicalize_with_parents(root);
let canonical_path = canonicalize_with_parents(path);
anyhow::ensure!(
canonical_path.starts_with(&canonical_root),
"refusing to copy outside destination worktree: {} resolves outside {}",
format_path_for_display(path),
format_path_for_display(root)
);
Ok(())
}
struct CopyLeaf {
src: PathBuf,
dest: PathBuf,
}
pub fn copy_dir_recursive(
src: &Path,
dest: &Path,
root: Option<&Path>,
force: bool,
progress: &Progress,
) -> anyhow::Result<(usize, u64)> {
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() {
if let Some(root) = root {
ensure_path_within_root(&dest_dir, root)?;
}
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_files = AtomicUsize::new(0);
let copied_bytes = AtomicU64::new(0);
COPY_POOL.install(|| {
leaves
.par_iter()
.try_for_each(|leaf| -> anyhow::Result<()> {
if let Some(bytes) = copy_leaf(&leaf.src, &leaf.dest, None, force)? {
copied_files.fetch_add(1, Ordering::Relaxed);
copied_bytes.fetch_add(bytes, Ordering::Relaxed);
progress.record(bytes);
}
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_files.into_inner(), copied_bytes.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());
}
}