OXYGENHUB

Coverage / crates/hub/src/repos/archive.rs 85.5%

59 of 69 lines run, 6 of 12 functions · run #470 on main · run missed

1
//! Source archives of a state (hub spec §9): `.tar.zst` and `.zip` of the
2
//! materialised tree, cached by state hash since a state never changes.
3
4
use super::cache::Loaded;
5
use oxy_core::objects::NodeKind;
6
use oxy_core::repo::Repo;
7
use oxy_core::state::FileContent;
8
use std::io::Write;
9
use std::path::{Path, PathBuf};
10
11
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12
pub enum Format {
13
    TarZst,
14
    Zip,
15
}
16
17
impl Format {
183×
    pub fn parse(ext: &str) -> Option<Format> {
193×
        match ext {
203×
            "tar.zst" => Some(Format::TarZst),
212×
            "zip" => Some(Format::Zip),
220×
            _ => None,
23
        }
243×
    }
25
266×
    pub fn ext(self) -> &'static str {
276×
        match self {
282×
            Format::TarZst => "tar.zst",
294×
            Format::Zip => "zip",
30
        }
316×
    }
32
333×
    pub fn mime(self) -> &'static str {
343×
        match self {
351×
            Format::TarZst => "application/zstd",
362×
            Format::Zip => "application/zip",
37
        }
383×
    }
39
}
40
41
/// One archive member: path, bytes, executable, symlink.
42
type Member = (String, Vec<u8>, bool, bool);
43
44
/// Every regular file and symlink of the state.
453×
fn entries(repo: &Repo, loaded: &Loaded) -> anyhow::Result<Vec<Member>> {
463×
    let mut out = Vec::new();
4712×
    for (path, f) in &loaded.snapshot.files {
4812×
        let bytes = match (&f.kind, &f.content) {
494×
            (NodeKind::Dir, _) | (_, FileContent::Dir) => continue,
508×
            (_, FileContent::Text { bytes, .. }) => bytes.clone(),
510×
            (_, FileContent::Opaque { blobs }) => match blobs.first() {
520×
                Some(b) => repo.read_blob(b)?,
530×
                None => vec![],
54
            },
55
        };
568×
        out.push((path.clone(), bytes, f.mode.exec, f.kind == NodeKind::Symlink));
57
    }
583×
    Ok(out)
593×
}
60
61
/// Write the archive of `loaded` under `prefix/` to `dest` (atomically).
623×
pub fn build(repo: &Repo, loaded: &Loaded, prefix: &str, format: Format, dest: &Path) -> anyhow::Result<()> {
633×
    if let Some(dir) = dest.parent() {
643×
        std::fs::create_dir_all(dir)?;
650×
    }
663×
    let tmp = dest.with_extension("partial");
673×
    let files = entries(repo, loaded)?;
683×
    match format {
69
        Format::TarZst => {
701×
            let out = std::fs::File::create(&tmp)?;
711×
            let enc = zstd::Encoder::new(out, 19)?;
721×
            let mut tar = tar::Builder::new(enc);
732×
            for (path, bytes, exec, symlink) in &files {
742×
                let name = format!("{prefix}/{path}");
752×
                let mut h = tar::Header::new_gnu();
762×
                h.set_mtime(0);
772×
                if *symlink {
780×
                    h.set_entry_type(tar::EntryType::Symlink);
790×
                    h.set_size(0);
800×
                    h.set_mode(0o777);
810×
                    tar.append_link(&mut h, &name, String::from_utf8_lossy(bytes).as_ref())?;
82
                } else {
832×
                    h.set_size(bytes.len() as u64);
842×
                    h.set_mode(if *exec { 0o755 } else { 0o644 });
852×
                    tar.append_data(&mut h, &name, bytes.as_slice())?;
86
                }
87
            }
881×
            tar.into_inner()?.finish()?.flush()?;
89
        }
90
        Format::Zip => {
912×
            let out = std::fs::File::create(&tmp)?;
922×
            let mut zip = zip::ZipWriter::new(out);
936×
            for (path, bytes, exec, symlink) in &files {
946×
                let name = format!("{prefix}/{path}");
956×
                let opts = zip::write::SimpleFileOptions::default()
966×
                    .compression_method(zip::CompressionMethod::Deflated)
976×
                    .unix_permissions(if *exec { 0o755 } else { 0o644 });
986×
                if *symlink {
990×
                    zip.add_symlink(name, String::from_utf8_lossy(bytes).as_ref(), opts)?;
100
                } else {
1016×
                    zip.start_file(name, opts)?;
1026×
                    zip.write_all(bytes)?;
103
                }
104
            }
1052×
            zip.finish()?.flush()?;
106
        }
107
    }
1083×
    std::fs::rename(&tmp, dest)?;
1093×
    Ok(())
1103×
}
111
112
/// Where the cached archive of a state lives.
1133×
pub fn cache_path(data: &Path, state: &str, prefix: &str, format: Format) -> PathBuf {
1143×
    data.join("cache").join("archives").join(format!("{state}-{prefix}.{}", format.ext()))
1153×
}