OXYGENHUB
# 42fa95540468 oxygenhub / crates / hub / src / repos / archive.rs
4.0 KiB · Rust · last changed Milestone 0.6: releases, Git mirror, code search, anchored API comments 1 month ago source blame history raw
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 {
18
    pub fn parse(ext: &str) -> Option<Format> {
19
        match ext {
20
            "tar.zst" => Some(Format::TarZst),
21
            "zip" => Some(Format::Zip),
22
            _ => None,
23
        }
24
    }
25
26
    pub fn ext(self) -> &'static str {
27
        match self {
28
            Format::TarZst => "tar.zst",
29
            Format::Zip => "zip",
30
        }
31
    }
32
33
    pub fn mime(self) -> &'static str {
34
        match self {
35
            Format::TarZst => "application/zstd",
36
            Format::Zip => "application/zip",
37
        }
38
    }
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.
45
fn entries(repo: &Repo, loaded: &Loaded) -> anyhow::Result<Vec<Member>> {
46
    let mut out = Vec::new();
47
    for (path, f) in &loaded.snapshot.files {
48
        let bytes = match (&f.kind, &f.content) {
49
            (NodeKind::Dir, _) | (_, FileContent::Dir) => continue,
50
            (_, FileContent::Text { bytes, .. }) => bytes.clone(),
51
            (_, FileContent::Opaque { blobs }) => match blobs.first() {
52
                Some(b) => repo.read_blob(b)?,
53
                None => vec![],
54
            },
55
        };
56
        out.push((path.clone(), bytes, f.mode.exec, f.kind == NodeKind::Symlink));
57
    }
58
    Ok(out)
59
}
60
61
/// Write the archive of `loaded` under `prefix/` to `dest` (atomically).
62
pub fn build(repo: &Repo, loaded: &Loaded, prefix: &str, format: Format, dest: &Path) -> anyhow::Result<()> {
63
    if let Some(dir) = dest.parent() {
64
        std::fs::create_dir_all(dir)?;
65
    }
66
    let tmp = dest.with_extension("partial");
67
    let files = entries(repo, loaded)?;
68
    match format {
69
        Format::TarZst => {
70
            let out = std::fs::File::create(&tmp)?;
71
            let enc = zstd::Encoder::new(out, 19)?;
72
            let mut tar = tar::Builder::new(enc);
73
            for (path, bytes, exec, symlink) in &files {
74
                let name = format!("{prefix}/{path}");
75
                let mut h = tar::Header::new_gnu();
76
                h.set_mtime(0);
77
                if *symlink {
78
                    h.set_entry_type(tar::EntryType::Symlink);
79
                    h.set_size(0);
80
                    h.set_mode(0o777);
81
                    tar.append_link(&mut h, &name, String::from_utf8_lossy(bytes).as_ref())?;
82
                } else {
83
                    h.set_size(bytes.len() as u64);
84
                    h.set_mode(if *exec { 0o755 } else { 0o644 });
85
                    tar.append_data(&mut h, &name, bytes.as_slice())?;
86
                }
87
            }
88
            tar.into_inner()?.finish()?.flush()?;
89
        }
90
        Format::Zip => {
91
            let out = std::fs::File::create(&tmp)?;
92
            let mut zip = zip::ZipWriter::new(out);
93
            for (path, bytes, exec, symlink) in &files {
94
                let name = format!("{prefix}/{path}");
95
                let opts = zip::write::SimpleFileOptions::default()
96
                    .compression_method(zip::CompressionMethod::Deflated)
97
                    .unix_permissions(if *exec { 0o755 } else { 0o644 });
98
                if *symlink {
99
                    zip.add_symlink(name, String::from_utf8_lossy(bytes).as_ref(), opts)?;
100
                } else {
101
                    zip.start_file(name, opts)?;
102
                    zip.write_all(bytes)?;
103
                }
104
            }
105
            zip.finish()?.flush()?;
106
        }
107
    }
108
    std::fs::rename(&tmp, dest)?;
109
    Ok(())
110
}
111
112
/// Where the cached archive of a state lives.
113
pub fn cache_path(data: &Path, state: &str, prefix: &str, format: Format) -> PathBuf {
114
    data.join("cache").join("archives").join(format!("{state}-{prefix}.{}", format.ext()))
115
}