1use crate::archive_error::ArchiveError;
2use crate::tree_differ::TreeDiffer;
3use crate::{get_full_file_extension, join_file_name};
4use rustc_hash::{FxHashMap, FxHashSet};
5use starbase_utils::glob;
6use std::path::{Path, PathBuf};
7use tracing::{instrument, trace};
8
9pub trait ArchivePacker {
11 fn add_file(&mut self, name: &str, file: &Path) -> Result<(), ArchiveError>;
13
14 fn add_dir(&mut self, name: &str, dir: &Path) -> Result<(), ArchiveError>;
16
17 fn pack(&mut self) -> Result<(), ArchiveError>;
19}
20
21pub trait ArchiveUnpacker {
23 fn unpack(&mut self, prefix: &str, differ: &mut TreeDiffer) -> Result<PathBuf, ArchiveError>;
26}
27
28#[derive(Debug)]
33pub struct Archiver<'owner> {
34 archive_file: &'owner Path,
36
37 prefix: &'owner str,
39
40 source_files: FxHashMap<PathBuf, String>,
42
43 source_globs: FxHashSet<String>,
45
46 pub source_root: &'owner Path,
49}
50
51impl<'owner> Archiver<'owner> {
52 pub fn new(source_root: &'owner Path, archive_file: &'owner Path) -> Self {
54 Archiver {
55 archive_file,
56 prefix: "",
57 source_files: FxHashMap::default(),
58 source_globs: FxHashSet::default(),
59 source_root,
60 }
61 }
62
63 pub fn add_source_file<F: AsRef<Path>>(
71 &mut self,
72 source: F,
73 custom_name: Option<&str>,
74 ) -> &mut Self {
75 let source = source.as_ref();
76 let source = source.strip_prefix(self.source_root).unwrap_or(source);
77
78 self.source_files.insert(
79 self.source_root.join(source),
80 custom_name
81 .map(|n| n.to_owned())
82 .unwrap_or_else(|| source.to_string_lossy().to_string()),
83 );
84
85 self
86 }
87
88 pub fn add_source_glob<G: AsRef<str>>(&mut self, glob: G) -> &mut Self {
94 self.source_globs.insert(glob.as_ref().to_owned());
95 self
96 }
97
98 pub fn set_prefix(&mut self, prefix: &'owner str) -> &mut Self {
101 self.prefix = prefix;
102 self
103 }
104
105 #[instrument(skip_all)]
110 pub fn pack<F, P>(&self, packer: F) -> Result<PathBuf, ArchiveError>
111 where
112 F: FnOnce(&Path) -> Result<P, ArchiveError>,
113 P: ArchivePacker,
114 {
115 trace!(
116 input_dir = ?self.source_root,
117 output_file = ?self.archive_file,
118 "Packing archive",
119 );
120
121 let mut archive = packer(self.archive_file)?;
122
123 for (source, file) in &self.source_files {
124 if !source.exists() {
125 trace!(source = ?source, "Source file does not exist, skipping");
126
127 continue;
128 }
129
130 let name = join_file_name([self.prefix, file]);
131
132 if source.is_file() {
133 archive.add_file(&name, source)?;
134 } else {
135 archive.add_dir(&name, source)?;
136 }
137 }
138
139 if !self.source_globs.is_empty() {
140 trace!(globs = ?self.source_globs, "Packing files using glob");
141
142 for file in glob::walk_files(self.source_root, &self.source_globs)? {
143 let file_name = file
144 .strip_prefix(self.source_root)
145 .unwrap()
146 .to_str()
147 .unwrap();
148
149 archive.add_file(&join_file_name([self.prefix, file_name]), &file)?;
150 }
151 }
152
153 archive.pack()?;
154
155 Ok(self.archive_file.to_path_buf())
156 }
157
158 pub fn pack_from_ext(&self) -> Result<(String, PathBuf), ArchiveError> {
161 let ext = get_full_file_extension(self.archive_file);
162 let out = self.archive_file.to_path_buf();
163
164 match ext.as_deref() {
165 Some("gz") => {
166 #[cfg(feature = "gz")]
167 self.pack(crate::gz::GzPacker::new)?;
168
169 #[cfg(not(feature = "gz"))]
170 return Err(ArchiveError::FeatureNotEnabled {
171 feature: "gz".into(),
172 path: self.archive_file.to_path_buf(),
173 });
174 }
175 Some("tar") => {
176 #[cfg(feature = "tar")]
177 self.pack(crate::tar::TarPacker::new)?;
178
179 #[cfg(not(feature = "tar"))]
180 return Err(ArchiveError::FeatureNotEnabled {
181 feature: "tar".into(),
182 path: self.archive_file.to_path_buf(),
183 });
184 }
185 Some("tar.bz2" | "tz2" | "tbz" | "tbz2") => {
186 #[cfg(feature = "tar-bz2")]
187 self.pack(crate::tar::TarPacker::new_bz2)?;
188
189 #[cfg(not(feature = "tar-bz2"))]
190 return Err(ArchiveError::FeatureNotEnabled {
191 feature: "tar-bz2".into(),
192 path: self.archive_file.to_path_buf(),
193 });
194 }
195 Some("tar.gz" | "tgz") => {
196 #[cfg(feature = "tar-gz")]
197 self.pack(crate::tar::TarPacker::new_gz)?;
198
199 #[cfg(not(feature = "tar-gz"))]
200 return Err(ArchiveError::FeatureNotEnabled {
201 feature: "tar-gz".into(),
202 path: self.archive_file.to_path_buf(),
203 });
204 }
205 Some("tar.xz" | "txz") => {
206 #[cfg(feature = "tar-xz")]
207 self.pack(crate::tar::TarPacker::new_xz)?;
208
209 #[cfg(not(feature = "tar-xz"))]
210 return Err(ArchiveError::FeatureNotEnabled {
211 feature: "tar-xz".into(),
212 path: self.archive_file.to_path_buf(),
213 });
214 }
215 Some("zst" | "zstd") => {
216 #[cfg(feature = "tar-zstd")]
217 self.pack(crate::tar::TarPacker::new_zstd)?;
218
219 #[cfg(not(feature = "tar-zstd"))]
220 return Err(ArchiveError::FeatureNotEnabled {
221 feature: "tar-zstd".into(),
222 path: self.archive_file.to_path_buf(),
223 });
224 }
225 Some("zip") => {
226 #[cfg(feature = "zip")]
227 self.pack(crate::zip::ZipPacker::new)?;
228
229 #[cfg(not(feature = "zip"))]
230 return Err(ArchiveError::FeatureNotEnabled {
231 feature: "zip".into(),
232 path: self.archive_file.to_path_buf(),
233 });
234 }
235 Some(ext) => {
236 return Err(ArchiveError::UnsupportedFormat {
237 format: ext.into(),
238 path: self.archive_file.to_path_buf(),
239 });
240 }
241 None => {
242 return Err(ArchiveError::UnknownFormat {
243 path: self.archive_file.to_path_buf(),
244 });
245 }
246 };
247
248 Ok((ext.unwrap(), out))
249 }
250
251 #[instrument(skip_all)]
261 pub fn unpack<F, P>(&self, unpacker: F) -> Result<PathBuf, ArchiveError>
262 where
263 F: FnOnce(&Path, &Path) -> Result<P, ArchiveError>,
264 P: ArchiveUnpacker,
265 {
266 trace!(
267 output_dir = ?self.source_root,
268 input_file = ?self.archive_file,
269 "Unpacking archive",
270 );
271
272 let mut lookup_paths = vec![];
273 lookup_paths.extend(self.source_files.values());
274 lookup_paths.extend(&self.source_globs);
275
276 let mut differ = TreeDiffer::load(self.source_root, lookup_paths)?;
277 let mut archive = unpacker(self.source_root, self.archive_file)?;
278
279 let out = archive.unpack(self.prefix, &mut differ)?;
280 differ.remove_stale_tracked_files();
281
282 Ok(out)
283 }
284
285 pub fn unpack_from_ext(&self) -> Result<(String, PathBuf), ArchiveError> {
291 let ext = get_full_file_extension(self.archive_file);
292 let out;
293
294 match ext.as_deref() {
295 Some("gz") => {
296 #[cfg(feature = "gz")]
297 {
298 out = self.unpack(crate::gz::GzUnpacker::new)?;
299 }
300
301 #[cfg(not(feature = "gz"))]
302 return Err(ArchiveError::FeatureNotEnabled {
303 feature: "gz".into(),
304 path: self.archive_file.to_path_buf(),
305 });
306 }
307 Some("tar") => {
308 #[cfg(feature = "tar")]
309 {
310 out = self.unpack(crate::tar::TarUnpacker::new)?;
311 }
312
313 #[cfg(not(feature = "tar"))]
314 return Err(ArchiveError::FeatureNotEnabled {
315 feature: "tar".into(),
316 path: self.archive_file.to_path_buf(),
317 });
318 }
319 Some("tar.bz2" | "tz2" | "tbz" | "tbz2") => {
320 #[cfg(feature = "tar-bz2")]
321 {
322 out = self.unpack(crate::tar::TarUnpacker::new_bz2)?;
323 }
324
325 #[cfg(not(feature = "tar-bz2"))]
326 return Err(ArchiveError::FeatureNotEnabled {
327 feature: "tar-bz2".into(),
328 path: self.archive_file.to_path_buf(),
329 });
330 }
331 Some("tar.gz" | "tgz") => {
332 #[cfg(feature = "tar-gz")]
333 {
334 out = self.unpack(crate::tar::TarUnpacker::new_gz)?;
335 }
336
337 #[cfg(not(feature = "tar-gz"))]
338 return Err(ArchiveError::FeatureNotEnabled {
339 feature: "tar-gz".into(),
340 path: self.archive_file.to_path_buf(),
341 });
342 }
343 Some("tar.xz" | "txz") => {
344 #[cfg(feature = "tar-xz")]
345 {
346 out = self.unpack(crate::tar::TarUnpacker::new_xz)?;
347 }
348
349 #[cfg(not(feature = "tar-xz"))]
350 return Err(ArchiveError::FeatureNotEnabled {
351 feature: "tar-xz".into(),
352 path: self.archive_file.to_path_buf(),
353 });
354 }
355 Some("zst" | "zstd") => {
356 #[cfg(feature = "tar-zstd")]
357 {
358 out = self.unpack(crate::tar::TarUnpacker::new_zstd)?;
359 }
360
361 #[cfg(not(feature = "tar-zstd"))]
362 return Err(ArchiveError::FeatureNotEnabled {
363 feature: "tar-zstd".into(),
364 path: self.archive_file.to_path_buf(),
365 });
366 }
367 Some("zip") => {
368 #[cfg(feature = "zip")]
369 {
370 out = self.unpack(crate::zip::ZipUnpacker::new)?;
371 }
372
373 #[cfg(not(feature = "zip"))]
374 return Err(ArchiveError::FeatureNotEnabled {
375 feature: "zip".into(),
376 path: self.archive_file.to_path_buf(),
377 });
378 }
379 Some(ext) => {
380 return Err(ArchiveError::UnsupportedFormat {
381 format: ext.into(),
382 path: self.archive_file.to_path_buf(),
383 });
384 }
385 None => {
386 return Err(ArchiveError::UnknownFormat {
387 path: self.archive_file.to_path_buf(),
388 });
389 }
390 };
391
392 Ok((ext.unwrap(), out))
393 }
394}