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 self.config.validate()?;
272
273 crate::api::create_archive(&output_path, &self.sources, &self.config)
274 }
275}
276
277#[cfg(test)]
278#[allow(clippy::unwrap_used)]
279mod tests {
280 use super::*;
281 use crate::formats::detect::ArchiveType;
282 use std::path::PathBuf;
283
284 #[test]
285 fn test_builder_basic() {
286 let creator = ArchiveCreator::new()
287 .output("test.tar.gz")
288 .add_source("src/");
289
290 assert_eq!(creator.output_path, Some(PathBuf::from("test.tar.gz")));
291 assert_eq!(creator.sources, vec![PathBuf::from("src/")]);
292 }
293
294 #[test]
295 fn test_builder_multiple_sources() {
296 let creator = ArchiveCreator::new()
297 .add_source("src/")
298 .add_source("Cargo.toml")
299 .add_source("README.md");
300
301 assert_eq!(creator.sources.len(), 3);
302 assert_eq!(creator.sources[0], PathBuf::from("src/"));
303 assert_eq!(creator.sources[1], PathBuf::from("Cargo.toml"));
304 assert_eq!(creator.sources[2], PathBuf::from("README.md"));
305 }
306
307 #[test]
308 fn test_builder_sources_array() {
309 let creator = ArchiveCreator::new().sources(&["src/", "Cargo.toml", "README.md"]);
310
311 assert_eq!(creator.sources.len(), 3);
312 }
313
314 #[test]
315 fn test_builder_config_methods() {
316 let creator = ArchiveCreator::new()
317 .compression_level(9)
318 .unwrap()
319 .follow_symlinks(true)
320 .include_hidden(true)
321 .exclude("*.log")
322 .exclude("target/")
323 .strip_prefix("/base")
324 .format(ArchiveType::TarGz);
325
326 assert_eq!(creator.config.compression_level, Some(9));
327 assert!(creator.config.follow_symlinks);
328 assert!(creator.config.include_hidden);
329 assert!(
330 creator
331 .config
332 .exclude_patterns
333 .contains(&"*.log".to_string())
334 );
335 assert!(
336 creator
337 .config
338 .exclude_patterns
339 .contains(&"target/".to_string())
340 );
341 assert_eq!(creator.config.strip_prefix, Some(PathBuf::from("/base")));
342 assert_eq!(creator.config.format, Some(ArchiveType::TarGz));
343 }
344
345 #[test]
346 fn test_builder_no_output_error() {
347 let creator = ArchiveCreator::new().add_source("src/");
348
349 let result = creator.create();
350 assert!(result.is_err());
351 assert!(matches!(
352 result.unwrap_err(),
353 ArchiveError::InvalidConfiguration { .. }
354 ));
355 }
356
357 #[test]
358 fn test_builder_no_sources_error() {
359 let creator = ArchiveCreator::new().output("test.tar.gz");
360
361 let result = creator.create();
362 assert!(result.is_err());
363 assert!(matches!(
364 result.unwrap_err(),
365 ArchiveError::InvalidConfiguration { .. }
366 ));
367 }
368
369 #[test]
370 fn test_builder_compression_level() {
371 let creator = ArchiveCreator::new().compression_level(9).unwrap();
372 assert_eq!(creator.config.compression_level, Some(9));
373 }
374
375 #[test]
376 fn test_builder_compression_level_mid() {
377 let creator = ArchiveCreator::new().compression_level(5).unwrap();
378 assert_eq!(creator.config.compression_level, Some(5));
379 }
380
381 #[test]
382 fn test_builder_compression_level_invalid() {
383 assert!(matches!(
384 ArchiveCreator::new().compression_level(0),
385 Err(ArchiveError::InvalidCompressionLevel { level: 0 })
386 ));
387 assert!(matches!(
388 ArchiveCreator::new().compression_level(10),
389 Err(ArchiveError::InvalidCompressionLevel { level: 10 })
390 ));
391 }
392
393 #[test]
394 fn test_builder_exclude_patterns() {
395 let creator = ArchiveCreator::new()
396 .exclude("*.log")
397 .exclude("*.tmp")
398 .exclude(".git");
399
400 assert!(
401 creator
402 .config
403 .exclude_patterns
404 .contains(&"*.log".to_string())
405 );
406 assert!(
407 creator
408 .config
409 .exclude_patterns
410 .contains(&"*.tmp".to_string())
411 );
412 assert!(
413 creator
414 .config
415 .exclude_patterns
416 .contains(&".git".to_string())
417 );
418
419 assert!(
421 creator
422 .config
423 .exclude_patterns
424 .contains(&".DS_Store".to_string())
425 );
426 }
427
428 #[test]
429 fn test_builder_full_config() {
430 let config = CreationConfig::default()
431 .with_follow_symlinks(true)
432 .with_compression_level(9)
433 .unwrap();
434
435 let creator = ArchiveCreator::new()
436 .output("test.tar.gz")
437 .add_source("src/")
438 .config(config);
439
440 assert!(creator.config.follow_symlinks);
441 assert_eq!(creator.config.compression_level, Some(9));
442 }
443
444 #[test]
445 fn test_builder_default() {
446 let creator = ArchiveCreator::default();
447 assert_eq!(creator.output_path, None);
448 assert_eq!(creator.sources.len(), 0);
449 }
450
451 #[test]
452 fn test_builder_new() {
453 let creator = ArchiveCreator::new();
454 assert_eq!(creator.output_path, None);
455 assert_eq!(creator.sources.len(), 0);
456 }
457}