use std::fs;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PackageCopyError {
#[error("Package '{name}' not found in search paths")]
PackageNotFound { name: String },
#[error("Package version '{version}' not found for '{name}'")]
VersionNotFound { name: String, version: String },
#[error("Source directory not found: {path}")]
SourceNotFound { path: String },
#[error("Destination already exists: {path}. Use force=true to overwrite.")]
DestinationExists { path: String },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
Other(String),
}
#[derive(Debug, Clone)]
pub struct PackageCopyResult {
pub source: PathBuf,
pub destination: PathBuf,
pub files_copied: usize,
pub bytes_copied: u64,
}
#[derive(Debug, Clone)]
pub struct PackageCopyConfig {
pub packages_path: Vec<PathBuf>,
pub force: bool,
pub normalize_paths: bool,
}
impl Default for PackageCopyConfig {
fn default() -> Self {
Self {
packages_path: Vec::new(),
force: false,
normalize_paths: true,
}
}
}
pub fn copy_package(
name: &str,
version: &str,
dest_base: &Path,
config: &PackageCopyConfig,
) -> Result<PackageCopyResult, PackageCopyError> {
let src = find_package_dir(name, version, &config.packages_path)?;
let dest = dest_base.join(name).join(version);
if dest.exists() {
if config.force {
fs::remove_dir_all(&dest)?;
} else {
return Err(PackageCopyError::DestinationExists {
path: dest.display().to_string(),
});
}
}
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)?;
}
let stats = copy_dir_recursive(&src, &dest)?;
Ok(PackageCopyResult {
source: src,
destination: dest,
files_copied: stats.files,
bytes_copied: stats.bytes,
})
}
pub fn find_package_dir(
name: &str,
version: &str,
search_paths: &[PathBuf],
) -> Result<PathBuf, PackageCopyError> {
for base in search_paths {
let candidates = [
base.join(name).join(version),
base.join(name).join(format!("{}-{}", name, version)),
];
for candidate in &candidates {
if candidate.exists() && candidate.is_dir() {
return Ok(normalize_path(candidate));
}
}
}
Err(PackageCopyError::VersionNotFound {
name: name.to_string(),
version: version.to_string(),
})
}
fn normalize_path(path: &Path) -> PathBuf {
dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
#[derive(Debug, Default)]
struct CopyStats {
files: usize,
bytes: u64,
}
fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<CopyStats, std::io::Error> {
let mut stats = CopyStats::default();
if !dest.exists() {
fs::create_dir_all(dest)?;
}
for entry in fs::read_dir(src)? {
let entry = entry?;
let file_type = entry.file_type()?;
let src_path = entry.path();
let dest_path = dest.join(entry.file_name());
if file_type.is_symlink() {
let target = fs::read_link(&src_path)?;
#[cfg(windows)]
{
if std::os::windows::fs::symlink_file(&target, &dest_path).is_err()
&& std::os::windows::fs::symlink_dir(&target, &dest_path).is_err()
{
if target.is_dir() {
let sub_stats = copy_dir_recursive(&target, &dest_path)?;
stats.files += sub_stats.files;
stats.bytes += sub_stats.bytes;
} else {
let bytes = fs::copy(&target, &dest_path)?;
stats.files += 1;
stats.bytes += bytes;
}
} else {
stats.files += 1;
}
}
#[cfg(unix)]
{
std::os::unix::fs::symlink(&target, &dest_path)?;
stats.files += 1;
}
} else if file_type.is_dir() {
let sub_stats = copy_dir_recursive(&src_path, &dest_path)?;
stats.files += sub_stats.files;
stats.bytes += sub_stats.bytes;
} else if file_type.is_file() {
let bytes = fs::copy(&src_path, &dest_path)?;
stats.files += 1;
stats.bytes += bytes;
}
}
Ok(stats)
}
pub fn dir_size(path: &Path) -> Result<u64, std::io::Error> {
let mut total = 0u64;
if path.is_dir() {
for entry in fs::read_dir(path)? {
let entry = entry?;
let meta = entry.metadata()?;
if meta.is_dir() {
total += dir_size(&entry.path())?;
} else {
total += meta.len();
}
}
}
Ok(total)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_find_package_dir_exists() {
let tmp = TempDir::new().unwrap();
let pkg_dir = tmp.path().join("testpkg").join("1.0.0");
fs::create_dir_all(&pkg_dir).unwrap();
fs::write(pkg_dir.join("package.py"), "name = 'testpkg'").unwrap();
let result = find_package_dir("testpkg", "1.0.0", &[tmp.path().to_path_buf()]);
assert!(result.is_ok());
}
#[test]
fn test_find_package_dir_not_found() {
let tmp = TempDir::new().unwrap();
let result = find_package_dir("nonexistent", "1.0.0", &[tmp.path().to_path_buf()]);
assert!(result.is_err());
}
#[test]
fn test_copy_package_basic() {
let src_tmp = TempDir::new().unwrap();
let dest_tmp = TempDir::new().unwrap();
let pkg_dir = src_tmp.path().join("mypkg").join("1.0.0");
fs::create_dir_all(&pkg_dir).unwrap();
fs::write(
pkg_dir.join("package.py"),
"name = 'mypkg'\nversion = '1.0.0'",
)
.unwrap();
fs::write(pkg_dir.join("data.txt"), "hello").unwrap();
let config = PackageCopyConfig {
packages_path: vec![src_tmp.path().to_path_buf()],
..Default::default()
};
let result = copy_package("mypkg", "1.0.0", dest_tmp.path(), &config).unwrap();
assert!(result.destination.exists());
assert!(result.files_copied > 0);
assert!(result.bytes_copied > 0);
}
#[test]
fn test_copy_package_destination_exists_no_force() {
let src_tmp = TempDir::new().unwrap();
let dest_tmp = TempDir::new().unwrap();
let pkg_dir = src_tmp.path().join("mypkg").join("1.0.0");
fs::create_dir_all(&pkg_dir).unwrap();
fs::write(pkg_dir.join("package.py"), "name = 'mypkg'").unwrap();
fs::create_dir_all(dest_tmp.path().join("mypkg").join("1.0.0")).unwrap();
let config = PackageCopyConfig {
packages_path: vec![src_tmp.path().to_path_buf()],
force: false,
..Default::default()
};
let result = copy_package("mypkg", "1.0.0", dest_tmp.path(), &config);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("already exists"));
}
#[test]
fn test_copy_package_destination_exists_with_force() {
let src_tmp = TempDir::new().unwrap();
let dest_tmp = TempDir::new().unwrap();
let pkg_dir = src_tmp.path().join("mypkg").join("1.0.0");
fs::create_dir_all(&pkg_dir).unwrap();
fs::write(pkg_dir.join("package.py"), "name = 'mypkg'").unwrap();
let config = PackageCopyConfig {
packages_path: vec![src_tmp.path().to_path_buf()],
force: true,
..Default::default()
};
let result = copy_package("mypkg", "1.0.0", dest_tmp.path(), &config).unwrap();
assert!(result.destination.exists());
}
}