use std::path::{Path, PathBuf};
use crate::{Error, Result};
pub async fn copy_dir(source: PathBuf, destination: PathBuf) -> Result<()> {
tokio::task::spawn_blocking(move || {
reject_symlinks(&source)?;
dircpy::CopyBuilder::new(&source, &destination)
.overwrite(true)
.run()
.map_err(|err| Error::copy_directory(err, source, destination))
})
.await
.map_err(|err| Error::io(std::io::Error::other(err), "Copy task panicked"))?
}
fn reject_symlinks(root: &Path) -> Result<()> {
let mut directories = vec![root.to_path_buf()];
while let Some(directory) = directories.pop() {
let entries = std::fs::read_dir(&directory).map_err(|err| {
Error::io(
err,
format!("Could not read directory: {}", directory.display()),
)
})?;
for entry in entries {
let path = entry
.map_err(|err| {
Error::io(
err,
format!("Could not read directory: {}", directory.display()),
)
})?
.path();
let file_type = std::fs::symlink_metadata(&path)
.map_err(|err| Error::io(err, format!("Could not inspect: {}", path.display())))?
.file_type();
if file_type.is_symlink() {
return Err(Error::SymlinkInSource { path });
}
if file_type.is_dir() {
directories.push(path);
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{Error, copy_dir};
#[tokio::test]
async fn copies_a_plain_tree() {
let source = tempfile::TempDir::with_prefix("adnv-src").unwrap();
let destination = tempfile::TempDir::with_prefix("adnv-dst").unwrap();
std::fs::create_dir(source.path().join("sub")).unwrap();
std::fs::write(source.path().join("sub/file.txt"), "content").unwrap();
copy_dir(
source.path().to_path_buf(),
destination.path().to_path_buf(),
)
.await
.expect("a tree without links copies");
assert_eq!(
std::fs::read_to_string(destination.path().join("sub/file.txt")).unwrap(),
"content"
);
}
#[tokio::test]
async fn refuses_a_symlink_in_the_source() {
let outside = tempfile::NamedTempFile::new().unwrap();
std::fs::write(outside.path(), "INTACT").unwrap();
let source = tempfile::TempDir::with_prefix("adnv-src").unwrap();
let destination = tempfile::TempDir::with_prefix("adnv-dst").unwrap();
std::fs::create_dir(source.path().join("nested")).unwrap();
std::os::unix::fs::symlink(outside.path(), source.path().join("nested/innocent.txt"))
.unwrap();
let result = copy_dir(
source.path().to_path_buf(),
destination.path().to_path_buf(),
)
.await;
assert!(
matches!(result, Err(Error::SymlinkInSource { .. })),
"the symlink was copied: {result:?}"
);
assert!(
!destination.path().join("nested/innocent.txt").exists(),
"the link reached the destination"
);
assert_eq!(
std::fs::read_to_string(outside.path()).unwrap(),
"INTACT",
"the link target was written through"
);
}
}