use actix_web::http::ContentEncoding; use libflate::gzip::Encoder; use serde::Deserialize; use std::path::Path; use strum_macros::{Display, EnumIter, EnumString}; use tar::Builder; use crate::errors::ContextualError; /// Available compression methods #[derive(Deserialize, Clone, Copy, EnumIter, EnumString, Display)] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum CompressionMethod { /// Gzipped tarball TarGz, /// Regular tarball Tar, } impl CompressionMethod { pub fn extension(self) -> String { match self { CompressionMethod::TarGz => "tar.gz", CompressionMethod::Tar => "tar", } .to_string() } pub fn content_type(self) -> String { match self { CompressionMethod::TarGz => "application/gzip", CompressionMethod::Tar => "application/tar", } .to_string() } pub fn content_encoding(self) -> ContentEncoding { match self { CompressionMethod::TarGz => ContentEncoding::Gzip, CompressionMethod::Tar => ContentEncoding::Identity, } } /// Make an archive out of the given directory, and write the output to the given writer. /// /// Recursively includes all files and subdirectories. /// /// If `skip_symlinks` is `true`, symlinks fill not be followed and will just be ignored. pub fn create_archive( self, dir: T, skip_symlinks: bool, out: W, ) -> Result<(), ContextualError> where T: AsRef, W: std::io::Write, { let dir = dir.as_ref(); match self { CompressionMethod::TarGz => tar_gz(dir, skip_symlinks, out), CompressionMethod::Tar => tar_dir(dir, skip_symlinks, out), } } } /// Write a gzipped tarball of `dir` in `out`. fn tar_gz(dir: &Path, skip_symlinks: bool, out: W) -> Result<(), ContextualError> where W: std::io::Write, { let mut out = Encoder::new(out).map_err(|e| ContextualError::IOError("GZIP".to_string(), e))?; tar_dir(dir, skip_symlinks, &mut out)?; out.finish() .into_result() .map_err(|e| ContextualError::IOError("GZIP finish".to_string(), e))?; Ok(()) } /// Write a tarball of `dir` in `out`. /// /// The target directory will be saved as a top-level directory in the archive. /// /// For example, consider this directory structure: /// /// ``` /// a /// └── b /// └── c /// ├── e /// ├── f /// └── g /// ``` /// /// Making a tarball out of `"a/b/c"` will result in this archive content: /// /// ``` /// c /// ├── e /// ├── f /// └── g /// ``` fn tar_dir(dir: &Path, skip_symlinks: bool, out: W) -> Result<(), ContextualError> where W: std::io::Write, { let inner_folder = dir.file_name().ok_or_else(|| { ContextualError::InvalidPathError("Directory name terminates in \"..\"".to_string()) })?; let directory = inner_folder.to_str().ok_or_else(|| { ContextualError::InvalidPathError( "Directory name contains invalid UTF-8 characters".to_string(), ) })?; tar(dir, directory.to_string(), skip_symlinks, out) .map_err(|e| ContextualError::ArchiveCreationError("tarball".to_string(), Box::new(e))) } /// Writes a tarball of `dir` in `out`. /// /// The content of `src_dir` will be saved in the archive as a folder named `inner_folder`. fn tar( src_dir: &Path, inner_folder: String, skip_symlinks: bool, out: W, ) -> Result<(), ContextualError> where W: std::io::Write, { let mut tar_builder = Builder::new(out); tar_builder.follow_symlinks(!skip_symlinks); // Recursively adds the content of src_dir into the archive stream tar_builder .append_dir_all(inner_folder, src_dir) .map_err(|e| { ContextualError::IOError( format!( "Failed to append the content of {} to the TAR archive", src_dir.to_str().unwrap_or("file") ), e, ) })?; // Finish the archive tar_builder.into_inner().map_err(|e| { ContextualError::IOError("Failed to finish writing the TAR archive".to_string(), e) })?; Ok(()) }