rez_next_package/
package_copy.rs1use std::fs;
13use std::path::{Path, PathBuf};
14
15use thiserror::Error;
16
17#[derive(Debug, Error)]
19pub enum PackageCopyError {
20 #[error("Package '{name}' not found in search paths")]
21 PackageNotFound { name: String },
22
23 #[error("Package version '{version}' not found for '{name}'")]
24 VersionNotFound { name: String, version: String },
25
26 #[error("Source directory not found: {path}")]
27 SourceNotFound { path: String },
28
29 #[error("Destination already exists: {path}. Use force=true to overwrite.")]
30 DestinationExists { path: String },
31
32 #[error("IO error: {0}")]
33 Io(#[from] std::io::Error),
34
35 #[error("{0}")]
36 Other(String),
37}
38
39#[derive(Debug, Clone)]
41pub struct PackageCopyResult {
42 pub source: PathBuf,
44 pub destination: PathBuf,
46 pub files_copied: usize,
48 pub bytes_copied: u64,
50}
51
52#[derive(Debug, Clone)]
57pub struct PackageCopyConfig {
58 pub packages_path: Vec<PathBuf>,
60 pub force: bool,
62 pub normalize_paths: bool,
64}
65
66impl Default for PackageCopyConfig {
67 fn default() -> Self {
68 Self {
69 packages_path: Vec::new(),
70 force: false,
71 normalize_paths: true,
72 }
73 }
74}
75
76pub fn copy_package(
91 name: &str,
92 version: &str,
93 dest_base: &Path,
94 config: &PackageCopyConfig,
95) -> Result<PackageCopyResult, PackageCopyError> {
96 let src = find_package_dir(name, version, &config.packages_path)?;
98
99 let dest = dest_base.join(name).join(version);
100
101 if dest.exists() {
103 if config.force {
104 fs::remove_dir_all(&dest)?;
105 } else {
106 return Err(PackageCopyError::DestinationExists {
107 path: dest.display().to_string(),
108 });
109 }
110 }
111
112 if let Some(parent) = dest.parent() {
114 fs::create_dir_all(parent)?;
115 }
116
117 let stats = copy_dir_recursive(&src, &dest)?;
119
120 Ok(PackageCopyResult {
121 source: src,
122 destination: dest,
123 files_copied: stats.files,
124 bytes_copied: stats.bytes,
125 })
126}
127
128pub fn find_package_dir(
130 name: &str,
131 version: &str,
132 search_paths: &[PathBuf],
133) -> Result<PathBuf, PackageCopyError> {
134 for base in search_paths {
135 let candidates = [
137 base.join(name).join(version),
138 base.join(name).join(format!("{}-{}", name, version)),
139 ];
140
141 for candidate in &candidates {
142 if candidate.exists() && candidate.is_dir() {
143 return Ok(normalize_path(candidate));
144 }
145 }
146 }
147
148 Err(PackageCopyError::VersionNotFound {
149 name: name.to_string(),
150 version: version.to_string(),
151 })
152}
153
154fn normalize_path(path: &Path) -> PathBuf {
158 dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
160}
161
162#[derive(Debug, Default)]
163struct CopyStats {
164 files: usize,
165 bytes: u64,
166}
167
168fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<CopyStats, std::io::Error> {
170 let mut stats = CopyStats::default();
171
172 if !dest.exists() {
173 fs::create_dir_all(dest)?;
174 }
175
176 for entry in fs::read_dir(src)? {
177 let entry = entry?;
178 let file_type = entry.file_type()?;
179 let src_path = entry.path();
180 let dest_path = dest.join(entry.file_name());
181
182 if file_type.is_symlink() {
183 let target = fs::read_link(&src_path)?;
184 #[cfg(windows)]
186 {
187 if std::os::windows::fs::symlink_file(&target, &dest_path).is_err()
188 && std::os::windows::fs::symlink_dir(&target, &dest_path).is_err()
189 {
190 if target.is_dir() {
191 let sub_stats = copy_dir_recursive(&target, &dest_path)?;
192 stats.files += sub_stats.files;
193 stats.bytes += sub_stats.bytes;
194 } else {
195 let bytes = fs::copy(&target, &dest_path)?;
196 stats.files += 1;
197 stats.bytes += bytes;
198 }
199 } else {
200 stats.files += 1;
201 }
202 }
203 #[cfg(unix)]
204 {
205 std::os::unix::fs::symlink(&target, &dest_path)?;
206 stats.files += 1;
207 }
208 } else if file_type.is_dir() {
209 let sub_stats = copy_dir_recursive(&src_path, &dest_path)?;
210 stats.files += sub_stats.files;
211 stats.bytes += sub_stats.bytes;
212 } else if file_type.is_file() {
213 let bytes = fs::copy(&src_path, &dest_path)?;
214 stats.files += 1;
215 stats.bytes += bytes;
216 }
217 }
218
219 Ok(stats)
220}
221
222pub fn dir_size(path: &Path) -> Result<u64, std::io::Error> {
224 let mut total = 0u64;
225 if path.is_dir() {
226 for entry in fs::read_dir(path)? {
227 let entry = entry?;
228 let meta = entry.metadata()?;
229 if meta.is_dir() {
230 total += dir_size(&entry.path())?;
231 } else {
232 total += meta.len();
233 }
234 }
235 }
236 Ok(total)
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use tempfile::TempDir;
243
244 #[test]
245 fn test_find_package_dir_exists() {
246 let tmp = TempDir::new().unwrap();
247 let pkg_dir = tmp.path().join("testpkg").join("1.0.0");
248 fs::create_dir_all(&pkg_dir).unwrap();
249 fs::write(pkg_dir.join("package.py"), "name = 'testpkg'").unwrap();
250
251 let result = find_package_dir("testpkg", "1.0.0", &[tmp.path().to_path_buf()]);
252 assert!(result.is_ok());
253 }
254
255 #[test]
256 fn test_find_package_dir_not_found() {
257 let tmp = TempDir::new().unwrap();
258 let result = find_package_dir("nonexistent", "1.0.0", &[tmp.path().to_path_buf()]);
259 assert!(result.is_err());
260 }
261
262 #[test]
263 fn test_copy_package_basic() {
264 let src_tmp = TempDir::new().unwrap();
265 let dest_tmp = TempDir::new().unwrap();
266
267 let pkg_dir = src_tmp.path().join("mypkg").join("1.0.0");
268 fs::create_dir_all(&pkg_dir).unwrap();
269 fs::write(
270 pkg_dir.join("package.py"),
271 "name = 'mypkg'\nversion = '1.0.0'",
272 )
273 .unwrap();
274 fs::write(pkg_dir.join("data.txt"), "hello").unwrap();
275
276 let config = PackageCopyConfig {
277 packages_path: vec![src_tmp.path().to_path_buf()],
278 ..Default::default()
279 };
280
281 let result = copy_package("mypkg", "1.0.0", dest_tmp.path(), &config).unwrap();
282 assert!(result.destination.exists());
283 assert!(result.files_copied > 0);
284 assert!(result.bytes_copied > 0);
285 }
286
287 #[test]
288 fn test_copy_package_destination_exists_no_force() {
289 let src_tmp = TempDir::new().unwrap();
290 let dest_tmp = TempDir::new().unwrap();
291
292 let pkg_dir = src_tmp.path().join("mypkg").join("1.0.0");
293 fs::create_dir_all(&pkg_dir).unwrap();
294 fs::write(pkg_dir.join("package.py"), "name = 'mypkg'").unwrap();
295
296 fs::create_dir_all(dest_tmp.path().join("mypkg").join("1.0.0")).unwrap();
298
299 let config = PackageCopyConfig {
300 packages_path: vec![src_tmp.path().to_path_buf()],
301 force: false,
302 ..Default::default()
303 };
304
305 let result = copy_package("mypkg", "1.0.0", dest_tmp.path(), &config);
306 assert!(result.is_err());
307 assert!(result.unwrap_err().to_string().contains("already exists"));
308 }
309
310 #[test]
311 fn test_copy_package_destination_exists_with_force() {
312 let src_tmp = TempDir::new().unwrap();
313 let dest_tmp = TempDir::new().unwrap();
314
315 let pkg_dir = src_tmp.path().join("mypkg").join("1.0.0");
316 fs::create_dir_all(&pkg_dir).unwrap();
317 fs::write(pkg_dir.join("package.py"), "name = 'mypkg'").unwrap();
318
319 let config = PackageCopyConfig {
320 packages_path: vec![src_tmp.path().to_path_buf()],
321 force: true,
322 ..Default::default()
323 };
324
325 let result = copy_package("mypkg", "1.0.0", dest_tmp.path(), &config).unwrap();
326 assert!(result.destination.exists());
327 }
328}