use super::utils::{get_file_mode, set_file_mode};
use crate::errors::*;
use fs_err as fs;
use std::fmt;
use std::io::{Cursor, Read, Seek, Write};
use std::path::PathBuf;
use tempfile::NamedTempFile;
use zip::write::FileOptions;
use zip::{CompressionMethod, ZipArchive, ZipWriter};
#[derive(Clone)]
pub struct FileObjectSource {
pub key: String,
pub path: PathBuf,
pub optional: bool,
}
pub enum Cache {
Hit(CacheRead),
Miss,
None,
Recache,
}
impl fmt::Debug for Cache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Cache::Hit(_) => write!(f, "Cache::Hit(...)"),
Cache::Miss => write!(f, "Cache::Miss"),
Cache::None => write!(f, "Cache::None"),
Cache::Recache => write!(f, "Cache::Recache"),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum CacheMode {
ReadOnly,
ReadWrite,
}
pub trait ReadSeek: Read + Seek + Send {}
impl<T: Read + Seek + Send> ReadSeek for T {}
pub struct CacheRead {
zip: ZipArchive<Box<dyn ReadSeek>>,
}
#[derive(Debug)]
pub struct DecompressionFailure;
impl std::fmt::Display for DecompressionFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "failed to decompress content")
}
}
impl std::error::Error for DecompressionFailure {}
impl CacheRead {
pub fn from<R>(reader: R) -> Result<CacheRead>
where
R: ReadSeek + 'static,
{
let z = ZipArchive::new(Box::new(reader) as Box<dyn ReadSeek>)
.context("Failed to parse cache entry")?;
Ok(CacheRead { zip: z })
}
pub fn get_object<T>(&mut self, name: &str, to: &mut T) -> Result<Option<u32>>
where
T: Write,
{
let file = self.zip.by_name(name).or(Err(DecompressionFailure))?;
if file.compression() != CompressionMethod::Stored {
bail!(DecompressionFailure);
}
let mode = file.unix_mode();
zstd::stream::copy_decode(file, to).or(Err(DecompressionFailure))?;
Ok(mode)
}
pub fn get_stdout(&mut self) -> Vec<u8> {
self.get_bytes("stdout")
}
pub fn get_stderr(&mut self) -> Vec<u8> {
self.get_bytes("stderr")
}
fn get_bytes(&mut self, name: &str) -> Vec<u8> {
let mut bytes = Vec::new();
drop(self.get_object(name, &mut bytes));
bytes
}
pub async fn extract_objects<T>(
mut self,
objects: T,
pool: &tokio::runtime::Handle,
) -> Result<()>
where
T: IntoIterator<Item = FileObjectSource> + Send + Sync + 'static,
{
pool.spawn_blocking(move || {
for FileObjectSource {
key,
path,
optional,
} in objects
{
let dir = match path.parent() {
Some(d) => d,
None => bail!("Output file without a parent directory!"),
};
let mut tmp = NamedTempFile::new_in(dir)?;
match (self.get_object(&key, &mut tmp), optional) {
(Ok(mode), _) => {
tmp.persist(&path)?;
if let Some(mode) = mode {
set_file_mode(&path, mode)?;
}
}
(Err(e), false) => return Err(e),
(Err(_), true) => continue,
}
}
Ok(())
})
.await?
}
}
pub struct CacheWrite {
zip: ZipWriter<Cursor<Vec<u8>>>,
}
impl CacheWrite {
pub fn new() -> CacheWrite {
CacheWrite {
zip: ZipWriter::new(Cursor::new(vec![])),
}
}
pub async fn from_objects<T>(objects: T, pool: &tokio::runtime::Handle) -> Result<CacheWrite>
where
T: IntoIterator<Item = FileObjectSource> + Send + Sync + 'static,
{
pool.spawn_blocking(move || {
let mut entry = CacheWrite::new();
for FileObjectSource {
key,
path,
optional,
} in objects
{
let f = fs::File::open(&path)
.with_context(|| format!("failed to open file `{:?}`", path));
match (f, optional) {
(Ok(mut f), _) => {
let mode = get_file_mode(&f)?;
entry.put_object(&key, &mut f, mode).with_context(|| {
format!("failed to put object `{:?}` in cache entry", path)
})?;
}
(Err(e), false) => return Err(e),
(Err(_), true) => continue,
}
}
Ok(entry)
})
.await?
}
pub fn put_object<T>(&mut self, name: &str, from: &mut T, mode: Option<u32>) -> Result<()>
where
T: Read,
{
let opts = FileOptions::default().compression_method(CompressionMethod::Stored);
let opts = if let Some(mode) = mode {
opts.unix_permissions(mode)
} else {
opts
};
self.zip
.start_file(name, opts)
.context("Failed to start cache entry object")?;
let compression_level = std::env::var("SCCACHE_CACHE_ZSTD_LEVEL")
.ok()
.and_then(|value| value.parse::<i32>().ok())
.unwrap_or(3);
zstd::stream::copy_encode(from, &mut self.zip, compression_level)?;
Ok(())
}
pub fn put_stdout(&mut self, bytes: &[u8]) -> Result<()> {
self.put_bytes("stdout", bytes)
}
pub fn put_stderr(&mut self, bytes: &[u8]) -> Result<()> {
self.put_bytes("stderr", bytes)
}
fn put_bytes(&mut self, name: &str, bytes: &[u8]) -> Result<()> {
if !bytes.is_empty() {
let mut cursor = Cursor::new(bytes);
return self.put_object(name, &mut cursor, None);
}
Ok(())
}
pub fn finish(self) -> Result<Vec<u8>> {
let CacheWrite { mut zip } = self;
let cur = zip.finish().context("Failed to finish cache entry zip")?;
Ok(cur.into_inner())
}
}
impl Default for CacheWrite {
fn default() -> Self {
Self::new()
}
}