use std::fs;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PackageRemoveError {
#[error("Package '{name}' not found in any search path")]
PackageNotFound { name: String },
#[error("Version '{version}' not found for package '{name}'")]
VersionNotFound { name: String, version: String },
#[error("Path is not a package directory: {path}")]
NotAPackage { path: String },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
Other(String),
}
#[derive(Debug, Clone, Default)]
pub struct PackageRemoveResult {
pub removed_paths: Vec<PathBuf>,
pub versions_removed: usize,
pub bytes_freed: u64,
}
#[derive(Debug, Clone)]
pub struct PackageRemoveConfig {
pub packages_path: Vec<PathBuf>,
pub force: bool,
pub prune_empty_families: bool,
}
impl Default for PackageRemoveConfig {
fn default() -> Self {
Self {
packages_path: Vec::new(),
force: false,
prune_empty_families: true,
}
}
}
pub fn remove_package_version(
name: &str,
version: &str,
config: &PackageRemoveConfig,
) -> Result<PackageRemoveResult, PackageRemoveError> {
let mut removed_paths = Vec::new();
let mut bytes_freed = 0u64;
for base in &config.packages_path {
let version_dir = base.join(name).join(version);
if version_dir.exists() && version_dir.is_dir() {
if let Ok(dir_entry) = fs::read_dir(&version_dir) {
for entry in dir_entry.flatten() {
if let Ok(meta) = entry.metadata() {
bytes_freed += meta.len();
}
}
}
fs::remove_dir_all(&version_dir)?;
removed_paths.push(version_dir);
}
}
if removed_paths.is_empty() {
return Ok(PackageRemoveResult::default());
}
if config.prune_empty_families {
for base in &config.packages_path {
let family_dir = base.join(name);
if family_dir.exists()
&& let Ok(entries) = fs::read_dir(&family_dir)
&& entries.count() == 0
{
fs::remove_dir(&family_dir)?;
}
}
}
Ok(PackageRemoveResult {
versions_removed: removed_paths.len(),
removed_paths,
bytes_freed,
})
}
pub fn remove_package_family(
name: &str,
config: &PackageRemoveConfig,
) -> Result<PackageRemoveResult, PackageRemoveError> {
let mut removed_paths = Vec::new();
let mut bytes_freed = 0u64;
for base in &config.packages_path {
let family_dir = base.join(name);
if family_dir.exists() && family_dir.is_dir() {
bytes_freed += crate::package_copy::dir_size(&family_dir).unwrap_or(0);
fs::remove_dir_all(&family_dir)?;
removed_paths.push(family_dir);
}
}
if removed_paths.is_empty() {
return Ok(PackageRemoveResult::default());
}
Ok(PackageRemoveResult {
versions_removed: removed_paths.len(),
removed_paths,
bytes_freed,
})
}
pub fn preview_removal(
name: &str,
version: Option<&str>,
search_paths: &[PathBuf],
) -> Vec<PathBuf> {
let mut paths = Vec::new();
for base in search_paths {
if let Some(ver) = version {
let version_dir = base.join(name).join(ver);
if version_dir.exists() {
paths.push(version_dir);
}
} else {
let family_dir = base.join(name);
if family_dir.exists() {
paths.push(family_dir);
}
}
}
paths
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_remove_package_version() {
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();
fs::write(pkg_dir.join("data.bin"), vec![0u8; 1024]).unwrap();
let config = PackageRemoveConfig {
packages_path: vec![tmp.path().to_path_buf()],
force: true,
..Default::default()
};
let result = remove_package_version("testpkg", "1.0.0", &config).unwrap();
assert_eq!(result.versions_removed, 1);
assert!(!pkg_dir.exists());
}
#[test]
fn test_remove_nonexistent_version() {
let tmp = TempDir::new().unwrap();
let config = PackageRemoveConfig {
packages_path: vec![tmp.path().to_path_buf()],
..Default::default()
};
let result = remove_package_version("nonexistent", "1.0.0", &config);
assert_eq!(result.unwrap().versions_removed, 0);
}
#[test]
fn test_preview_removal() {
let tmp = TempDir::new().unwrap();
let pkg_dir = tmp.path().join("previewpkg").join("1.0.0");
fs::create_dir_all(&pkg_dir).unwrap();
let paths = preview_removal("previewpkg", Some("1.0.0"), &[tmp.path().to_path_buf()]);
assert_eq!(paths.len(), 1);
}
#[test]
fn test_preview_removal_family() {
let tmp = TempDir::new().unwrap();
let pkg_dir = tmp.path().join("familypkg").join("1.0.0");
fs::create_dir_all(&pkg_dir).unwrap();
let paths = preview_removal("familypkg", None, &[tmp.path().to_path_buf()]);
assert_eq!(paths.len(), 1);
}
}