OXYGENHUB

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

61 of 69 lines run, 6 of 12 functions · run #539 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 {
184×
    pub fn parse(ext: &str) -> Option<Format> {
194×
        match ext {
204×
            "tar.zst" => Some(Format::TarZst),
213×
            "zip" => Some(Format::Zip),
220×
            _ => None,
23
        }
244×
    }
25
268×
    pub fn ext(self) -> &'static str {
278×
        match self {
282×
            Format::TarZst => "tar.zst",
296×
            Format::Zip => "zip",
30
        }
318×
    }
32
334×
    pub fn mime(self) -> &'static str {
344×
        match self {
351×
            Format::TarZst => "application/zstd",
363×
            Format::Zip => "application/zip",
37
        }
384×
    }
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.
454×
fn entries(repo: &Repo, loaded: &Loaded) -> anyhow::Result<Vec<Member>> {
464×
    let mut out = Vec::new();
4715×
    for (path, f) in &loaded.snapshot.files {
4815×
        let bytes = match (&f.kind, &f.content) {
495×
            (NodeKind::Dir, _) | (_, FileContent::Dir) => continue,
509×
            (_, FileContent::Text { bytes, .. }) => bytes.clone(),
511×
            (_, FileContent::Opaque { blobs }) => match blobs.first() {
521×
                Some(b) => repo.read_blob(b)?,
530×
                None => vec![],
54
            },
55
        };
5610×
        out.push((path.clone(), bytes, f.mode.exec, f.kind == NodeKind::Symlink));
57
    }
584×
    Ok(out)
594×
}
60
61
/// Write the archive of `loaded` under `prefix/` to `dest` (atomically).
624×
pub fn build(repo: &Repo, loaded: &Loaded, prefix: &str, format: Format, dest: &Path) -> anyhow::Result<()> {
634×
    if let Some(dir) = dest.parent() {
644×
        std::fs::create_dir_all(dir)?;
650×
    }
664×
    let tmp = dest.with_extension("partial");
674×
    let files = entries(repo, loaded)?;
684×
    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 => {
913×
            let out = std::fs::File::create(&tmp)?;
923×
            let mut zip = zip::ZipWriter::new(out);
938×
            for (path, bytes, exec, symlink) in &files {
948×
                let name = format!("{prefix}/{path}");
958×
                let opts = zip::write::SimpleFileOptions::default()
968×
                    .compression_method(zip::CompressionMethod::Deflated)
978×
                    .unix_permissions(if *exec { 0o755 } else { 0o644 });
988×
                if *symlink {
990×
                    zip.add_symlink(name, String::from_utf8_lossy(bytes).as_ref(), opts)?;
100
                } else {
1018×
                    zip.start_file(name, opts)?;
1028×
                    zip.write_all(bytes)?;
103
                }
104
            }
1053×
            zip.finish()?.flush()?;
106
        }
107
    }
1084×
    std::fs::rename(&tmp, dest)?;
1094×
    Ok(())
1104×
}
111
112
/// Where the cached archive of a state lives.
1134×
pub fn cache_path(data: &Path, state: &str, prefix: &str, format: Format) -> PathBuf {
1144×
    data.join("cache").join("archives").join(format!("{state}-{prefix}.{}", format.ext()))
1154×
}