exarch_core/creation/
creator.rs1use std::path::Path;
4use std::path::PathBuf;
5
6use crate::creation::config::CreationConfig;
7use crate::creation::report::CreationReport;
8use crate::error::ArchiveError;
9use crate::error::Result;
10use crate::formats::detect::ArchiveType;
11
12#[derive(Debug, Default)]
33pub struct ArchiveCreator {
34 output_path: Option<PathBuf>,
35 sources: Vec<PathBuf>,
36 config: CreationConfig,
37}
38
39impl ArchiveCreator {
40 #[must_use]
50 pub fn new() -> Self {
51 Self::default()
52 }
53
54 #[must_use]
67 pub fn output<P: AsRef<Path>>(mut self, path: P) -> Self {
68 self.output_path = Some(path.as_ref().to_path_buf());
69 self
70 }
71
72 #[must_use]
84 pub fn add_source<P: AsRef<Path>>(mut self, path: P) -> Self {
85 self.sources.push(path.as_ref().to_path_buf());
86 self
87 }
88
89 #[must_use]
99 pub fn sources<P: AsRef<Path>>(mut self, paths: &[P]) -> Self {
100 self.sources
101 .extend(paths.iter().map(|p| p.as_ref().to_path_buf()));
102 self
103 }
104
105 #[must_use]
118 pub fn config(mut self, config: CreationConfig) -> Self {
119 self.config = config;
120 self
121 }
122
123 pub fn compression_level(mut self, level: u8) -> Result<Self> {
142 if !(1..=9).contains(&level) {
143 return Err(ArchiveError::InvalidCompressionLevel { level });
144 }
145 self.config.compression_level = Some(level);
146 Ok(self)
147 }
148
149 #[must_use]
161 pub fn follow_symlinks(mut self, follow: bool) -> Self {
162 self.config.follow_symlinks = follow;
163 self
164 }
165
166 #[must_use]
178 pub fn include_hidden(mut self, include: bool) -> Self {
179 self.config.include_hidden = include;
180 self
181 }
182
183 #[must_use]
195 pub fn exclude<S: Into<String>>(mut self, pattern: S) -> Self {
196 self.config.exclude_patterns.push(pattern.into());
197 self
198 }
199
200 #[must_use]
212 pub fn strip_prefix<P: AsRef<Path>>(mut self, prefix: P) -> Self {
213 self.config.strip_prefix = Some(prefix.as_ref().to_path_buf());
214 self
215 }
216
217 #[must_use]
230 pub fn format(mut self, format: ArchiveType) -> Self {
231 self.config.format = Some(format);
232 self
233 }
234
235 pub fn create(self) -> Result<CreationReport> {
258 let output_path = self
259 .output_path
260 .ok_or_else(|| ArchiveError::InvalidConfiguration {
261 reason: "output path not set".to_string(),
262 })?;
263
264 if self.sources.is_empty() {
265 return Err(ArchiveError::InvalidConfiguration {
266 reason: "no source paths provided".to_string(),
267 });
268 }
269
270 crate::api::create_archive(&output_path, &self.sources, &self.config)
272 }
273}
274
275#[cfg(test)]
276#[allow(clippy::unwrap_used)]
277mod tests {
278 use super::*;
279 use crate::formats::detect::ArchiveType;
280 use std::assert_matches;
281 use std::path::PathBuf;
282
283 #[test]
284 fn test_builder_basic() {
285 let creator = ArchiveCreator::new()
286 .output("test.tar.gz")
287 .add_source("src/");
288
289 assert_eq!(creator.output_path, Some(PathBuf::from("test.tar.gz")));
290 assert_eq!(creator.sources, vec![PathBuf::from("src/")]);
291 }
292
293 #[test]
294 fn test_builder_multiple_sources() {
295 let creator = ArchiveCreator::new()
296 .add_source("src/")
297 .add_source("Cargo.toml")
298 .add_source("README.md");
299
300 assert_eq!(creator.sources.len(), 3);
301 assert_eq!(creator.sources[0], PathBuf::from("src/"));
302 assert_eq!(creator.sources[1], PathBuf::from("Cargo.toml"));
303 assert_eq!(creator.sources[2], PathBuf::from("README.md"));
304 }
305
306 #[test]
307 fn test_builder_sources_array() {
308 let creator = ArchiveCreator::new().sources(&["src/", "Cargo.toml", "README.md"]);
309
310 assert_eq!(creator.sources.len(), 3);
311 }
312
313 #[test]
314 fn test_builder_config_methods() {
315 let creator = ArchiveCreator::new()
316 .compression_level(9)
317 .unwrap()
318 .follow_symlinks(true)
319 .include_hidden(true)
320 .exclude("*.log")
321 .exclude("target/")
322 .strip_prefix("/base")
323 .format(ArchiveType::TarGz);
324
325 assert_eq!(creator.config.compression_level, Some(9));
326 assert!(creator.config.follow_symlinks);
327 assert!(creator.config.include_hidden);
328 assert!(
329 creator
330 .config
331 .exclude_patterns
332 .contains(&"*.log".to_string())
333 );
334 assert!(
335 creator
336 .config
337 .exclude_patterns
338 .contains(&"target/".to_string())
339 );
340 assert_eq!(creator.config.strip_prefix, Some(PathBuf::from("/base")));
341 assert_eq!(creator.config.format, Some(ArchiveType::TarGz));
342 }
343
344 #[test]
345 fn test_builder_no_output_error() {
346 let creator = ArchiveCreator::new().add_source("src/");
347
348 let result = creator.create();
349 assert!(result.is_err());
350 assert_matches!(
351 result.unwrap_err(),
352 ArchiveError::InvalidConfiguration { .. }
353 );
354 }
355
356 #[test]
357 fn test_builder_no_sources_error() {
358 let creator = ArchiveCreator::new().output("test.tar.gz");
359
360 let result = creator.create();
361 assert!(result.is_err());
362 assert_matches!(
363 result.unwrap_err(),
364 ArchiveError::InvalidConfiguration { .. }
365 );
366 }
367
368 #[test]
369 fn test_builder_compression_level() {
370 let creator = ArchiveCreator::new().compression_level(9).unwrap();
371 assert_eq!(creator.config.compression_level, Some(9));
372 }
373
374 #[test]
375 fn test_builder_compression_level_mid() {
376 let creator = ArchiveCreator::new().compression_level(5).unwrap();
377 assert_eq!(creator.config.compression_level, Some(5));
378 }
379
380 #[test]
381 fn test_builder_compression_level_invalid() {
382 assert_matches!(
383 ArchiveCreator::new().compression_level(0),
384 Err(ArchiveError::InvalidCompressionLevel { level: 0 })
385 );
386 assert_matches!(
387 ArchiveCreator::new().compression_level(10),
388 Err(ArchiveError::InvalidCompressionLevel { level: 10 })
389 );
390 }
391
392 #[test]
393 fn test_builder_exclude_patterns() {
394 let creator = ArchiveCreator::new()
395 .exclude("*.log")
396 .exclude("*.tmp")
397 .exclude(".git");
398
399 assert!(
400 creator
401 .config
402 .exclude_patterns
403 .contains(&"*.log".to_string())
404 );
405 assert!(
406 creator
407 .config
408 .exclude_patterns
409 .contains(&"*.tmp".to_string())
410 );
411 assert!(
412 creator
413 .config
414 .exclude_patterns
415 .contains(&".git".to_string())
416 );
417
418 assert!(
420 creator
421 .config
422 .exclude_patterns
423 .contains(&".DS_Store".to_string())
424 );
425 }
426
427 #[test]
428 fn test_builder_full_config() {
429 let config = CreationConfig::default()
430 .with_follow_symlinks(true)
431 .with_compression_level(9)
432 .unwrap();
433
434 let creator = ArchiveCreator::new()
435 .output("test.tar.gz")
436 .add_source("src/")
437 .config(config);
438
439 assert!(creator.config.follow_symlinks);
440 assert_eq!(creator.config.compression_level, Some(9));
441 }
442
443 #[test]
444 fn test_builder_default() {
445 let creator = ArchiveCreator::default();
446 assert_eq!(creator.output_path, None);
447 assert_eq!(creator.sources.len(), 0);
448 }
449
450 #[test]
451 fn test_builder_new() {
452 let creator = ArchiveCreator::new();
453 assert_eq!(creator.output_path, None);
454 assert_eq!(creator.sources.len(), 0);
455 }
456}