containers_api/
tarball.rs

1//! Utility functions to compression.
2
3use flate2::{write::GzEncoder, Compression};
4use std::{
5    fs::{self, File},
6    io::{self, Write},
7    path::{Path, MAIN_SEPARATOR},
8};
9use tar::Builder;
10
11#[cfg(feature = "par-compress")]
12use gzp::{
13    deflate::Gzip,
14    par::compress::{ParCompress, ParCompressBuilder},
15};
16
17/// Writes a gunzip encoded tarball to `buf` from entries found in `path`.
18pub fn dir<W, P>(buf: W, path: P) -> io::Result<()>
19where
20    W: Write,
21    P: AsRef<Path>,
22{
23    let encoder = GzEncoder::new(buf, Compression::best());
24    let path = path.as_ref();
25    ArchiveBuilder::build(encoder, path)?;
26
27    Ok(())
28}
29
30#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))]
31#[cfg(feature = "par-compress")]
32/// Same as [`dir`](dir) but initializes the underlying buffer, returns it and utilizes compression
33/// parallelization on multiple cores to speed up the work.
34pub fn dir_par<P>(path: P) -> io::Result<Vec<u8>>
35where
36    P: AsRef<Path>,
37{
38    use memfile::MemFile;
39    use std::io::{Read, Seek};
40
41    let tx = MemFile::create_default(&path.as_ref().to_string_lossy())?;
42    let mut rx = tx.try_clone()?;
43    let encoder: ParCompress<Gzip> = ParCompressBuilder::new().from_writer(tx);
44
45    let path = path.as_ref();
46    ArchiveBuilder::build(encoder, path)?;
47
48    rx.rewind()?;
49    let mut data = vec![];
50    rx.read_to_end(&mut data)?;
51    Ok(data)
52}
53
54#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "freebsd")))]
55#[cfg(feature = "par-compress")]
56/// Same as [`dir`](dir) but initializes the underlying buffer, returns it and utilizes compression
57/// parallelization on multiple cores to speed up the work.
58pub fn dir_par<P>(path: P) -> io::Result<Vec<u8>>
59where
60    P: AsRef<Path>,
61{
62    use std::io::{Read, Seek};
63
64    let tmp_dir = tempfile::tempdir()?;
65    let tmp_file_path = tmp_dir.path().join("data");
66    let tx = std::fs::File::create(&tmp_file_path)?;
67
68    let encoder: ParCompress<Gzip> = ParCompressBuilder::new().from_writer(tx);
69
70    let path = path.as_ref();
71    ArchiveBuilder::build(encoder, path)?;
72
73    let mut rx = std::fs::File::open(&tmp_file_path)?;
74    rx.rewind()?;
75    let mut data = vec![];
76    rx.read_to_end(&mut data)?;
77    Ok(data)
78}
79
80fn resolve_base_path(canonical_path: &Path) -> io::Result<String> {
81    let mut base_path_str = canonical_path
82        .to_str()
83        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid base path"))?
84        .to_owned();
85    if let Some(last) = base_path_str.chars().last() {
86        if last != MAIN_SEPARATOR {
87            base_path_str.push(MAIN_SEPARATOR)
88        }
89    }
90    Ok(base_path_str)
91}
92
93struct ArchiveBuilder<W: Write> {
94    archive: Builder<W>,
95    base_path: String,
96}
97
98impl<W: Write> ArchiveBuilder<W> {
99    fn build(buf: W, path: &Path) -> io::Result<()> {
100        let canonical = path.canonicalize()?;
101        let mut builder = Self::new(buf, &canonical)?;
102        builder.bundle(&canonical, false)?;
103        builder.archive.finish()?;
104        builder.archive.into_inner()?.flush()
105    }
106
107    fn new(buf: W, canonical: &Path) -> io::Result<Self> {
108        let base_path = resolve_base_path(canonical)?;
109
110        Ok(Self {
111            archive: Builder::new(buf),
112            base_path,
113        })
114    }
115
116    /// Starts the traversal by bundling files/directories in the base path to the archive.
117    fn bundle(&mut self, dir: &Path, bundle_dir: bool) -> io::Result<()> {
118        if fs::metadata(dir)?.is_dir() {
119            if bundle_dir {
120                self.append_entry(dir)?;
121            }
122            for entry in fs::read_dir(dir)? {
123                let entry = entry?;
124                if fs::metadata(entry.path())?.is_dir() {
125                    self.bundle(&entry.path(), true)?;
126                } else {
127                    self.append_entry(entry.path().as_path())?
128                }
129            }
130        }
131        Ok(())
132    }
133
134    fn append_entry(&mut self, path: &Path) -> io::Result<()> {
135        let canonical = path.canonicalize()?;
136        let relativized = canonical
137            .to_str()
138            .ok_or_else(|| {
139                io::Error::new(io::ErrorKind::InvalidInput, "invalid canonicalized path")
140            })?
141            .trim_start_matches(&self.base_path[..]);
142        if path.is_dir() {
143            self.archive
144                .append_dir(Path::new(relativized), &canonical)?
145        } else {
146            self.archive
147                .append_file(Path::new(relativized), &mut File::open(&canonical)?)?
148        }
149        Ok(())
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use flate2::read::GzDecoder;
157    use tar::Archive;
158    const N_DIRS: usize = 3;
159    const N_ENTRIES: usize = 10;
160
161    fn _prepare_dirs(tmp: &std::path::Path) {
162        for i in 1..=N_DIRS {
163            let d_path = tmp.join(&format!("d{i}"));
164            std::fs::create_dir(&d_path).unwrap();
165            for j in 1..=N_ENTRIES {
166                let f_path = d_path.join(&format!("f{}", i * j));
167                let mut f = std::fs::File::create(&f_path).unwrap();
168                let _ = f.write(&[j as u8]).unwrap();
169                f.flush().unwrap();
170            }
171        }
172    }
173
174    fn _verify_archive(buf: &[u8]) {
175        let decoder = GzDecoder::new(buf);
176        let mut archive = Archive::new(decoder);
177
178        let tmp = tempfile::tempdir().unwrap();
179        archive.unpack(tmp.path()).unwrap();
180
181        for i in 1..=N_DIRS {
182            let d_path = tmp.path().join(&format!("d{i}"));
183            assert!(d_path.exists());
184            for j in 1..=N_ENTRIES {
185                let f_path = d_path.join(&format!("f{}", i * j));
186                assert!(f_path.exists());
187            }
188        }
189    }
190
191    #[test]
192    fn creates_gzipped_dir() {
193        let tmp = tempfile::tempdir().unwrap();
194        _prepare_dirs(tmp.path());
195        let mut buf = vec![];
196        dir(&mut buf, tmp.path()).unwrap();
197        _verify_archive(&buf[..]);
198    }
199
200    #[test]
201    #[cfg(feature = "par-compress")]
202    fn creates_gzipped_dir_par() {
203        let tmp = tempfile::tempdir().unwrap();
204        _prepare_dirs(tmp.path());
205        let buf = dir_par(tmp.path()).unwrap();
206        _verify_archive(&buf[..]);
207    }
208}