//! Source archives of a state (hub spec ยง9): `.tar.zst` and `.zip` of the //! materialised tree, cached by state hash since a state never changes. use super::cache::Loaded; use oxy_core::objects::NodeKind; use oxy_core::repo::Repo; use oxy_core::state::FileContent; use std::io::Write; use std::path::{Path, PathBuf}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Format { TarZst, Zip, } impl Format { pub fn parse(ext: &str) -> Option { match ext { "tar.zst" => Some(Format::TarZst), "zip" => Some(Format::Zip), _ => None, } } pub fn ext(self) -> &'static str { match self { Format::TarZst => "tar.zst", Format::Zip => "zip", } } pub fn mime(self) -> &'static str { match self { Format::TarZst => "application/zstd", Format::Zip => "application/zip", } } } /// One archive member: path, bytes, executable, symlink. type Member = (String, Vec, bool, bool); /// Every regular file and symlink of the state. fn entries(repo: &Repo, loaded: &Loaded) -> anyhow::Result> { let mut out = Vec::new(); for (path, f) in &loaded.snapshot.files { let bytes = match (&f.kind, &f.content) { (NodeKind::Dir, _) | (_, FileContent::Dir) => continue, (_, FileContent::Text { bytes, .. }) => bytes.clone(), (_, FileContent::Opaque { blobs }) => match blobs.first() { Some(b) => repo.read_blob(b)?, None => vec![], }, }; out.push((path.clone(), bytes, f.mode.exec, f.kind == NodeKind::Symlink)); } Ok(out) } /// Write the archive of `loaded` under `prefix/` to `dest` (atomically). pub fn build(repo: &Repo, loaded: &Loaded, prefix: &str, format: Format, dest: &Path) -> anyhow::Result<()> { if let Some(dir) = dest.parent() { std::fs::create_dir_all(dir)?; } let tmp = dest.with_extension("partial"); let files = entries(repo, loaded)?; match format { Format::TarZst => { let out = std::fs::File::create(&tmp)?; let enc = zstd::Encoder::new(out, 19)?; let mut tar = tar::Builder::new(enc); for (path, bytes, exec, symlink) in &files { let name = format!("{prefix}/{path}"); let mut h = tar::Header::new_gnu(); h.set_mtime(0); if *symlink { h.set_entry_type(tar::EntryType::Symlink); h.set_size(0); h.set_mode(0o777); tar.append_link(&mut h, &name, String::from_utf8_lossy(bytes).as_ref())?; } else { h.set_size(bytes.len() as u64); h.set_mode(if *exec { 0o755 } else { 0o644 }); tar.append_data(&mut h, &name, bytes.as_slice())?; } } tar.into_inner()?.finish()?.flush()?; } Format::Zip => { let out = std::fs::File::create(&tmp)?; let mut zip = zip::ZipWriter::new(out); for (path, bytes, exec, symlink) in &files { let name = format!("{prefix}/{path}"); let opts = zip::write::SimpleFileOptions::default() .compression_method(zip::CompressionMethod::Deflated) .unix_permissions(if *exec { 0o755 } else { 0o644 }); if *symlink { zip.add_symlink(name, String::from_utf8_lossy(bytes).as_ref(), opts)?; } else { zip.start_file(name, opts)?; zip.write_all(bytes)?; } } zip.finish()?.flush()?; } } std::fs::rename(&tmp, dest)?; Ok(()) } /// Where the cached archive of a state lives. pub fn cache_path(data: &Path, state: &str, prefix: &str, format: Format) -> PathBuf { data.join("cache").join("archives").join(format!("{state}-{prefix}.{}", format.ext())) }