use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::config::{Result, TestbedError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Compression {
None,
#[default]
Gzip,
Xz,
}
impl Compression {
pub fn extension(&self) -> &'static str {
match self {
Compression::None => "",
Compression::Gzip => ".gz",
Compression::Xz => ".xz",
}
}
}
pub fn compress_qcow2_with(src: &Path, dest: &Path, algo: Compression) -> Result<()> {
match algo {
Compression::None => {
std::fs::copy(src, dest).map_err(|e| TestbedError::Qcow2Error {
message: format!("copying qcow2: {e}"),
})?;
}
Compression::Gzip => compress_gzip(src, dest)?,
Compression::Xz => compress_xz(src, dest)?,
}
Ok(())
}
pub fn decompress(src: &Path, dest: &Path) -> Result<()> {
let ext = src.extension().and_then(|e| e.to_str()).unwrap_or("");
match ext {
"gz" => decompress_gzip(src, dest),
"xz" => decompress_xz(src, dest),
_ => {
std::fs::copy(src, dest).map_err(|e| TestbedError::Qcow2Error {
message: format!("copying qcow2: {e}"),
})?;
Ok(())
}
}
}
pub fn is_compressed(path: &Path) -> bool {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
name.ends_with(".qcow2.gz") || name.ends_with(".qcow2.xz")
}
pub fn ensure_decompressed(image_path: &Path) -> Result<PathBuf> {
if !is_compressed(image_path) {
return Ok(image_path.to_path_buf());
}
let cache_dir = crate::config::image_cache_dir();
std::fs::create_dir_all(&cache_dir).map_err(|e| TestbedError::Qcow2Error {
message: format!("creating image cache dir: {e}"),
})?;
let qcow2_name = image_path
.file_name()
.and_then(|n| n.to_str())
.map(|n| {
n.trim_end_matches(".gz")
.trim_end_matches(".xz")
.to_string()
})
.unwrap_or_else(|| "image.qcow2".to_string());
let dest = cache_dir.join(&qcow2_name);
if dest.exists() {
let meta = std::fs::metadata(&dest).map_err(|e| TestbedError::Qcow2Error {
message: format!("stat decompressed image: {e}"),
})?;
if meta.len() > 0 {
return Ok(dest);
}
}
let temp_dest = dest.with_extension("decompressing");
decompress(image_path, &temp_dest)?;
std::fs::rename(&temp_dest, &dest).map_err(|e| TestbedError::Qcow2Error {
message: format!("moving decompressed image: {e}"),
})?;
Ok(dest)
}
pub fn compress_gzip(src: &Path, dest: &Path) -> Result<()> {
let src_file = File::open(src).map_err(|e| TestbedError::Qcow2Error {
message: format!("opening source: {e}"),
})?;
let dest_file = File::create(dest).map_err(|e| TestbedError::Qcow2Error {
message: format!("creating destination: {e}"),
})?;
let mut reader = BufReader::new(src_file);
let mut encoder = flate2::write::GzEncoder::new(
BufWriter::new(dest_file),
flate2::Compression::best(),
);
let mut buf = [0u8; 65536];
loop {
let n = reader.read(&mut buf).map_err(|e| TestbedError::Qcow2Error {
message: format!("reading source: {e}"),
})?;
if n == 0 { break; }
encoder.write_all(&buf[..n]).map_err(|e| TestbedError::Qcow2Error {
message: format!("writing compressed data: {e}"),
})?;
}
encoder.finish().map_err(|e| TestbedError::Qcow2Error {
message: format!("finalizing gzip: {e}"),
})?;
Ok(())
}
pub fn decompress_gzip(src: &Path, dest: &Path) -> Result<()> {
let src_file = File::open(src).map_err(|e| TestbedError::Qcow2Error {
message: format!("opening source: {e}"),
})?;
let dest_file = File::create(dest).map_err(|e| TestbedError::Qcow2Error {
message: format!("creating destination: {e}"),
})?;
let mut decoder = flate2::read::GzDecoder::new(BufReader::new(src_file));
let mut writer = BufWriter::new(dest_file);
let mut buf = [0u8; 65536];
loop {
let n = decoder.read(&mut buf).map_err(|e| TestbedError::Qcow2Error {
message: format!("reading compressed data: {e}"),
})?;
if n == 0 { break; }
writer.write_all(&buf[..n]).map_err(|e| TestbedError::Qcow2Error {
message: format!("writing decompressed data: {e}"),
})?;
}
writer.flush().map_err(|e| TestbedError::Qcow2Error {
message: format!("flushing output: {e}"),
})?;
Ok(())
}
pub fn compress_xz(src: &Path, dest: &Path) -> Result<()> {
let src_file = File::open(src).map_err(|e| TestbedError::Qcow2Error {
message: format!("opening source: {e}"),
})?;
let dest_file = File::create(dest).map_err(|e| TestbedError::Qcow2Error {
message: format!("creating destination: {e}"),
})?;
let mut reader = BufReader::new(src_file);
let mut encoder = xz2::write::XzEncoder::new(
BufWriter::new(dest_file),
9,
);
let mut buf = [0u8; 65536];
loop {
let n = reader.read(&mut buf).map_err(|e| TestbedError::Qcow2Error {
message: format!("reading source: {e}"),
})?;
if n == 0 { break; }
encoder.write_all(&buf[..n]).map_err(|e| TestbedError::Qcow2Error {
message: format!("writing compressed data: {e}"),
})?;
}
encoder.finish().map_err(|e| TestbedError::Qcow2Error {
message: format!("finalizing xz: {e}"),
})?;
Ok(())
}
pub fn decompress_xz(src: &Path, dest: &Path) -> Result<()> {
let src_file = File::open(src).map_err(|e| TestbedError::Qcow2Error {
message: format!("opening source: {e}"),
})?;
let dest_file = File::create(dest).map_err(|e| TestbedError::Qcow2Error {
message: format!("creating destination: {e}"),
})?;
let mut decoder = xz2::read::XzDecoder::new(BufReader::new(src_file));
let mut writer = BufWriter::new(dest_file);
let mut buf = [0u8; 65536];
loop {
let n = decoder.read(&mut buf).map_err(|e| TestbedError::Qcow2Error {
message: format!("reading compressed data: {e}"),
})?;
if n == 0 { break; }
writer.write_all(&buf[..n]).map_err(|e| TestbedError::Qcow2Error {
message: format!("writing decompressed data: {e}"),
})?;
}
writer.flush().map_err(|e| TestbedError::Qcow2Error {
message: format!("flushing output: {e}"),
})?;
Ok(())
}
#[allow(dead_code)]
pub fn compress_system(src: &Path, dest: &Path, algo: Compression) -> Result<()> {
let (cmd, args) = match algo {
Compression::Xz => ("xz", vec!["-9", "-c", src.to_str().unwrap()]),
Compression::Gzip => ("gzip", vec!["-9", "-c", src.to_str().unwrap()]),
Compression::None => {
std::fs::copy(src, dest).map_err(|e| TestbedError::Qcow2Error {
message: format!("copying qcow2: {e}"),
})?;
return Ok(());
}
};
let out_file = File::create(dest).map_err(|e| TestbedError::Qcow2Error {
message: format!("creating destination: {e}"),
})?;
let status = Command::new(cmd)
.args(&args)
.stdout(out_file)
.status()
.map_err(|e| TestbedError::Qcow2Error {
message: format!("spawning {cmd}: {e}"),
})?;
if !status.success() {
return Err(TestbedError::Qcow2Error {
message: format!("{cmd} compression failed (exit {status})"),
});
}
Ok(())
}