From 6869b94ef2c76f5cac9728bbcce9514b5ea45665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Misty=20De=20M=C3=A9o?= Date: Tue, 5 Mar 2024 15:53:09 +1100 Subject: [PATCH] feat: add zip/tarball decompression --- src/compression.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++- src/error.rs | 18 ++++++++ src/local.rs | 81 ++++++++++++++++++++++++++++++++ 3 files changed, 210 insertions(+), 1 deletion(-) diff --git a/src/compression.rs b/src/compression.rs index 70e1dea..f15642a 100644 --- a/src/compression.rs +++ b/src/compression.rs @@ -120,7 +120,7 @@ pub(crate) fn tar_dir( // Wrap our file in compression let zip_output = ZstdEncoder::new(final_zip_file, 0).map_err(|details| { AxoassetError::LocalAssetArchive { - reason: format!("failed to create zstd encoder"), + reason: "failed to create zstd encoder".to_string(), details, } })?; @@ -162,6 +162,80 @@ pub(crate) fn tar_dir( Ok(()) } +#[cfg(feature = "compression-tar")] +fn open_tarball( + tarball: &Utf8Path, + compression: &CompressionImpl, +) -> crate::error::Result> { + use std::io::Read; + + use flate2::read::GzDecoder; + use xz2::read::XzDecoder; + use zstd::stream::Decoder as ZstdDecoder; + + use crate::LocalAsset; + + let source = LocalAsset::load_bytes(tarball)?; + let mut tarball_bytes = vec![]; + + match compression { + CompressionImpl::Gzip => { + let mut decoder = GzDecoder::new(source.as_slice()); + decoder.read_to_end(&mut tarball_bytes)?; + } + CompressionImpl::Xzip => { + let mut decoder = XzDecoder::new(source.as_slice()); + decoder.read_to_end(&mut tarball_bytes)?; + } + CompressionImpl::Zstd => { + let mut decoder = ZstdDecoder::new(source.as_slice())?; + decoder.read_to_end(&mut tarball_bytes)?; + } + }; + + Ok(tarball_bytes) +} + +#[cfg(feature = "compression-tar")] +pub(crate) fn untar_all( + tarball: &Utf8Path, + dest_path: &Utf8Path, + compression: &CompressionImpl, +) -> crate::error::Result<()> { + let tarball_bytes = open_tarball(tarball, compression)?; + let mut tarball = tar::Archive::new(tarball_bytes.as_slice()); + tarball.unpack(dest_path)?; + + Ok(()) +} + +#[cfg(feature = "compression-tar")] +pub(crate) fn untar_file( + tarball: &Utf8Path, + filename: &str, + compression: &CompressionImpl, +) -> crate::error::Result> { + use std::io::Read; + + let tarball_bytes = open_tarball(tarball, compression)?; + let mut tarball = tar::Archive::new(tarball_bytes.as_slice()); + for entry in tarball.entries()? { + let mut entry = entry?; + if let Some(name) = entry.path()?.file_name() { + if name == filename { + let mut buf = vec![]; + entry.read_to_end(&mut buf)?; + + return Ok(buf); + } + } + } + + Err(crate::AxoassetError::ExtractFilenameFailed { + desired_filename: filename.to_owned(), + }) +} + #[cfg(feature = "compression-zip")] pub(crate) fn zip_dir( src_path: &Utf8Path, @@ -223,3 +297,39 @@ pub(crate) fn zip_dir( zip.finish()?; Ok(()) } + +#[cfg(feature = "compression-zip")] +pub(crate) fn unzip_all(zipfile: &Utf8Path, dest_path: &Utf8Path) -> crate::error::Result<()> { + use std::io::Cursor; + + use crate::LocalAsset; + + let source = LocalAsset::load_bytes(zipfile)?; + let seekable = Cursor::new(source); + let mut archive = zip::ZipArchive::new(seekable)?; + archive.extract(&dest_path)?; + + Ok(()) +} + +#[cfg(feature = "compression-zip")] +pub(crate) fn unzip_file(zipfile: &Utf8Path, filename: &str) -> crate::error::Result> { + use std::io::{Cursor, Read}; + + use crate::LocalAsset; + + let source = LocalAsset::load_bytes(zipfile)?; + let seekable = Cursor::new(source); + let mut archive = zip::ZipArchive::new(seekable)?; + let mut file = + archive + .by_name(&filename) + .map_err(|_| crate::AxoassetError::ExtractFilenameFailed { + desired_filename: filename.to_owned(), + })?; + + let mut buf = vec![]; + file.read_to_end(&mut buf)?; + + Ok(buf) +} diff --git a/src/error.rs b/src/error.rs index c3a3253..eea982d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -32,6 +32,17 @@ pub enum AxoassetError { #[error(transparent)] MimeParseParse(#[from] mime::FromStrError), + /// This error is a transparent error forwarded from the flate2 library. + /// This error indicates that an error of some kind occurred while performing io. + #[error(transparent)] + Io(#[from] std::io::Error), + + /// This error is a transparent error forwarded from the flate2 library. + /// This error indicates that an error of some kind occurred while opening a ZIP file. + #[error(transparent)] + #[cfg(feature = "compression-zip")] + Zip(#[from] zip::result::ZipError), + /// This error indicates that axoasset was asked to create a new remote /// asset, likely by being given an path that starts with http or https. /// Axoasset can only create new assets on the file system. @@ -291,6 +302,13 @@ pub enum AxoassetError { desired_filename: String, }, + #[error("Failed to find {desired_filename} within archive being decompressed")] + /// This error indicates we failed to find the desired file within a tarball or zip + ExtractFilenameFailed { + /// The filename we were searching for + desired_filename: String, + }, + #[error("Failed to walk to ancestor of {origin_path}")] /// Walkdir failed to yield an entry WalkDirFailed { diff --git a/src/local.rs b/src/local.rs index 6143a81..4698eb6 100644 --- a/src/local.rs +++ b/src/local.rs @@ -381,6 +381,26 @@ impl LocalAsset { ) } + /// Extracts the entire tarball at `tarball` to a provided directory + #[cfg(any(feature = "compression", feature = "compression-tar"))] + pub fn untar_gz_all(tarball: &Utf8Path, dest_path: &Utf8Path) -> Result<()> { + crate::compression::untar_all( + tarball, + dest_path, + &crate::compression::CompressionImpl::Gzip, + ) + } + + /// Extracts the file named `filename` within the tarball at `tarball` and returns its contents as bytes + #[cfg(any(feature = "compression", feature = "compression-tar"))] + pub fn untar_gz_file(tarball: &Utf8Path, filename: &str) -> Result> { + crate::compression::untar_file( + tarball, + filename, + &crate::compression::CompressionImpl::Gzip, + ) + } + /// Creates a new .tar.xz file from a provided directory /// /// The with_root argument specifies that all contents of dest_dir should be placed @@ -401,6 +421,29 @@ impl LocalAsset { ) } + /// Extracts the entire tarball at `tarball` to a provided directory + #[cfg(any(feature = "compression", feature = "compression-tar"))] + pub fn untar_xz_all( + tarball: impl AsRef, + dest_path: impl AsRef, + ) -> Result<()> { + crate::compression::untar_all( + Utf8Path::new(tarball.as_ref()), + Utf8Path::new(dest_path.as_ref()), + &crate::compression::CompressionImpl::Xzip, + ) + } + + /// Extracts the file named `filename` within the tarball at `tarball` and returns its contents as bytes + #[cfg(any(feature = "compression", feature = "compression-tar"))] + pub fn untar_xz_file(tarball: impl AsRef, filename: &str) -> Result> { + crate::compression::untar_file( + Utf8Path::new(tarball.as_ref()), + filename, + &crate::compression::CompressionImpl::Xzip, + ) + } + /// Creates a new .tar.zstd file from a provided directory /// /// The with_root argument specifies that all contents of dest_dir should be placed @@ -421,6 +464,29 @@ impl LocalAsset { ) } + /// Extracts the entire tarball at `tarball` to a provided directory + #[cfg(any(feature = "compression", feature = "compression-tar"))] + pub fn untar_zstd_all( + tarball: impl AsRef, + dest_path: impl AsRef, + ) -> Result<()> { + crate::compression::untar_all( + Utf8Path::new(tarball.as_ref()), + Utf8Path::new(dest_path.as_ref()), + &crate::compression::CompressionImpl::Zstd, + ) + } + + /// Extracts the file named `filename` within the tarball at `tarball` and returns its contents as bytes + #[cfg(any(feature = "compression", feature = "compression-tar"))] + pub fn untar_zstd_file(tarball: impl AsRef, filename: &str) -> Result> { + crate::compression::untar_file( + Utf8Path::new(tarball.as_ref()), + filename, + &crate::compression::CompressionImpl::Zstd, + ) + } + /// Creates a new .zip file from a provided directory /// /// The with_root argument specifies that all contents of dest_dir should be placed @@ -443,4 +509,19 @@ impl LocalAsset { details: e.into(), }) } + + /// Extracts a .zip file to the a provided directory + #[cfg(any(feature = "compression", feature = "compression-zip"))] + pub fn unzip_all(zipfile: impl AsRef, dest_dir: impl AsRef) -> Result<()> { + crate::compression::unzip_all( + Utf8Path::new(zipfile.as_ref()), + Utf8Path::new(dest_dir.as_ref()), + ) + } + + /// Extracts the file named `filename` within the ZIP file at `zipfile` and returns its contents as bytes + #[cfg(any(feature = "compression", feature = "compression-zip"))] + pub fn unzip_file(zipfile: impl AsRef, filename: &str) -> Result> { + crate::compression::unzip_file(Utf8Path::new(zipfile.as_ref()), filename) + } }