|
| 1 | +// This Source Code Form is subject to the terms of the Mozilla Public |
| 2 | +// License, v. 2.0. If a copy of the MPL was not distributed with this |
| 3 | +// file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 4 | + |
| 5 | +//! Tools for creating and inserting into tarballs. |
| 6 | +
|
| 7 | +use anyhow::{anyhow, bail, Context, Result}; |
| 8 | +use async_trait::async_trait; |
| 9 | +use camino::Utf8Path; |
| 10 | +use flate2::write::GzEncoder; |
| 11 | +use std::convert::TryInto; |
| 12 | +use std::fs::{File, OpenOptions}; |
| 13 | +use tar::Builder; |
| 14 | + |
| 15 | +/// These interfaces are similar to some methods in [tar::Builder]. |
| 16 | +/// |
| 17 | +/// They use [tokio::block_in_place] to avoid blocking other async |
| 18 | +/// tasks using the executor. |
| 19 | +#[async_trait] |
| 20 | +pub trait AsyncAppendFile { |
| 21 | + async fn append_file_async<P>(&mut self, path: P, file: &mut File) -> std::io::Result<()> |
| 22 | + where |
| 23 | + P: AsRef<Utf8Path> + Send; |
| 24 | + |
| 25 | + async fn append_path_with_name_async<P, N>(&mut self, path: P, name: N) -> std::io::Result<()> |
| 26 | + where |
| 27 | + P: AsRef<Utf8Path> + Send, |
| 28 | + N: AsRef<Utf8Path> + Send; |
| 29 | + |
| 30 | + async fn append_dir_all_async<P, Q>(&mut self, path: P, src_path: Q) -> std::io::Result<()> |
| 31 | + where |
| 32 | + P: AsRef<Utf8Path> + Send, |
| 33 | + Q: AsRef<Utf8Path> + Send; |
| 34 | +} |
| 35 | + |
| 36 | +#[async_trait] |
| 37 | +impl<W: Encoder> AsyncAppendFile for Builder<W> { |
| 38 | + async fn append_file_async<P>(&mut self, path: P, file: &mut File) -> std::io::Result<()> |
| 39 | + where |
| 40 | + P: AsRef<Utf8Path> + Send, |
| 41 | + { |
| 42 | + tokio::task::block_in_place(move || self.append_file(path.as_ref(), file)) |
| 43 | + } |
| 44 | + |
| 45 | + async fn append_path_with_name_async<P, N>(&mut self, path: P, name: N) -> std::io::Result<()> |
| 46 | + where |
| 47 | + P: AsRef<Utf8Path> + Send, |
| 48 | + N: AsRef<Utf8Path> + Send, |
| 49 | + { |
| 50 | + tokio::task::block_in_place(move || { |
| 51 | + self.append_path_with_name(path.as_ref(), name.as_ref()) |
| 52 | + }) |
| 53 | + } |
| 54 | + |
| 55 | + async fn append_dir_all_async<P, Q>(&mut self, path: P, src_path: Q) -> std::io::Result<()> |
| 56 | + where |
| 57 | + P: AsRef<Utf8Path> + Send, |
| 58 | + Q: AsRef<Utf8Path> + Send, |
| 59 | + { |
| 60 | + tokio::task::block_in_place(move || self.append_dir_all(path.as_ref(), src_path.as_ref())) |
| 61 | + } |
| 62 | +} |
| 63 | + |
| 64 | +/// Helper to open a tarfile for reading/writing. |
| 65 | +pub fn create_tarfile<P: AsRef<Utf8Path> + std::fmt::Debug>(tarfile: P) -> Result<File> { |
| 66 | + OpenOptions::new() |
| 67 | + .write(true) |
| 68 | + .read(true) |
| 69 | + .truncate(true) |
| 70 | + .create(true) |
| 71 | + .open(tarfile.as_ref()) |
| 72 | + .map_err(|err| anyhow!("Cannot create tarfile {:?}: {}", tarfile, err)) |
| 73 | +} |
| 74 | + |
| 75 | +/// Helper to open a tarfile for reading. |
| 76 | +pub fn open_tarfile<P: AsRef<Utf8Path> + std::fmt::Debug>(tarfile: P) -> Result<File> { |
| 77 | + OpenOptions::new() |
| 78 | + .read(true) |
| 79 | + .open(tarfile.as_ref()) |
| 80 | + .map_err(|err| anyhow!("Cannot open tarfile {:?}: {}", tarfile, err)) |
| 81 | +} |
| 82 | + |
| 83 | +pub trait Encoder: std::io::Write + Send {} |
| 84 | +impl<T> Encoder for T where T: std::io::Write + Send {} |
| 85 | + |
| 86 | +pub struct ArchiveBuilder<E: Encoder> { |
| 87 | + pub builder: tar::Builder<E>, |
| 88 | +} |
| 89 | + |
| 90 | +impl<E: Encoder> ArchiveBuilder<E> { |
| 91 | + pub fn new(builder: tar::Builder<E>) -> Self { |
| 92 | + Self { builder } |
| 93 | + } |
| 94 | + |
| 95 | + pub fn into_inner(self) -> Result<E> { |
| 96 | + self.builder.into_inner().context("Finalizing archive") |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +/// Adds a package at `package_path` to a new zone image |
| 101 | +/// being built using the `archive` builder. |
| 102 | +pub async fn add_package_to_zone_archive<E: Encoder>( |
| 103 | + archive: &mut ArchiveBuilder<E>, |
| 104 | + package_path: &Utf8Path, |
| 105 | +) -> Result<()> { |
| 106 | + let tmp = camino_tempfile::tempdir()?; |
| 107 | + let gzr = flate2::read::GzDecoder::new(open_tarfile(package_path)?); |
| 108 | + if gzr.header().is_none() { |
| 109 | + bail!( |
| 110 | + "Missing gzip header from {} - cannot add it to zone image", |
| 111 | + package_path, |
| 112 | + ); |
| 113 | + } |
| 114 | + let mut component_reader = tar::Archive::new(gzr); |
| 115 | + let entries = component_reader.entries()?; |
| 116 | + |
| 117 | + // First, unpack the existing entries |
| 118 | + for entry in entries { |
| 119 | + let mut entry = entry?; |
| 120 | + |
| 121 | + // Ignore the JSON header files |
| 122 | + let entry_path = entry.path()?; |
| 123 | + if entry_path == Utf8Path::new("oxide.json") { |
| 124 | + continue; |
| 125 | + } |
| 126 | + |
| 127 | + let entry_path: &Utf8Path = entry_path.strip_prefix("root/")?.try_into()?; |
| 128 | + let entry_unpack_path = tmp.path().join(entry_path); |
| 129 | + entry.unpack(&entry_unpack_path)?; |
| 130 | + |
| 131 | + let entry_path = entry.path()?.into_owned(); |
| 132 | + let entry_path: &Utf8Path = entry_path.as_path().try_into()?; |
| 133 | + assert!(entry_unpack_path.exists()); |
| 134 | + |
| 135 | + archive |
| 136 | + .builder |
| 137 | + .append_path_with_name_async(entry_unpack_path, entry_path) |
| 138 | + .await?; |
| 139 | + } |
| 140 | + Ok(()) |
| 141 | +} |
| 142 | + |
| 143 | +pub async fn new_compressed_archive_builder( |
| 144 | + path: &Utf8Path, |
| 145 | +) -> Result<ArchiveBuilder<GzEncoder<File>>> { |
| 146 | + let file = create_tarfile(path)?; |
| 147 | + // TODO: Consider using async compression, async tar. |
| 148 | + // It's not the *worst* thing in the world for a packaging tool to block |
| 149 | + // here, but it would help the other async threads remain responsive if |
| 150 | + // we avoided blocking. |
| 151 | + let gzw = GzEncoder::new(file, flate2::Compression::fast()); |
| 152 | + let mut archive = Builder::new(gzw); |
| 153 | + archive.mode(tar::HeaderMode::Deterministic); |
| 154 | + |
| 155 | + Ok(ArchiveBuilder::new(archive)) |
| 156 | +} |
0 commit comments