use crate::{CopyError, CopyOptions};
use std::collections::HashSet;
use std::fs;
use std::io::{self, copy};
use std::os::unix::fs::{self as unix_fs, FileTypeExt, PermissionsExt};
use std::path::{Path, PathBuf};
use walkdir_minimal::WalkDir;
pub fn copy_all<P: AsRef<Path>>(src: P, dst: P, opts: &CopyOptions) -> Result<(), CopyError> {
let src = src.as_ref();
let dst = dst.as_ref();
if !src.exists() {
return Err(CopyError::Io(io::Error::from(io::ErrorKind::NotFound)));
}
if src.is_file() {
let dest_path = if dst.is_dir() {
dst.join(src.file_name().unwrap_or_default())
} else {
dst.to_path_buf()
};
return copy_one(src, &dest_path, opts);
}
if src.is_dir() {
if dst.exists() && !dst.is_dir() {
return Err(CopyError::Io(io::Error::from(io::ErrorKind::NotADirectory)));
}
let base_dst = if !dst.exists() {
fs::create_dir_all(dst)?;
dst.to_path_buf()
} else if opts.content_only {
dst.to_path_buf()
} else {
let p = dst.join(src.file_name().unwrap_or_default());
fs::create_dir_all(&p)?;
p
};
return walk_and_copy(src, &base_dst, opts, &mut HashSet::new());
}
Err(CopyError::Io(io::Error::from(io::ErrorKind::Unsupported)))
}
fn walk_and_copy(
src: &Path,
dst: &Path,
opts: &CopyOptions,
visited: &mut HashSet<PathBuf>,
) -> Result<(), CopyError> {
if !visited.insert(src.to_path_buf()) {
return Err(CopyError::SymlinkLoop(src.to_path_buf()));
}
let walker = WalkDir::new(src)?.max_depth(opts.depth);
for entry_res in walker {
let entry = entry_res.map_err(CopyError::Walk)?;
let src_path = entry.path();
let dst_path = dst.join(src_path.strip_prefix(src).unwrap_or(src_path));
let ft = entry.symlink_metadata().map_err(CopyError::Io)?.file_type();
if ft.is_block_device() || ft.is_char_device() || ft.is_fifo() || ft.is_socket() {
continue;
}
if ft.is_dir() {
if !dst_path.exists() {
fs::create_dir_all(&dst_path)?;
}
} else if ft.is_file() {
copy_one(src_path, &dst_path, opts)?;
} else if ft.is_symlink() {
if opts.follow_symlinks {
let target = fs::read_link(src_path)?;
let target_abs = if target.is_absolute() {
target
} else {
src_path
.parent()
.unwrap_or_else(|| Path::new("/"))
.join(&target)
};
if opts.restrict_symlinks {
if let (Ok(base_real), Ok(target_real)) =
(src.canonicalize(), target_abs.canonicalize())
{
if !target_real.starts_with(&base_real) {
eprintln!(
"Skipping symlink outside source {} -> {}",
src_path.display(),
target_real.display()
);
continue;
}
}
}
let target_ft = target_abs
.symlink_metadata()
.map_err(CopyError::Io)?
.file_type();
if target_ft.is_block_device()
|| target_ft.is_char_device()
|| target_ft.is_fifo()
|| target_ft.is_socket()
{
continue;
}
if target_ft.is_file() {
copy_one(&target_abs, &dst_path, opts)?;
} else if target_ft.is_dir() {
walk_and_copy(&target_abs, &dst_path, opts, visited)?;
}
} else {
recreate_symlink(src_path, &dst_path, opts)?;
}
}
}
Ok(())
}
pub fn copy_one<P: AsRef<Path>>(src: P, dst: P, opts: &CopyOptions) -> Result<(), CopyError> {
let src = src.as_ref();
let dst = dst.as_ref();
if dst.exists() {
if !opts.overwrite {
return Ok(());
}
fs::remove_file(dst)?;
} else if let Some(p) = dst.parent() {
fs::create_dir_all(p)?;
}
let mut input = fs::File::open(src)?;
let mut output = fs::File::create(dst)?;
copy(&mut input, &mut output)?;
let mode = fs::metadata(src)?.permissions().mode() & 0o777;
let mut perms = output.metadata()?.permissions();
perms.set_mode(mode);
fs::set_permissions(dst, perms)?;
Ok(())
}
fn recreate_symlink(src: &Path, dst: &Path, opts: &CopyOptions) -> Result<(), CopyError> {
let target = fs::read_link(src)?;
if dst.exists() {
if opts.overwrite {
fs::remove_file(dst)?;
} else {
return Ok(());
}
}
if let Some(p) = dst.parent() {
fs::create_dir_all(p)?;
}
unix_fs::symlink(&target, dst)?;
Ok(())
}