exarch_core/creation/
creator.rs

1//! Builder for creating archives with fluent API.
2
3use std::path::Path;
4use std::path::PathBuf;
5
6use crate::creation::config::CreationConfig;
7use crate::creation::report::CreationReport;
8use crate::error::ExtractionError;
9use crate::error::Result;
10use crate::formats::detect::ArchiveType;
11
12/// Builder for creating archives with fluent API.
13///
14/// Provides a convenient, type-safe interface for configuring and creating
15/// archives with various compression formats and security options.
16///
17/// # Examples
18///
19/// ```no_run
20/// use exarch_core::creation::ArchiveCreator;
21///
22/// let report = ArchiveCreator::new()
23///     .output("backup.tar.gz")
24///     .add_source("src/")
25///     .add_source("Cargo.toml")
26///     .compression_level(9)
27///     .create()?;
28///
29/// println!("Created archive with {} files", report.files_added);
30/// # Ok::<(), exarch_core::ExtractionError>(())
31/// ```
32#[derive(Debug, Default)]
33pub struct ArchiveCreator {
34    output_path: Option<PathBuf>,
35    sources: Vec<PathBuf>,
36    config: CreationConfig,
37}
38
39impl ArchiveCreator {
40    /// Creates a new `ArchiveCreator` with default settings.
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// use exarch_core::creation::ArchiveCreator;
46    ///
47    /// let creator = ArchiveCreator::new();
48    /// ```
49    #[must_use]
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// Sets the output archive path.
55    ///
56    /// The archive format is auto-detected from the file extension
57    /// unless explicitly set via `format()`.
58    ///
59    /// # Examples
60    ///
61    /// ```
62    /// use exarch_core::creation::ArchiveCreator;
63    ///
64    /// let creator = ArchiveCreator::new().output("backup.tar.gz");
65    /// ```
66    #[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    /// Adds a source file or directory.
73    ///
74    /// # Examples
75    ///
76    /// ```
77    /// use exarch_core::creation::ArchiveCreator;
78    ///
79    /// let creator = ArchiveCreator::new()
80    ///     .add_source("src/")
81    ///     .add_source("Cargo.toml");
82    /// ```
83    #[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    /// Adds multiple source files or directories.
90    ///
91    /// # Examples
92    ///
93    /// ```
94    /// use exarch_core::creation::ArchiveCreator;
95    ///
96    /// let creator = ArchiveCreator::new().sources(&["src/", "Cargo.toml", "README.md"]);
97    /// ```
98    #[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    /// Sets the full configuration.
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// use exarch_core::creation::ArchiveCreator;
111    /// use exarch_core::creation::CreationConfig;
112    ///
113    /// let config = CreationConfig::default().with_follow_symlinks(true);
114    ///
115    /// let creator = ArchiveCreator::new().config(config);
116    /// ```
117    #[must_use]
118    pub fn config(mut self, config: CreationConfig) -> Self {
119        self.config = config;
120        self
121    }
122
123    /// Sets the compression level (1-9).
124    ///
125    /// Higher values provide better compression but slower speed.
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use exarch_core::creation::ArchiveCreator;
131    ///
132    /// let creator = ArchiveCreator::new().compression_level(9); // Maximum compression
133    /// ```
134    #[must_use]
135    pub fn compression_level(mut self, level: u8) -> Self {
136        self.config.compression_level = Some(level);
137        self
138    }
139
140    /// Sets whether to follow symlinks.
141    ///
142    /// Default: `false` (symlinks stored as symlinks).
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// use exarch_core::creation::ArchiveCreator;
148    ///
149    /// let creator = ArchiveCreator::new().follow_symlinks(true);
150    /// ```
151    #[must_use]
152    pub fn follow_symlinks(mut self, follow: bool) -> Self {
153        self.config.follow_symlinks = follow;
154        self
155    }
156
157    /// Sets whether to include hidden files.
158    ///
159    /// Default: `false` (skip hidden files).
160    ///
161    /// # Examples
162    ///
163    /// ```
164    /// use exarch_core::creation::ArchiveCreator;
165    ///
166    /// let creator = ArchiveCreator::new().include_hidden(true);
167    /// ```
168    #[must_use]
169    pub fn include_hidden(mut self, include: bool) -> Self {
170        self.config.include_hidden = include;
171        self
172    }
173
174    /// Adds an exclude pattern.
175    ///
176    /// Files matching this pattern will be skipped.
177    ///
178    /// # Examples
179    ///
180    /// ```
181    /// use exarch_core::creation::ArchiveCreator;
182    ///
183    /// let creator = ArchiveCreator::new().exclude("*.log").exclude("target/");
184    /// ```
185    #[must_use]
186    pub fn exclude<S: Into<String>>(mut self, pattern: S) -> Self {
187        self.config.exclude_patterns.push(pattern.into());
188        self
189    }
190
191    /// Sets the strip prefix for archive paths.
192    ///
193    /// If set, this prefix will be removed from all entry paths in the archive.
194    ///
195    /// # Examples
196    ///
197    /// ```
198    /// use exarch_core::creation::ArchiveCreator;
199    ///
200    /// let creator = ArchiveCreator::new().strip_prefix("/base/path");
201    /// ```
202    #[must_use]
203    pub fn strip_prefix<P: AsRef<Path>>(mut self, prefix: P) -> Self {
204        self.config.strip_prefix = Some(prefix.as_ref().to_path_buf());
205        self
206    }
207
208    /// Sets explicit archive format.
209    ///
210    /// If not set, format is auto-detected from output file extension.
211    ///
212    /// # Examples
213    ///
214    /// ```
215    /// use exarch_core::creation::ArchiveCreator;
216    /// use exarch_core::formats::detect::ArchiveType;
217    ///
218    /// let creator = ArchiveCreator::new().format(ArchiveType::TarGz);
219    /// ```
220    #[must_use]
221    pub fn format(mut self, format: ArchiveType) -> Self {
222        self.config.format = Some(format);
223        self
224    }
225
226    /// Creates the archive.
227    ///
228    /// # Errors
229    ///
230    /// Returns an error if:
231    /// - Output path not set
232    /// - No sources provided
233    /// - Source files don't exist
234    /// - I/O errors during creation
235    /// - Invalid configuration (e.g., invalid compression level)
236    ///
237    /// # Examples
238    ///
239    /// ```no_run
240    /// use exarch_core::creation::ArchiveCreator;
241    ///
242    /// let report = ArchiveCreator::new()
243    ///     .output("backup.tar.gz")
244    ///     .add_source("src/")
245    ///     .create()?;
246    /// # Ok::<(), exarch_core::ExtractionError>(())
247    /// ```
248    pub fn create(self) -> Result<CreationReport> {
249        let output_path =
250            self.output_path
251                .ok_or_else(|| ExtractionError::InvalidConfiguration {
252                    reason: "output path not set".to_string(),
253                })?;
254
255        if self.sources.is_empty() {
256            return Err(ExtractionError::InvalidConfiguration {
257                reason: "no source paths provided".to_string(),
258            });
259        }
260
261        // Validate configuration
262        self.config.validate()?;
263
264        crate::api::create_archive(&output_path, &self.sources, &self.config)
265    }
266}
267
268#[cfg(test)]
269#[allow(clippy::unwrap_used)]
270mod tests {
271    use super::*;
272    use crate::formats::detect::ArchiveType;
273    use std::path::PathBuf;
274
275    #[test]
276    fn test_builder_basic() {
277        let creator = ArchiveCreator::new()
278            .output("test.tar.gz")
279            .add_source("src/");
280
281        assert_eq!(creator.output_path, Some(PathBuf::from("test.tar.gz")));
282        assert_eq!(creator.sources, vec![PathBuf::from("src/")]);
283    }
284
285    #[test]
286    fn test_builder_multiple_sources() {
287        let creator = ArchiveCreator::new()
288            .add_source("src/")
289            .add_source("Cargo.toml")
290            .add_source("README.md");
291
292        assert_eq!(creator.sources.len(), 3);
293        assert_eq!(creator.sources[0], PathBuf::from("src/"));
294        assert_eq!(creator.sources[1], PathBuf::from("Cargo.toml"));
295        assert_eq!(creator.sources[2], PathBuf::from("README.md"));
296    }
297
298    #[test]
299    fn test_builder_sources_array() {
300        let creator = ArchiveCreator::new().sources(&["src/", "Cargo.toml", "README.md"]);
301
302        assert_eq!(creator.sources.len(), 3);
303    }
304
305    #[test]
306    fn test_builder_config_methods() {
307        let creator = ArchiveCreator::new()
308            .compression_level(9)
309            .follow_symlinks(true)
310            .include_hidden(true)
311            .exclude("*.log")
312            .exclude("target/")
313            .strip_prefix("/base")
314            .format(ArchiveType::TarGz);
315
316        assert_eq!(creator.config.compression_level, Some(9));
317        assert!(creator.config.follow_symlinks);
318        assert!(creator.config.include_hidden);
319        assert!(
320            creator
321                .config
322                .exclude_patterns
323                .contains(&"*.log".to_string())
324        );
325        assert!(
326            creator
327                .config
328                .exclude_patterns
329                .contains(&"target/".to_string())
330        );
331        assert_eq!(creator.config.strip_prefix, Some(PathBuf::from("/base")));
332        assert_eq!(creator.config.format, Some(ArchiveType::TarGz));
333    }
334
335    #[test]
336    fn test_builder_no_output_error() {
337        let creator = ArchiveCreator::new().add_source("src/");
338
339        let result = creator.create();
340        assert!(result.is_err());
341        assert!(matches!(
342            result.unwrap_err(),
343            ExtractionError::InvalidConfiguration { .. }
344        ));
345    }
346
347    #[test]
348    fn test_builder_no_sources_error() {
349        let creator = ArchiveCreator::new().output("test.tar.gz");
350
351        let result = creator.create();
352        assert!(result.is_err());
353        assert!(matches!(
354            result.unwrap_err(),
355            ExtractionError::InvalidConfiguration { .. }
356        ));
357    }
358
359    #[test]
360    fn test_builder_compression_level() {
361        let creator = ArchiveCreator::new().compression_level(9);
362
363        assert_eq!(creator.config.compression_level, Some(9));
364    }
365
366    #[test]
367    fn test_builder_exclude_patterns() {
368        let creator = ArchiveCreator::new()
369            .exclude("*.log")
370            .exclude("*.tmp")
371            .exclude(".git");
372
373        assert!(
374            creator
375                .config
376                .exclude_patterns
377                .contains(&"*.log".to_string())
378        );
379        assert!(
380            creator
381                .config
382                .exclude_patterns
383                .contains(&"*.tmp".to_string())
384        );
385        assert!(
386            creator
387                .config
388                .exclude_patterns
389                .contains(&".git".to_string())
390        );
391
392        // Default exclude patterns should still be there
393        assert!(
394            creator
395                .config
396                .exclude_patterns
397                .contains(&".DS_Store".to_string())
398        );
399    }
400
401    #[test]
402    fn test_builder_full_config() {
403        let config = CreationConfig::default()
404            .with_follow_symlinks(true)
405            .with_compression_level(9);
406
407        let creator = ArchiveCreator::new()
408            .output("test.tar.gz")
409            .add_source("src/")
410            .config(config);
411
412        assert!(creator.config.follow_symlinks);
413        assert_eq!(creator.config.compression_level, Some(9));
414    }
415
416    #[test]
417    fn test_builder_default() {
418        let creator = ArchiveCreator::default();
419        assert_eq!(creator.output_path, None);
420        assert_eq!(creator.sources.len(), 0);
421    }
422
423    #[test]
424    fn test_builder_new() {
425        let creator = ArchiveCreator::new();
426        assert_eq!(creator.output_path, None);
427        assert_eq!(creator.sources.len(), 0);
428    }
429}