use std::path::{Path, PathBuf};
use thiserror::Error;
use crate::package_copy::{self, PackageCopyConfig, PackageCopyError};
use crate::package_remove::{self, PackageRemoveConfig, PackageRemoveError};
#[derive(Debug, Error)]
pub enum PackageMoveError {
#[error("{0}")]
Copy(#[from] PackageCopyError),
#[error("{0}")]
Remove(#[from] PackageRemoveError),
#[error("Cannot move a package to the same location: {path}")]
SameLocation { path: String },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
Other(String),
}
#[derive(Debug, Clone)]
pub struct PackageMoveResult {
pub source: PathBuf,
pub destination: PathBuf,
pub files_copied: usize,
pub bytes_copied: u64,
pub source_removed: bool,
}
#[derive(Debug, Clone)]
pub struct PackageMoveConfig {
pub packages_path: Vec<PathBuf>,
pub force: bool,
pub keep_source: bool,
pub normalize_paths: bool,
}
impl Default for PackageMoveConfig {
fn default() -> Self {
Self {
packages_path: Vec::new(),
force: false,
keep_source: false,
normalize_paths: true,
}
}
}
impl PackageMoveConfig {
fn to_copy_config(&self) -> PackageCopyConfig {
PackageCopyConfig {
packages_path: self.packages_path.clone(),
force: self.force,
normalize_paths: self.normalize_paths,
}
}
fn to_remove_config(&self) -> PackageRemoveConfig {
PackageRemoveConfig {
packages_path: self.packages_path.clone(),
force: true, prune_empty_families: true,
}
}
}
pub fn move_package(
name: &str,
version: &str,
dest_base: &Path,
config: &PackageMoveConfig,
) -> Result<PackageMoveResult, PackageMoveError> {
let dest = dest_base.join(name).join(version);
let src = package_copy::find_package_dir(name, version, &config.packages_path)?;
let src_normalized = dunce::canonicalize(&src).unwrap_or_else(|_| src.clone());
let dest_normalized = dunce::canonicalize(&dest).unwrap_or_else(|_| dest.clone());
if src_normalized == dest_normalized {
return Err(PackageMoveError::SameLocation {
path: src.display().to_string(),
});
}
let copy_result =
package_copy::copy_package(name, version, dest_base, &config.to_copy_config())?;
let source_removed = if !config.keep_source {
package_remove::remove_package_version(name, version, &config.to_remove_config())?;
true
} else {
false
};
Ok(PackageMoveResult {
source: copy_result.source,
destination: copy_result.destination,
files_copied: copy_result.files_copied,
bytes_copied: copy_result.bytes_copied,
source_removed,
})
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_move_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");
std::fs::create_dir_all(&pkg_dir).unwrap();
std::fs::write(
pkg_dir.join("package.py"),
"name = 'mypkg'\nversion = '1.0.0'",
)
.unwrap();
std::fs::write(pkg_dir.join("data.txt"), "hello").unwrap();
let config = PackageMoveConfig {
packages_path: vec![src_tmp.path().to_path_buf()],
..Default::default()
};
let result = move_package("mypkg", "1.0.0", dest_tmp.path(), &config).unwrap();
assert!(result.destination.exists());
assert!(result.source_removed);
assert!(!pkg_dir.exists(), "Source should be removed after move");
}
#[test]
fn test_move_package_keep_source() {
let src_tmp = TempDir::new().unwrap();
let dest_tmp = TempDir::new().unwrap();
let pkg_dir = src_tmp.path().join("mypkg").join("1.0.0");
std::fs::create_dir_all(&pkg_dir).unwrap();
std::fs::write(pkg_dir.join("package.py"), "name = 'mypkg'").unwrap();
let config = PackageMoveConfig {
packages_path: vec![src_tmp.path().to_path_buf()],
keep_source: true,
..Default::default()
};
let result = move_package("mypkg", "1.0.0", dest_tmp.path(), &config).unwrap();
assert!(result.destination.exists());
assert!(!result.source_removed);
assert!(
pkg_dir.exists(),
"Source should remain when keep_source=true"
);
}
#[test]
fn test_move_package_same_location() {
let tmp = TempDir::new().unwrap();
let pkg_dir = tmp.path().join("mypkg").join("1.0.0");
std::fs::create_dir_all(&pkg_dir).unwrap();
let config = PackageMoveConfig {
packages_path: vec![tmp.path().to_path_buf()],
..Default::default()
};
let result = move_package("mypkg", "1.0.0", tmp.path(), &config);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("same location"));
}
#[test]
fn test_move_package_not_found() {
let src_tmp = TempDir::new().unwrap();
let dest_tmp = TempDir::new().unwrap();
let config = PackageMoveConfig {
packages_path: vec![src_tmp.path().to_path_buf()],
..Default::default()
};
let result = move_package("nonexistent", "1.0.0", dest_tmp.path(), &config);
assert!(result.is_err());
}
#[test]
fn test_move_package_overwrite_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");
std::fs::create_dir_all(&pkg_dir).unwrap();
std::fs::write(pkg_dir.join("package.py"), "name = 'mypkg'").unwrap();
std::fs::create_dir_all(dest_tmp.path().join("mypkg").join("1.0.0")).unwrap();
let config = PackageMoveConfig {
packages_path: vec![src_tmp.path().to_path_buf()],
force: true,
..Default::default()
};
let result = move_package("mypkg", "1.0.0", dest_tmp.path(), &config).unwrap();
assert!(result.source_removed);
}
}