rez_next_package/
package_remove.rs1use std::fs;
12use std::path::PathBuf;
13
14use thiserror::Error;
15
16#[derive(Debug, Error)]
18pub enum PackageRemoveError {
19 #[error("Package '{name}' not found in any search path")]
20 PackageNotFound { name: String },
21
22 #[error("Version '{version}' not found for package '{name}'")]
23 VersionNotFound { name: String, version: String },
24
25 #[error("Path is not a package directory: {path}")]
26 NotAPackage { path: String },
27
28 #[error("IO error: {0}")]
29 Io(#[from] std::io::Error),
30
31 #[error("{0}")]
32 Other(String),
33}
34
35#[derive(Debug, Clone, Default)]
37pub struct PackageRemoveResult {
38 pub removed_paths: Vec<PathBuf>,
40 pub versions_removed: usize,
42 pub bytes_freed: u64,
44}
45
46#[derive(Debug, Clone)]
48pub struct PackageRemoveConfig {
49 pub packages_path: Vec<PathBuf>,
51 pub force: bool,
53 pub prune_empty_families: bool,
55}
56
57impl Default for PackageRemoveConfig {
58 fn default() -> Self {
59 Self {
60 packages_path: Vec::new(),
61 force: false,
62 prune_empty_families: true,
63 }
64 }
65}
66
67pub fn remove_package_version(
80 name: &str,
81 version: &str,
82 config: &PackageRemoveConfig,
83) -> Result<PackageRemoveResult, PackageRemoveError> {
84 let mut removed_paths = Vec::new();
85 let mut bytes_freed = 0u64;
86
87 for base in &config.packages_path {
88 let version_dir = base.join(name).join(version);
89 if version_dir.exists() && version_dir.is_dir() {
90 if let Ok(dir_entry) = fs::read_dir(&version_dir) {
92 for entry in dir_entry.flatten() {
93 if let Ok(meta) = entry.metadata() {
94 bytes_freed += meta.len();
95 }
96 }
97 }
98
99 fs::remove_dir_all(&version_dir)?;
100 removed_paths.push(version_dir);
101 }
102 }
103
104 if removed_paths.is_empty() {
105 return Ok(PackageRemoveResult::default());
106 }
107
108 if config.prune_empty_families {
110 for base in &config.packages_path {
111 let family_dir = base.join(name);
112 if family_dir.exists()
113 && let Ok(entries) = fs::read_dir(&family_dir)
114 && entries.count() == 0
115 {
116 fs::remove_dir(&family_dir)?;
117 }
118 }
119 }
120
121 Ok(PackageRemoveResult {
122 versions_removed: removed_paths.len(),
123 removed_paths,
124 bytes_freed,
125 })
126}
127
128pub fn remove_package_family(
132 name: &str,
133 config: &PackageRemoveConfig,
134) -> Result<PackageRemoveResult, PackageRemoveError> {
135 let mut removed_paths = Vec::new();
136 let mut bytes_freed = 0u64;
137
138 for base in &config.packages_path {
139 let family_dir = base.join(name);
140 if family_dir.exists() && family_dir.is_dir() {
141 bytes_freed += crate::package_copy::dir_size(&family_dir).unwrap_or(0);
143
144 fs::remove_dir_all(&family_dir)?;
145 removed_paths.push(family_dir);
146 }
147 }
148
149 if removed_paths.is_empty() {
150 return Ok(PackageRemoveResult::default());
151 }
152
153 Ok(PackageRemoveResult {
154 versions_removed: removed_paths.len(),
155 removed_paths,
156 bytes_freed,
157 })
158}
159
160pub fn preview_removal(
162 name: &str,
163 version: Option<&str>,
164 search_paths: &[PathBuf],
165) -> Vec<PathBuf> {
166 let mut paths = Vec::new();
167
168 for base in search_paths {
169 if let Some(ver) = version {
170 let version_dir = base.join(name).join(ver);
171 if version_dir.exists() {
172 paths.push(version_dir);
173 }
174 } else {
175 let family_dir = base.join(name);
176 if family_dir.exists() {
177 paths.push(family_dir);
178 }
179 }
180 }
181
182 paths
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use tempfile::TempDir;
189
190 #[test]
191 fn test_remove_package_version() {
192 let tmp = TempDir::new().unwrap();
193 let pkg_dir = tmp.path().join("testpkg").join("1.0.0");
194 fs::create_dir_all(&pkg_dir).unwrap();
195 fs::write(pkg_dir.join("package.py"), "name = 'testpkg'").unwrap();
196 fs::write(pkg_dir.join("data.bin"), vec![0u8; 1024]).unwrap();
197
198 let config = PackageRemoveConfig {
199 packages_path: vec![tmp.path().to_path_buf()],
200 force: true,
201 ..Default::default()
202 };
203
204 let result = remove_package_version("testpkg", "1.0.0", &config).unwrap();
205 assert_eq!(result.versions_removed, 1);
206 assert!(!pkg_dir.exists());
207 }
208
209 #[test]
210 fn test_remove_nonexistent_version() {
211 let tmp = TempDir::new().unwrap();
212 let config = PackageRemoveConfig {
213 packages_path: vec![tmp.path().to_path_buf()],
214 ..Default::default()
215 };
216
217 let result = remove_package_version("nonexistent", "1.0.0", &config);
218 assert_eq!(result.unwrap().versions_removed, 0);
219 }
220
221 #[test]
222 fn test_preview_removal() {
223 let tmp = TempDir::new().unwrap();
224 let pkg_dir = tmp.path().join("previewpkg").join("1.0.0");
225 fs::create_dir_all(&pkg_dir).unwrap();
226
227 let paths = preview_removal("previewpkg", Some("1.0.0"), &[tmp.path().to_path_buf()]);
228 assert_eq!(paths.len(), 1);
229 }
230
231 #[test]
232 fn test_preview_removal_family() {
233 let tmp = TempDir::new().unwrap();
234 let pkg_dir = tmp.path().join("familypkg").join("1.0.0");
235 fs::create_dir_all(&pkg_dir).unwrap();
236
237 let paths = preview_removal("familypkg", None, &[tmp.path().to_path_buf()]);
238 assert_eq!(paths.len(), 1);
239 }
240}