extern crate tar;
use anyhow::bail;
use clap::Args;
use std::fs::File;
use std::io::{self, Seek, SeekFrom, Write};
use tar::{Archive, Builder};
use tempfile::tempfile;
use crate::utils::*;
#[derive(Args, Debug)]
pub struct TarArgs {
#[clap(flatten)]
pub common_args: CommonArgs,
}
#[derive(Default)]
pub struct Tar {}
impl Tar {
pub fn new(_args: &TarArgs) -> Tar {
Tar {}
}
}
impl Compressor for Tar {
fn name(&self) -> &str {
"tar"
}
fn default_extracted_target(&self) -> ExtractedTarget {
ExtractedTarget::DIRECTORY
}
fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result {
match output {
CmprssOutput::Path(path) => {
let file = File::create(path)?;
self.compress_internal(input, Builder::new(file))
}
CmprssOutput::Pipe(mut pipe) => {
let mut temp_file = tempfile()?;
self.compress_internal(input, Builder::new(&mut temp_file))?;
temp_file.seek(SeekFrom::Start(0))?;
io::copy(&mut temp_file, &mut pipe)?;
Ok(())
}
CmprssOutput::Writer(mut writer) => {
let mut temp_file = tempfile()?;
self.compress_internal(input, Builder::new(&mut temp_file))?;
temp_file.seek(SeekFrom::Start(0))?;
io::copy(&mut temp_file, &mut writer)?;
Ok(())
}
}
}
fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result {
match output {
CmprssOutput::Path(ref out_dir) => {
if !out_dir.exists() {
std::fs::create_dir_all(out_dir)?;
} else if !out_dir.is_dir() {
bail!("tar extraction output must be a directory");
}
match input {
CmprssInput::Path(paths) => {
if paths.len() != 1 {
bail!("tar extraction expects a single archive file");
}
let file = File::open(&paths[0])?;
let mut archive = Archive::new(file);
Ok(archive.unpack(out_dir)?)
}
CmprssInput::Pipe(mut pipe) => {
let mut temp_file = tempfile()?;
io::copy(&mut pipe, &mut temp_file)?;
temp_file.seek(SeekFrom::Start(0))?;
let mut archive = Archive::new(temp_file);
Ok(archive.unpack(out_dir)?)
}
CmprssInput::Reader(reader) => {
let mut archive = Archive::new(reader.0);
archive.unpack(out_dir)?;
Ok(())
}
}
}
CmprssOutput::Pipe(_) => bail!("tar extraction to stdout is not supported"),
CmprssOutput::Writer(mut writer) => match input {
CmprssInput::Path(paths) => {
if paths.len() != 1 {
bail!("tar extraction expects a single archive file");
}
let mut file = File::open(&paths[0])?;
io::copy(&mut file, &mut writer)?;
Ok(())
}
CmprssInput::Pipe(mut pipe) => {
io::copy(&mut pipe, &mut writer)?;
Ok(())
}
CmprssInput::Reader(mut reader) => {
io::copy(&mut reader, &mut writer)?;
Ok(())
}
},
}
}
}
impl Tar {
fn compress_internal<W: Write>(&self, input: CmprssInput, mut archive: Builder<W>) -> Result {
match input {
CmprssInput::Path(paths) => {
for path in paths {
if path.is_file() {
archive.append_file(
path.file_name().unwrap(),
&mut File::open(path.as_path())?,
)?;
} else if path.is_dir() {
archive.append_dir_all(path.file_name().unwrap(), path.as_path())?;
} else {
bail!("unsupported file type for tar compression");
}
}
}
CmprssInput::Pipe(mut pipe) => {
let mut temp_file = tempfile()?;
io::copy(&mut pipe, &mut temp_file)?;
temp_file.seek(SeekFrom::Start(0))?;
archive.append_file("archive", &mut temp_file)?;
}
CmprssInput::Reader(_) => {
bail!("Cannot tar a reader input directly");
}
}
Ok(archive.finish()?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use assert_fs::prelude::*;
use predicates::prelude::*;
use std::path::PathBuf;
#[test]
fn test_tar_interface() {
let compressor = Tar::default();
test_compressor_interface(&compressor, "tar", Some("tar"));
}
#[test]
fn test_tar_default_compression() -> Result {
let compressor = Tar::default();
test_compression(&compressor)
}
#[test]
fn test_directory_handling() -> Result {
let compressor = Tar::default();
let dir = assert_fs::TempDir::new()?;
let file_path = dir.child("file.txt");
file_path.write_str("garbage data for testing")?;
let working_dir = assert_fs::TempDir::new()?;
let archive = working_dir.child("dir_archive.tar");
archive.assert(predicate::path::missing());
compressor.compress(
CmprssInput::Path(vec![dir.path().to_path_buf()]),
CmprssOutput::Path(archive.path().to_path_buf()),
)?;
archive.assert(predicate::path::is_file());
let extract_dir = working_dir.child("extracted");
std::fs::create_dir_all(extract_dir.path())?;
compressor.extract(
CmprssInput::Path(vec![archive.path().to_path_buf()]),
CmprssOutput::Path(extract_dir.path().to_path_buf()),
)?;
let dir_name: PathBuf = dir.path().file_name().unwrap().into();
extract_dir
.child(dir_name)
.child("file.txt")
.assert(predicate::path::eq_file(file_path.path()));
Ok(())
}
}