#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]
use std::fs;
use std::path::Path;
use rayon::prelude::*;
use crate::error::CopyError;
use crate::progress::{CopyProgress, ProgressTracker};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CopyResult {
Created {
files_copied: u64,
},
Exists,
SourceNotFound,
}
#[derive(Debug, Clone)]
struct FileEntry {
source: std::path::PathBuf,
target: std::path::PathBuf,
is_symlink: bool,
}
pub fn copy_file<F>(source: &Path, target: &Path, on_progress: F) -> Result<CopyResult, CopyError>
where
F: Fn(&CopyProgress),
{
log::debug!("Copying file: {} -> {}", source.display(), target.display());
if !source.exists() {
log::debug!("Source does not exist");
return Ok(CopyResult::SourceNotFound);
}
if target.exists() {
log::debug!("Target already exists");
return Ok(CopyResult::Exists);
}
on_progress(&CopyProgress::new(
1,
0,
Some(source.to_string_lossy().to_string()),
));
if let Some(parent) = target.parent() {
fs::create_dir_all(parent).map_err(|e| CopyError::CreateDirError {
path: parent.to_path_buf(),
io_error: e,
})?;
}
copy_file_with_reflink(source, target)?;
on_progress(&CopyProgress::new(
1,
1,
Some(source.to_string_lossy().to_string()),
));
Ok(CopyResult::Created { files_copied: 1 })
}
pub fn overwrite_file<F>(
source: &Path,
target: &Path,
on_progress: F,
) -> Result<CopyResult, CopyError>
where
F: Fn(&CopyProgress),
{
log::debug!(
"Overwriting file: {} -> {}",
source.display(),
target.display()
);
if !source.exists() {
log::debug!("Source does not exist");
return Ok(CopyResult::SourceNotFound);
}
on_progress(&CopyProgress::new(
1,
0,
Some(source.to_string_lossy().to_string()),
));
if let Some(parent) = target.parent() {
fs::create_dir_all(parent).map_err(|e| CopyError::CreateDirError {
path: parent.to_path_buf(),
io_error: e,
})?;
}
copy_file_with_reflink(source, target)?;
on_progress(&CopyProgress::new(
1,
1,
Some(source.to_string_lossy().to_string()),
));
Ok(CopyResult::Created { files_copied: 1 })
}
pub fn copy_directory<F>(
source: &Path,
target: &Path,
on_progress: F,
) -> Result<CopyResult, CopyError>
where
F: Fn(&CopyProgress) + Sync,
{
log::debug!(
"Copying directory: {} -> {}",
source.display(),
target.display()
);
if !source.exists() {
log::debug!("Source does not exist");
return Ok(CopyResult::SourceNotFound);
}
if target.exists() {
log::debug!("Target already exists");
return Ok(CopyResult::Exists);
}
let entries = enumerate_directory(source, target)?;
let total_files = entries.len() as u64;
log::debug!("Found {total_files} files to copy");
if total_files == 0 {
fs::create_dir_all(target).map_err(|e| CopyError::CreateDirError {
path: target.to_path_buf(),
io_error: e,
})?;
return Ok(CopyResult::Created { files_copied: 0 });
}
let tracker = ProgressTracker::new();
tracker.set_total(total_files);
on_progress(&tracker.snapshot(None));
let mut dirs: std::collections::BTreeSet<std::path::PathBuf> =
std::collections::BTreeSet::new();
for entry in &entries {
if let Some(parent) = entry.target.parent() {
dirs.insert(parent.to_path_buf());
}
}
for dir in &dirs {
fs::create_dir_all(dir).map_err(|e| CopyError::CreateDirError {
path: dir.clone(),
io_error: e,
})?;
}
let tracker_ref = &tracker;
let on_progress_ref = &on_progress;
entries
.par_iter()
.try_for_each(|entry| -> Result<(), CopyError> {
if entry.is_symlink {
copy_symlink(&entry.source, &entry.target)?;
} else {
copy_file_with_reflink(&entry.source, &entry.target)?;
}
tracker_ref.increment_copied();
let copied = tracker_ref.copied();
if copied.is_multiple_of(100) || copied == total_files {
on_progress_ref(
&tracker_ref.snapshot(Some(entry.source.to_string_lossy().to_string())),
);
}
Ok(())
})?;
on_progress(&tracker.snapshot(None));
Ok(CopyResult::Created {
files_copied: total_files,
})
}
fn enumerate_directory(source: &Path, target: &Path) -> Result<Vec<FileEntry>, CopyError> {
let mut entries = Vec::new();
for entry in jwalk::WalkDir::new(source)
.skip_hidden(false)
.follow_links(false)
{
let entry = entry.map_err(|e| CopyError::EnumerationError {
path: source.to_path_buf(),
message: e.to_string(),
})?;
let source_path = entry.path();
if source_path == source {
continue;
}
let file_type = entry.file_type();
if file_type.is_dir() {
continue;
}
let rel_path =
source_path
.strip_prefix(source)
.map_err(|_| CopyError::EnumerationError {
path: source_path.clone(),
message: "Failed to strip prefix".to_string(),
})?;
let target_path = target.join(rel_path);
entries.push(FileEntry {
source: source_path.clone(),
target: target_path,
is_symlink: file_type.is_symlink(),
});
}
Ok(entries)
}
fn copy_file_with_reflink(source: &Path, target: &Path) -> Result<(), CopyError> {
if matches!(reflink_copy::reflink(source, target), Ok(())) {
log::trace!("Reflinked {} -> {}", source.display(), target.display());
} else {
fs::copy(source, target).map_err(|e| CopyError::FileCopyError {
source_path: source.to_path_buf(),
target_path: target.to_path_buf(),
io_error: e,
})?;
log::trace!("Copied {} -> {}", source.display(), target.display());
}
Ok(())
}
fn copy_symlink(source: &Path, target: &Path) -> Result<(), CopyError> {
let link_target = fs::read_link(source).map_err(|e| CopyError::ReadLinkError {
path: source.to_path_buf(),
io_error: e,
})?;
#[cfg(unix)]
{
std::os::unix::fs::symlink(&link_target, target).map_err(|e| {
CopyError::CreateSymlinkError {
path: target.to_path_buf(),
io_error: e,
}
})?;
}
#[cfg(windows)]
{
if link_target.is_dir() {
std::os::windows::fs::symlink_dir(&link_target, target).map_err(|e| {
CopyError::CreateSymlinkError {
path: target.to_path_buf(),
io_error: e,
}
})?;
} else {
std::os::windows::fs::symlink_file(&link_target, target).map_err(|e| {
CopyError::CreateSymlinkError {
path: target.to_path_buf(),
io_error: e,
}
})?;
}
}
log::trace!(
"Symlinked {} -> {} (target: {})",
source.display(),
target.display(),
link_target.display()
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tempfile::TempDir;
#[test]
fn test_copy_file_creates_new() {
let dir = TempDir::new().unwrap();
let source = dir.path().join("source.txt");
let target = dir.path().join("target.txt");
fs::write(&source, "hello world").unwrap();
let progress_count = AtomicU64::new(0);
let result = copy_file(&source, &target, |_| {
progress_count.fetch_add(1, Ordering::SeqCst);
})
.unwrap();
assert!(matches!(result, CopyResult::Created { files_copied: 1 }));
assert!(target.exists());
assert_eq!(fs::read_to_string(&target).unwrap(), "hello world");
assert!(progress_count.load(Ordering::SeqCst) >= 1);
}
#[test]
fn test_copy_file_exists() {
let dir = TempDir::new().unwrap();
let source = dir.path().join("source.txt");
let target = dir.path().join("target.txt");
fs::write(&source, "source content").unwrap();
fs::write(&target, "target content").unwrap();
let result = copy_file(&source, &target, |_| {}).unwrap();
assert_eq!(result, CopyResult::Exists);
assert_eq!(fs::read_to_string(&target).unwrap(), "target content");
}
#[test]
fn test_copy_file_source_not_found() {
let dir = TempDir::new().unwrap();
let source = dir.path().join("nonexistent.txt");
let target = dir.path().join("target.txt");
let result = copy_file(&source, &target, |_| {}).unwrap();
assert_eq!(result, CopyResult::SourceNotFound);
}
#[test]
fn test_copy_directory() {
let dir = TempDir::new().unwrap();
let source = dir.path().join("source_dir");
let target = dir.path().join("target_dir");
fs::create_dir_all(source.join("subdir")).unwrap();
fs::write(source.join("file1.txt"), "content1").unwrap();
fs::write(source.join("subdir/file2.txt"), "content2").unwrap();
let progress_updates = Arc::new(AtomicU64::new(0));
let progress_updates_clone = Arc::clone(&progress_updates);
let result = copy_directory(&source, &target, move |_| {
progress_updates_clone.fetch_add(1, Ordering::SeqCst);
})
.unwrap();
assert!(matches!(result, CopyResult::Created { files_copied: 2 }));
assert!(target.join("file1.txt").exists());
assert!(target.join("subdir/file2.txt").exists());
assert_eq!(
fs::read_to_string(target.join("file1.txt")).unwrap(),
"content1"
);
assert_eq!(
fs::read_to_string(target.join("subdir/file2.txt")).unwrap(),
"content2"
);
}
#[test]
fn test_copy_directory_exists() {
let dir = TempDir::new().unwrap();
let source = dir.path().join("source_dir");
let target = dir.path().join("target_dir");
fs::create_dir_all(&source).unwrap();
fs::create_dir_all(&target).unwrap();
let result = copy_directory(&source, &target, |_| {}).unwrap();
assert_eq!(result, CopyResult::Exists);
}
#[test]
fn test_overwrite_file() {
let dir = TempDir::new().unwrap();
let source = dir.path().join("source.txt");
let target = dir.path().join("target.txt");
fs::write(&source, "new content").unwrap();
fs::write(&target, "old content").unwrap();
let result = overwrite_file(&source, &target, |_| {}).unwrap();
assert!(matches!(result, CopyResult::Created { files_copied: 1 }));
assert_eq!(fs::read_to_string(&target).unwrap(), "new content");
}
}