You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
104 lines
3.7 KiB
Rust
104 lines
3.7 KiB
Rust
use crate::format::gzip::GZipFormat;
|
|
use crate::format::xz::XZFormat;
|
|
use crate::format::{FileFormat, FileObject};
|
|
use crate::utils::xz_decoder::XzDecoder;
|
|
use anyhow::{bail, Context};
|
|
use libflate::gzip;
|
|
use std::fs::File;
|
|
use std::io::{BufReader, BufWriter, Read, Write};
|
|
use std::path::Path;
|
|
use std::{fs, io};
|
|
use tar::{Archive, EntryType};
|
|
|
|
const TAR_HEADER: &[u8] = &[0x75, 0x73, 0x74, 0x61, 0x72];
|
|
|
|
pub enum TarFormat {
|
|
Xz,
|
|
Gz,
|
|
Uncompressed,
|
|
}
|
|
|
|
impl FileFormat for TarFormat {
|
|
fn parse(file: &FileObject) -> anyhow::Result<Self> {
|
|
if file.header.starts_with(TAR_HEADER) {
|
|
tracing::info!("Detected uncompressed tar file");
|
|
|
|
Ok(Self::Uncompressed)
|
|
} else if file.ext.contains(".tar.") {
|
|
if let Ok(_xz) = XZFormat::parse(file) {
|
|
tracing::info!("Detected tar file compressed with xz");
|
|
|
|
Ok(Self::Xz)
|
|
} else if let Ok(_gz) = GZipFormat::parse(file) {
|
|
tracing::info!("Detected tarfile compressed with gz");
|
|
|
|
Ok(Self::Gz)
|
|
} else {
|
|
bail!("Not a tar file or a tar with unknown compression");
|
|
}
|
|
} else if file.ext.ends_with(".tar") {
|
|
tracing::info!("Assuming tar based on the file extension");
|
|
Ok(Self::Uncompressed)
|
|
} else {
|
|
bail!("Not a tar file");
|
|
}
|
|
}
|
|
|
|
fn extract(&self, file: &Path, output: &Path) -> anyhow::Result<()> {
|
|
let mut reader = BufReader::new(File::open(file).context("Opening input")?);
|
|
match self {
|
|
TarFormat::Xz => {
|
|
let mut decoder = XzDecoder::new(reader);
|
|
extract_tar(&mut decoder, output)
|
|
}
|
|
TarFormat::Gz => {
|
|
let mut decoder = gzip::Decoder::new(&mut reader).context("Creating decoder")?;
|
|
extract_tar(&mut decoder, output)
|
|
}
|
|
TarFormat::Uncompressed => extract_tar(&mut reader, output).context("Extract tar"),
|
|
}
|
|
}
|
|
}
|
|
/// Extracts a tar file to the given output directory
|
|
fn extract_tar<R: Read>(reader: &mut R, output: &Path) -> anyhow::Result<()> {
|
|
if output.is_file() {
|
|
bail!("The output must be a directory.");
|
|
}
|
|
let mut archive = Archive::new(reader);
|
|
|
|
for file in archive.entries().context("Reading tar entries")? {
|
|
let mut file = file.context("Retrieving tar file entry")?;
|
|
let header = file.header();
|
|
let file_path = header.path().context("Retrieving path of file")?;
|
|
let output_path = output.join(file_path);
|
|
|
|
match header.entry_type() {
|
|
EntryType::Regular => {
|
|
if let Some(parent) = output_path.parent() {
|
|
if !parent.exists() {
|
|
tracing::debug!("Creating parent path {parent:?}");
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
}
|
|
tracing::debug!("Decompressing entry to {output_path:?}");
|
|
let mut output_file = BufWriter::new(
|
|
File::create(&output_path)
|
|
.with_context(|| format!("Create output file {output_path:?}"))?,
|
|
);
|
|
io::copy(&mut file, &mut output_file).context("writing tar entry to output")?;
|
|
output_file.flush()?;
|
|
}
|
|
EntryType::Directory => {
|
|
tracing::debug!("Creating output directory {output_path:?}");
|
|
fs::create_dir_all(&output_path)
|
|
.with_context(|| format!("Failed to create output directory {output_path:?}"))?
|
|
}
|
|
other => {
|
|
tracing::debug!("Ignoring entry of type {other:?}");
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|