Skip to main content

starbase_archive/
tar.rs

1use crate::archive::{ArchivePacker, ArchiveUnpacker};
2use crate::archive_error::ArchiveError;
3pub use crate::tar_error::TarError;
4use crate::tree_differ::TreeDiffer;
5use binstall_tar::{Archive as TarArchive, Builder as TarBuilder};
6use starbase_utils::fs;
7use std::io::{Write, prelude::*};
8use std::path::{Path, PathBuf};
9use tracing::{instrument, trace};
10
11/// Creates tar archives.
12pub struct TarPacker {
13    archive: TarBuilder<Box<dyn Write>>,
14}
15
16impl TarPacker {
17    /// Create a new packer with a custom writer.
18    pub fn create(writer: Box<dyn Write>) -> Result<Self, ArchiveError> {
19        Ok(TarPacker {
20            archive: TarBuilder::new(writer),
21        })
22    }
23
24    /// Create a new `.tar` packer.
25    pub fn new(output_file: &Path) -> Result<Self, ArchiveError> {
26        TarPacker::create(Box::new(fs::create_file(output_file)?))
27    }
28
29    /// Create a new `.tar.gz` packer.
30    #[cfg(feature = "tar-gz")]
31    pub fn new_gz(output_file: &Path) -> Result<Self, ArchiveError> {
32        Self::new_gz_with_level(output_file, 4)
33    }
34
35    /// Create a new `.tar.gz` packer with a custom compression level.
36    #[cfg(feature = "tar-gz")]
37    pub fn new_gz_with_level(output_file: &Path, level: u32) -> Result<Self, ArchiveError> {
38        TarPacker::create(Box::new(flate2::write::GzEncoder::new(
39            fs::create_file(output_file)?,
40            flate2::Compression::new(level),
41        )))
42    }
43
44    /// Create a new `.tar.xz` packer.
45    #[cfg(feature = "tar-xz")]
46    pub fn new_xz(output_file: &Path) -> Result<Self, ArchiveError> {
47        Self::new_xz_with_level(output_file, 4)
48    }
49
50    /// Create a new `.tar.xz` packer with a custom compression level.
51    #[cfg(feature = "tar-xz")]
52    pub fn new_xz_with_level(output_file: &Path, level: u32) -> Result<Self, ArchiveError> {
53        TarPacker::create(Box::new(liblzma::write::XzEncoder::new(
54            fs::create_file(output_file)?,
55            level,
56        )))
57    }
58
59    /// Create a new `.tar.zstd` packer.
60    #[cfg(feature = "tar-zstd")]
61    pub fn new_zstd(output_file: &Path) -> Result<Self, ArchiveError> {
62        Self::new_zstd_with_level(output_file, 3) // Default in lib
63    }
64
65    /// Create a new `.tar.zstd` packer with a custom compression level.
66    #[cfg(feature = "tar-zstd")]
67    pub fn new_zstd_with_level(output_file: &Path, level: u32) -> Result<Self, ArchiveError> {
68        let encoder = zstd::stream::Encoder::new(fs::create_file(output_file)?, level as i32)
69            .map_err(|error| TarError::ZstdDictionary {
70                error: Box::new(error),
71            })?;
72
73        TarPacker::create(Box::new(encoder.auto_finish()))
74    }
75
76    /// Create a new `.tar.bz2` packer.
77    #[cfg(feature = "tar-bz2")]
78    pub fn new_bz2(output_file: &Path) -> Result<Self, ArchiveError> {
79        Self::new_bz2_with_level(output_file, 6) // Default in lib
80    }
81
82    /// Create a new `.tar.gz` packer with a custom compression level.
83    #[cfg(feature = "tar-bz2")]
84    pub fn new_bz2_with_level(output_file: &Path, level: u32) -> Result<Self, ArchiveError> {
85        TarPacker::create(Box::new(bzip2::write::BzEncoder::new(
86            fs::create_file(output_file)?,
87            bzip2::Compression::new(level),
88        )))
89    }
90}
91
92impl ArchivePacker for TarPacker {
93    fn add_file(&mut self, name: &str, file: &Path) -> Result<(), ArchiveError> {
94        trace!(source = name, input = ?file, "Packing file");
95
96        self.archive
97            .append_file(name, &mut fs::open_file(file)?)
98            .map_err(|error| TarError::AddFailure {
99                source: file.to_path_buf(),
100                error: Box::new(error),
101            })?;
102
103        Ok(())
104    }
105
106    fn add_dir(&mut self, name: &str, dir: &Path) -> Result<(), ArchiveError> {
107        trace!(source = name, input = ?dir, "Packing directory");
108
109        self.archive
110            .append_dir_all(name, dir)
111            .map_err(|error| TarError::AddFailure {
112                source: dir.to_path_buf(),
113                error: Box::new(error),
114            })?;
115
116        Ok(())
117    }
118
119    #[instrument(name = "pack_tar", skip_all)]
120    fn pack(&mut self) -> Result<(), ArchiveError> {
121        trace!("Creating tarball");
122
123        self.archive
124            .finish()
125            .map_err(|error| TarError::PackFailure {
126                error: Box::new(error),
127            })?;
128
129        Ok(())
130    }
131}
132
133/// Opens tar archives.
134pub struct TarUnpacker {
135    archive: TarArchive<Box<dyn Read>>,
136    output_dir: PathBuf,
137}
138
139impl TarUnpacker {
140    /// Create a new unpacker with a custom reader.
141    pub fn create(output_dir: &Path, reader: Box<dyn Read>) -> Result<Self, ArchiveError> {
142        fs::create_dir_all(output_dir)?;
143
144        Ok(TarUnpacker {
145            archive: TarArchive::new(reader),
146            output_dir: output_dir.to_path_buf(),
147        })
148    }
149
150    /// Create a new `.tar` unpacker.
151    pub fn new(output_dir: &Path, input_file: &Path) -> Result<Self, ArchiveError> {
152        TarUnpacker::create(output_dir, Box::new(fs::open_file(input_file)?))
153    }
154
155    /// Create a new `.tar.gz` unpacker.
156    #[cfg(feature = "tar-gz")]
157    pub fn new_gz(output_dir: &Path, input_file: &Path) -> Result<Self, ArchiveError> {
158        TarUnpacker::create(
159            output_dir,
160            Box::new(flate2::read::GzDecoder::new(fs::open_file(input_file)?)),
161        )
162    }
163
164    /// Create a new `.tar.xz` unpacker.
165    #[cfg(feature = "tar-xz")]
166    pub fn new_xz(output_dir: &Path, input_file: &Path) -> Result<Self, ArchiveError> {
167        TarUnpacker::create(
168            output_dir,
169            Box::new(liblzma::read::XzDecoder::new(fs::open_file(input_file)?)),
170        )
171    }
172
173    /// Create a new `.tar.zstd` unpacker.
174    #[cfg(feature = "tar-zstd")]
175    pub fn new_zstd(output_dir: &Path, input_file: &Path) -> Result<Self, ArchiveError> {
176        let decoder = zstd::stream::Decoder::new(fs::open_file(input_file)?).map_err(|error| {
177            TarError::ZstdDictionary {
178                error: Box::new(error),
179            }
180        })?;
181
182        TarUnpacker::create(output_dir, Box::new(decoder))
183    }
184
185    /// Create a new `.tar.bz2` unpacker.
186    #[cfg(feature = "tar-bz2")]
187    pub fn new_bz2(output_dir: &Path, input_file: &Path) -> Result<Self, ArchiveError> {
188        TarUnpacker::create(
189            output_dir,
190            Box::new(bzip2::read::BzDecoder::new(fs::open_file(input_file)?)),
191        )
192    }
193}
194
195impl ArchiveUnpacker for TarUnpacker {
196    #[instrument(name = "unpack_tar", skip_all)]
197    fn unpack(&mut self, prefix: &str, differ: &mut TreeDiffer) -> Result<PathBuf, ArchiveError> {
198        self.archive.set_overwrite(true);
199
200        trace!(output_dir = ?self.output_dir, "Opening tarball");
201
202        let mut count = 0;
203
204        for entry in self
205            .archive
206            .entries()
207            .map_err(|error| TarError::UnpackFailure {
208                error: Box::new(error),
209            })?
210        {
211            let mut entry = entry.map_err(|error| TarError::UnpackFailure {
212                error: Box::new(error),
213            })?;
214            let mut path: PathBuf = entry.path().unwrap().into_owned();
215
216            // Remove the prefix
217            if !prefix.is_empty()
218                && let Ok(suffix) = path.strip_prefix(prefix)
219            {
220                path = suffix.to_owned();
221            }
222
223            // Unpack the file if different than destination
224            let output_path = self.output_dir.join(&path);
225
226            if let Some(parent_dir) = output_path.parent() {
227                fs::create_dir_all(parent_dir)?;
228            }
229
230            // trace!(source = ?path, "Unpacking file");
231
232            // NOTE: gzip doesn't support seeking, so we can't use the following util then!
233            // if differ.should_write_source(entry.size(), &mut entry, &output_path)? {
234            entry
235                .unpack(&output_path)
236                .map_err(|error| TarError::ExtractFailure {
237                    source: output_path.clone(),
238                    error: Box::new(error),
239                })?;
240            // }
241
242            differ.untrack_file(&output_path);
243            count += 1;
244        }
245
246        trace!("Unpacked {} files", count);
247
248        Ok(self.output_dir.clone())
249    }
250}