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}