rez_next_package/
package_move.rs1use std::path::{Path, PathBuf};
12
13use thiserror::Error;
14
15use crate::package_copy::{self, PackageCopyConfig, PackageCopyError};
16use crate::package_remove::{self, PackageRemoveConfig, PackageRemoveError};
17
18#[derive(Debug, Error)]
20pub enum PackageMoveError {
21 #[error("{0}")]
22 Copy(#[from] PackageCopyError),
23
24 #[error("{0}")]
25 Remove(#[from] PackageRemoveError),
26
27 #[error("Cannot move a package to the same location: {path}")]
28 SameLocation { path: String },
29
30 #[error("IO error: {0}")]
31 Io(#[from] std::io::Error),
32
33 #[error("{0}")]
34 Other(String),
35}
36
37#[derive(Debug, Clone)]
39pub struct PackageMoveResult {
40 pub source: PathBuf,
42 pub destination: PathBuf,
44 pub files_copied: usize,
46 pub bytes_copied: u64,
48 pub source_removed: bool,
50}
51
52#[derive(Debug, Clone)]
56pub struct PackageMoveConfig {
57 pub packages_path: Vec<PathBuf>,
59 pub force: bool,
61 pub keep_source: bool,
63 pub normalize_paths: bool,
65}
66
67impl Default for PackageMoveConfig {
68 fn default() -> Self {
69 Self {
70 packages_path: Vec::new(),
71 force: false,
72 keep_source: false,
73 normalize_paths: true,
74 }
75 }
76}
77
78impl PackageMoveConfig {
79 fn to_copy_config(&self) -> PackageCopyConfig {
81 PackageCopyConfig {
82 packages_path: self.packages_path.clone(),
83 force: self.force,
84 normalize_paths: self.normalize_paths,
85 }
86 }
87
88 fn to_remove_config(&self) -> PackageRemoveConfig {
90 PackageRemoveConfig {
91 packages_path: self.packages_path.clone(),
92 force: true, prune_empty_families: true,
94 }
95 }
96}
97
98pub fn move_package(
119 name: &str,
120 version: &str,
121 dest_base: &Path,
122 config: &PackageMoveConfig,
123) -> Result<PackageMoveResult, PackageMoveError> {
124 let dest = dest_base.join(name).join(version);
126 let src = package_copy::find_package_dir(name, version, &config.packages_path)?;
127
128 let src_normalized = dunce::canonicalize(&src).unwrap_or_else(|_| src.clone());
130 let dest_normalized = dunce::canonicalize(&dest).unwrap_or_else(|_| dest.clone());
131
132 if src_normalized == dest_normalized {
133 return Err(PackageMoveError::SameLocation {
134 path: src.display().to_string(),
135 });
136 }
137
138 let copy_result =
140 package_copy::copy_package(name, version, dest_base, &config.to_copy_config())?;
141
142 let source_removed = if !config.keep_source {
144 package_remove::remove_package_version(name, version, &config.to_remove_config())?;
145 true
146 } else {
147 false
148 };
149
150 Ok(PackageMoveResult {
151 source: copy_result.source,
152 destination: copy_result.destination,
153 files_copied: copy_result.files_copied,
154 bytes_copied: copy_result.bytes_copied,
155 source_removed,
156 })
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use tempfile::TempDir;
163
164 #[test]
165 fn test_move_package_basic() {
166 let src_tmp = TempDir::new().unwrap();
167 let dest_tmp = TempDir::new().unwrap();
168
169 let pkg_dir = src_tmp.path().join("mypkg").join("1.0.0");
170 std::fs::create_dir_all(&pkg_dir).unwrap();
171 std::fs::write(
172 pkg_dir.join("package.py"),
173 "name = 'mypkg'\nversion = '1.0.0'",
174 )
175 .unwrap();
176 std::fs::write(pkg_dir.join("data.txt"), "hello").unwrap();
177
178 let config = PackageMoveConfig {
179 packages_path: vec![src_tmp.path().to_path_buf()],
180 ..Default::default()
181 };
182
183 let result = move_package("mypkg", "1.0.0", dest_tmp.path(), &config).unwrap();
184 assert!(result.destination.exists());
185 assert!(result.source_removed);
186 assert!(!pkg_dir.exists(), "Source should be removed after move");
187 }
188
189 #[test]
190 fn test_move_package_keep_source() {
191 let src_tmp = TempDir::new().unwrap();
192 let dest_tmp = TempDir::new().unwrap();
193
194 let pkg_dir = src_tmp.path().join("mypkg").join("1.0.0");
195 std::fs::create_dir_all(&pkg_dir).unwrap();
196 std::fs::write(pkg_dir.join("package.py"), "name = 'mypkg'").unwrap();
197
198 let config = PackageMoveConfig {
199 packages_path: vec![src_tmp.path().to_path_buf()],
200 keep_source: true,
201 ..Default::default()
202 };
203
204 let result = move_package("mypkg", "1.0.0", dest_tmp.path(), &config).unwrap();
205 assert!(result.destination.exists());
206 assert!(!result.source_removed);
207 assert!(
208 pkg_dir.exists(),
209 "Source should remain when keep_source=true"
210 );
211 }
212
213 #[test]
214 fn test_move_package_same_location() {
215 let tmp = TempDir::new().unwrap();
216 let pkg_dir = tmp.path().join("mypkg").join("1.0.0");
217 std::fs::create_dir_all(&pkg_dir).unwrap();
218
219 let config = PackageMoveConfig {
220 packages_path: vec![tmp.path().to_path_buf()],
221 ..Default::default()
222 };
223
224 let result = move_package("mypkg", "1.0.0", tmp.path(), &config);
225 assert!(result.is_err());
226 assert!(result.unwrap_err().to_string().contains("same location"));
227 }
228
229 #[test]
230 fn test_move_package_not_found() {
231 let src_tmp = TempDir::new().unwrap();
232 let dest_tmp = TempDir::new().unwrap();
233
234 let config = PackageMoveConfig {
235 packages_path: vec![src_tmp.path().to_path_buf()],
236 ..Default::default()
237 };
238
239 let result = move_package("nonexistent", "1.0.0", dest_tmp.path(), &config);
240 assert!(result.is_err());
241 }
242
243 #[test]
244 fn test_move_package_overwrite_with_force() {
245 let src_tmp = TempDir::new().unwrap();
246 let dest_tmp = TempDir::new().unwrap();
247
248 let pkg_dir = src_tmp.path().join("mypkg").join("1.0.0");
249 std::fs::create_dir_all(&pkg_dir).unwrap();
250 std::fs::write(pkg_dir.join("package.py"), "name = 'mypkg'").unwrap();
251
252 std::fs::create_dir_all(dest_tmp.path().join("mypkg").join("1.0.0")).unwrap();
254
255 let config = PackageMoveConfig {
256 packages_path: vec![src_tmp.path().to_path_buf()],
257 force: true,
258 ..Default::default()
259 };
260
261 let result = move_package("mypkg", "1.0.0", dest_tmp.path(), &config).unwrap();
262 assert!(result.source_removed);
263 }
264}