use super::FormatHandler;
use crate::{progress::ProgressContext, Error, Result};
use std::io::Read;
use std::path::{Path, PathBuf};
pub struct TarHandler;
impl TarHandler {
pub fn new() -> Self {
Self
}
fn detect_compression(&self, file_path: &Path) -> CompressionType {
if let Some(filename) = file_path.file_name().and_then(|n| n.to_str()) {
if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") {
CompressionType::Gzip
} else if filename.ends_with(".tar.xz") || filename.ends_with(".txz") {
CompressionType::Xz
} else if filename.ends_with(".tar.bz2") || filename.ends_with(".tbz2") {
CompressionType::Bzip2
} else if filename.ends_with(".tar") {
CompressionType::None
} else {
CompressionType::Unknown
}
} else {
CompressionType::Unknown
}
}
}
#[derive(Debug, Clone, Copy)]
enum CompressionType {
None,
Gzip,
Xz,
Bzip2,
Unknown,
}
#[async_trait::async_trait]
impl FormatHandler for TarHandler {
fn name(&self) -> &str {
"tar"
}
fn can_handle(&self, file_path: &Path) -> bool {
if let Some(filename) = file_path.file_name().and_then(|n| n.to_str()) {
filename.ends_with(".tar")
|| filename.ends_with(".tar.gz")
|| filename.ends_with(".tgz")
|| filename.ends_with(".tar.xz")
|| filename.ends_with(".txz")
|| filename.ends_with(".tar.bz2")
|| filename.ends_with(".tbz2")
} else {
false
}
}
async fn extract(
&self,
source_path: &Path,
target_dir: &Path,
progress: &ProgressContext,
) -> Result<Vec<PathBuf>> {
std::fs::create_dir_all(target_dir)?;
let compression = self.detect_compression(source_path);
progress.start("Extracting TAR archive", None).await?;
let file = std::fs::File::open(source_path)?;
let mut extracted_files = Vec::new();
match compression {
CompressionType::None => {
self.extract_tar(file, target_dir, &mut extracted_files)
.await?;
}
CompressionType::Gzip => {
let decoder = flate2::read::GzDecoder::new(file);
self.extract_tar(decoder, target_dir, &mut extracted_files)
.await?;
}
CompressionType::Xz => {
return Err(Error::unsupported_format("tar.xz"));
}
CompressionType::Bzip2 => {
return Err(Error::unsupported_format("tar.bz2"));
}
CompressionType::Unknown => {
return Err(Error::unsupported_format("unknown tar format"));
}
}
progress.finish("TAR extraction completed").await?;
Ok(extracted_files)
}
}
impl TarHandler {
async fn extract_tar<R: Read>(
&self,
reader: R,
target_dir: &Path,
extracted_files: &mut Vec<PathBuf>,
) -> Result<()> {
let mut archive = tar::Archive::new(reader);
for entry in archive.entries()? {
let mut entry = entry?;
let path = entry.path()?;
let target_path = target_dir.join(&path);
if let Some(parent) = target_path.parent() {
std::fs::create_dir_all(parent)?;
}
if entry.header().entry_type().is_dir() {
std::fs::create_dir_all(&target_path)?;
} else {
entry.unpack(&target_path)?;
#[cfg(unix)]
{
let mode = entry.header().mode()?;
if mode & 0o111 != 0 {
self.make_executable(&target_path)?;
}
}
extracted_files.push(target_path);
}
}
Ok(())
}
}
impl Default for TarHandler {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_tar_handler_can_handle() {
let handler = TarHandler::new();
assert!(handler.can_handle(Path::new("test.tar")));
assert!(handler.can_handle(Path::new("test.tar.gz")));
assert!(handler.can_handle(Path::new("test.tgz")));
assert!(handler.can_handle(Path::new("test.tar.xz")));
assert!(handler.can_handle(Path::new("test.tar.bz2")));
assert!(!handler.can_handle(Path::new("test.zip")));
assert!(!handler.can_handle(Path::new("test.exe")));
}
#[tokio::test]
async fn test_tar_handler_name() {
let handler = TarHandler::new();
assert_eq!(handler.name(), "tar");
}
#[test]
fn test_compression_detection() {
let handler = TarHandler::new();
assert!(matches!(
handler.detect_compression(Path::new("test.tar")),
CompressionType::None
));
assert!(matches!(
handler.detect_compression(Path::new("test.tar.gz")),
CompressionType::Gzip
));
assert!(matches!(
handler.detect_compression(Path::new("test.tgz")),
CompressionType::Gzip
));
assert!(matches!(
handler.detect_compression(Path::new("test.tar.xz")),
CompressionType::Xz
));
}
}