mirror of
https://github.com/ouch-org/ouch.git
synced 2025-06-06 11:35:45 +00:00
Reworked compression
Now works with multiple formats, like archive.zip.gz.lz.gz.gz2.xz Now with minimum in-memory copying, compressing and decompressing (with exception to .zip, due to the format limitations) is all done directly into the final destination file.
This commit is contained in:
parent
b969bda5a8
commit
bb93e46535
@ -1,25 +1,20 @@
|
||||
use std::{
|
||||
io::Read,
|
||||
env, fs,
|
||||
io::prelude::*,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use tar;
|
||||
use utils::colors;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::{oof, utils};
|
||||
|
||||
pub fn unpack_archive(
|
||||
reader: Box<dyn Read>,
|
||||
output_folder: &Path,
|
||||
flags: &oof::Flags,
|
||||
) -> crate::Result<Vec<PathBuf>> {
|
||||
// TODO: move this printing to the caller.
|
||||
// println!(
|
||||
// "{}[INFO]{} attempting to decompress {:?}",
|
||||
// colors::blue(),
|
||||
// colors::reset(),
|
||||
// &input_path
|
||||
// );
|
||||
|
||||
let mut archive = tar::Archive::new(reader);
|
||||
|
||||
let mut files_unpacked = vec![];
|
||||
@ -47,3 +42,31 @@ pub fn unpack_archive(
|
||||
|
||||
Ok(files_unpacked)
|
||||
}
|
||||
|
||||
pub fn build_archive_from_paths<W>(input_filenames: &[PathBuf], writer: W) -> crate::Result<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
let mut builder = tar::Builder::new(writer);
|
||||
|
||||
for filename in input_filenames {
|
||||
let previous_location = utils::cd_into_same_dir_as(filename)?;
|
||||
|
||||
// Safe unwrap, input shall be treated before
|
||||
let filename = filename.file_name().unwrap();
|
||||
|
||||
for entry in WalkDir::new(&filename) {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
println!("Compressing '{}'.", utils::to_utf(path));
|
||||
if !path.is_dir() {
|
||||
let mut file = fs::File::open(path)?;
|
||||
builder.append_file(path, &mut file)?;
|
||||
}
|
||||
}
|
||||
env::set_current_dir(previous_location)?;
|
||||
}
|
||||
|
||||
Ok(builder.into_inner()?)
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, Read, Seek},
|
||||
env, fs,
|
||||
io::{self, prelude::*},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use walkdir::WalkDir;
|
||||
use zip::{self, read::ZipFile, ZipArchive};
|
||||
|
||||
use crate::{
|
||||
@ -90,3 +91,54 @@ where
|
||||
|
||||
Ok(unpacked_files)
|
||||
}
|
||||
|
||||
pub fn build_archive_from_paths<W>(input_filenames: &[PathBuf], writer: W) -> crate::Result<W>
|
||||
where
|
||||
W: Write + Seek,
|
||||
{
|
||||
let mut writer = zip::ZipWriter::new(writer);
|
||||
let options = zip::write::FileOptions::default();
|
||||
|
||||
// Vec of any filename that failed the UTF-8 check
|
||||
let invalid_unicode_filenames: Vec<PathBuf> = input_filenames
|
||||
.iter()
|
||||
.map(|path| (path, path.to_str()))
|
||||
.filter(|(_, x)| x.is_none())
|
||||
.map(|(a, _)| a.to_path_buf())
|
||||
.collect();
|
||||
|
||||
if !invalid_unicode_filenames.is_empty() {
|
||||
panic!(
|
||||
"invalid unicode filenames found, cannot be supported by Zip:\n {:#?}",
|
||||
invalid_unicode_filenames
|
||||
);
|
||||
}
|
||||
|
||||
for filename in input_filenames {
|
||||
let previous_location = utils::cd_into_same_dir_as(filename)?;
|
||||
|
||||
// Safe unwrap, input shall be treated before
|
||||
let filename = filename.file_name().unwrap();
|
||||
|
||||
for entry in WalkDir::new(filename) {
|
||||
let entry = entry?;
|
||||
let path = &entry.path();
|
||||
|
||||
println!("Compressing '{}'.", utils::to_utf(path));
|
||||
if path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
writer.start_file(path.to_str().unwrap().to_owned(), options)?;
|
||||
// TODO: check if isn't there a function that already does this for us......
|
||||
// TODO: better error messages
|
||||
let file_bytes = fs::read(entry.path())?;
|
||||
writer.write_all(&*file_bytes)?;
|
||||
}
|
||||
|
||||
env::set_current_dir(previous_location)?;
|
||||
}
|
||||
|
||||
let bytes = writer.finish()?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ pub enum Command {
|
||||
/// Files to be compressed
|
||||
Compress {
|
||||
files: Vec<PathBuf>,
|
||||
compressed_output_path: PathBuf,
|
||||
output_path: PathBuf,
|
||||
},
|
||||
/// Files to be decompressed and their extensions
|
||||
Decompress {
|
||||
@ -103,9 +103,9 @@ pub fn parse_args_from(mut args: Vec<OsString>) -> crate::Result<ParsedArgs> {
|
||||
}
|
||||
|
||||
// Safety: we checked that args.len() >= 2
|
||||
let compressed_output_path = files.pop().unwrap();
|
||||
let output_path = files.pop().unwrap();
|
||||
|
||||
let command = Command::Compress { files, compressed_output_path };
|
||||
let command = Command::Compress { files, output_path };
|
||||
ParsedArgs { command, flags }
|
||||
},
|
||||
// Defaults to decompression when there is no subcommand
|
||||
@ -163,7 +163,7 @@ mod tests {
|
||||
});
|
||||
assert_eq!(test_cli("compress foo bar baz.zip").unwrap().command, Command::Compress {
|
||||
files: vec!["foo".into(), "bar".into()],
|
||||
compressed_output_path: "baz.zip".into()
|
||||
output_path: "baz.zip".into()
|
||||
});
|
||||
assert_eq!(test_cli("compress").unwrap_err(), crate::Error::MissingArgumentsForCompression);
|
||||
}
|
||||
|
146
src/commands.rs
146
src/commands.rs
@ -1,29 +1,31 @@
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, BufReader, Read},
|
||||
io::{self, BufReader, BufWriter, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use utils::colors;
|
||||
|
||||
use crate::{
|
||||
archive,
|
||||
cli::Command,
|
||||
compressors::{
|
||||
BzipCompressor, Compressor, Entry, GzipCompressor, LzmaCompressor, TarCompressor,
|
||||
ZipCompressor,
|
||||
},
|
||||
extension::{
|
||||
self,
|
||||
CompressionFormat::{self, *},
|
||||
},
|
||||
file, oof, utils,
|
||||
oof, utils,
|
||||
utils::to_utf,
|
||||
};
|
||||
|
||||
pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> {
|
||||
match command {
|
||||
Command::Compress { files, compressed_output_path } => {
|
||||
compress_files(files, &compressed_output_path, flags)?
|
||||
Command::Compress { files, output_path } => {
|
||||
let formats = extension::extensions_from_path(&output_path);
|
||||
let output_file = fs::File::create(&output_path)?;
|
||||
let compression_result = compress_files(files, formats, output_file, flags);
|
||||
if let Err(_err) = compression_result {
|
||||
fs::remove_file(&output_path).unwrap();
|
||||
}
|
||||
},
|
||||
Command::Decompress { files, output_folder } => {
|
||||
let mut output_paths = vec![];
|
||||
@ -67,81 +69,73 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
type BoxedCompressor = Box<dyn Compressor>;
|
||||
|
||||
fn get_compressor(file: &file::File) -> crate::Result<(Option<BoxedCompressor>, BoxedCompressor)> {
|
||||
let extension = match &file.extension {
|
||||
Some(extension) => extension,
|
||||
None => {
|
||||
// This is reached when the output file given does not have an extension or has an unsupported one
|
||||
return Err(crate::Error::MissingExtensionError(file.path.to_path_buf()));
|
||||
},
|
||||
};
|
||||
|
||||
// Supported first compressors:
|
||||
// .tar and .zip
|
||||
let first_compressor: Option<Box<dyn Compressor>> = match &extension.first_ext {
|
||||
Some(ext) => match ext {
|
||||
CompressionFormat::Tar => Some(Box::new(TarCompressor)),
|
||||
CompressionFormat::Zip => Some(Box::new(ZipCompressor)),
|
||||
CompressionFormat::Bzip => Some(Box::new(BzipCompressor)),
|
||||
CompressionFormat::Gzip => Some(Box::new(GzipCompressor)),
|
||||
CompressionFormat::Lzma => Some(Box::new(LzmaCompressor)),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
// Supported second compressors:
|
||||
// any
|
||||
let second_compressor: Box<dyn Compressor> = match extension.second_ext {
|
||||
CompressionFormat::Tar => Box::new(TarCompressor),
|
||||
CompressionFormat::Zip => Box::new(ZipCompressor),
|
||||
CompressionFormat::Bzip => Box::new(BzipCompressor),
|
||||
CompressionFormat::Gzip => Box::new(GzipCompressor),
|
||||
CompressionFormat::Lzma => Box::new(LzmaCompressor),
|
||||
};
|
||||
|
||||
Ok((first_compressor, second_compressor))
|
||||
}
|
||||
|
||||
fn compress_files(
|
||||
files: Vec<PathBuf>,
|
||||
output_path: &Path,
|
||||
flags: &oof::Flags,
|
||||
formats: Vec<CompressionFormat>,
|
||||
output_file: fs::File,
|
||||
_flags: &oof::Flags,
|
||||
) -> crate::Result<()> {
|
||||
let mut output = file::File::from(output_path)?;
|
||||
let file_writer = BufWriter::new(output_file);
|
||||
|
||||
let (first_compressor, second_compressor) = get_compressor(&output)?;
|
||||
if formats.len() == 1 {
|
||||
let build_archive_from_paths = match formats[0] {
|
||||
Tar => archive::tar::build_archive_from_paths,
|
||||
Zip => archive::zip::build_archive_from_paths,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if output_path.exists() && !utils::permission_for_overwriting(output_path, flags)? {
|
||||
// The user does not want to overwrite the file
|
||||
return Ok(());
|
||||
let mut bufwriter = build_archive_from_paths(&files, file_writer)?;
|
||||
bufwriter.flush()?;
|
||||
} else {
|
||||
let mut writer: Box<dyn Write> = Box::new(file_writer);
|
||||
|
||||
// Grab previous encoder and wrap it inside of a new one
|
||||
let chain_writer_encoder = |format: &CompressionFormat, encoder: Box<dyn Write>| {
|
||||
let encoder: Box<dyn Write> = match format {
|
||||
Gzip => Box::new(flate2::write::GzEncoder::new(encoder, Default::default())),
|
||||
Bzip => Box::new(bzip2::write::BzEncoder::new(encoder, Default::default())),
|
||||
Lzma => Box::new(xz2::write::XzEncoder::new(encoder, 6)),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
encoder
|
||||
};
|
||||
|
||||
for format in formats.iter().skip(1).rev() {
|
||||
writer = chain_writer_encoder(format, writer);
|
||||
}
|
||||
|
||||
match formats[0] {
|
||||
Gzip | Bzip | Lzma => {
|
||||
writer = chain_writer_encoder(&formats[0], writer);
|
||||
let mut reader = fs::File::open(&files[0]).unwrap();
|
||||
io::copy(&mut reader, &mut writer)?;
|
||||
},
|
||||
Tar => {
|
||||
let mut writer = archive::tar::build_archive_from_paths(&files, writer)?;
|
||||
writer.flush()?;
|
||||
},
|
||||
Zip => {
|
||||
eprintln!(
|
||||
"{yellow}Warning:{reset}",
|
||||
yellow = colors::yellow(),
|
||||
reset = colors::reset()
|
||||
);
|
||||
eprintln!("\tCompressing .zip entirely in memory.");
|
||||
eprintln!("\tIf the file is too big, your pc might freeze!");
|
||||
eprintln!(
|
||||
"\tThis is a limitation for formats like '{}'.",
|
||||
formats.iter().map(|format| format.to_string()).collect::<String>()
|
||||
);
|
||||
eprintln!("\tThe design of .zip makes it impossible to compress via stream.");
|
||||
|
||||
let mut vec_buffer = io::Cursor::new(vec![]);
|
||||
archive::zip::build_archive_from_paths(&files, &mut vec_buffer)?;
|
||||
let vec_buffer = vec_buffer.into_inner();
|
||||
io::copy(&mut vec_buffer.as_slice(), &mut writer)?;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let bytes = match first_compressor {
|
||||
Some(first_compressor) => {
|
||||
let mut entry = Entry::Files(files);
|
||||
let bytes = first_compressor.compress(entry)?;
|
||||
|
||||
output.contents_in_memory = Some(bytes);
|
||||
entry = Entry::InMemory(output);
|
||||
second_compressor.compress(entry)?
|
||||
},
|
||||
None => {
|
||||
let entry = Entry::Files(files);
|
||||
second_compressor.compress(entry)?
|
||||
},
|
||||
};
|
||||
|
||||
println!(
|
||||
"{}[INFO]{} writing to {:?}. ({})",
|
||||
colors::yellow(),
|
||||
colors::reset(),
|
||||
output_path,
|
||||
utils::Bytes::new(bytes.len() as u64)
|
||||
);
|
||||
fs::write(output_path, bytes)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1,59 +0,0 @@
|
||||
use std::{fs, io::Write, path::PathBuf};
|
||||
|
||||
use utils::colors;
|
||||
|
||||
use super::{Compressor, Entry};
|
||||
use crate::{extension::CompressionFormat, file::File, utils};
|
||||
|
||||
pub struct BzipCompressor;
|
||||
|
||||
impl BzipCompressor {
|
||||
fn compress_files(files: Vec<PathBuf>, format: CompressionFormat) -> crate::Result<Vec<u8>> {
|
||||
utils::check_for_multiple_files(&files, &format)?;
|
||||
let path = &files[0];
|
||||
utils::ensure_exists(path)?;
|
||||
let contents = {
|
||||
let bytes = fs::read(path)?;
|
||||
Self::compress_bytes(&*bytes)?
|
||||
};
|
||||
|
||||
println!(
|
||||
"{}[INFO]{} compressed {:?} into memory ({})",
|
||||
colors::yellow(),
|
||||
colors::reset(),
|
||||
&path,
|
||||
utils::Bytes::new(contents.len() as u64)
|
||||
);
|
||||
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
fn compress_file_in_memory(file: File) -> crate::Result<Vec<u8>> {
|
||||
// Ensure that our file has in-memory content
|
||||
let bytes = match file.contents_in_memory {
|
||||
Some(bytes) => bytes,
|
||||
None => {
|
||||
return Err(crate::Error::InternalError);
|
||||
},
|
||||
};
|
||||
|
||||
Self::compress_bytes(&*bytes)
|
||||
}
|
||||
|
||||
fn compress_bytes(bytes: &[u8]) -> crate::Result<Vec<u8>> {
|
||||
let buffer = vec![];
|
||||
let mut encoder = bzip2::write::BzEncoder::new(buffer, bzip2::Compression::new(6));
|
||||
encoder.write_all(bytes)?;
|
||||
Ok(encoder.finish()?)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: customizable compression level
|
||||
impl Compressor for BzipCompressor {
|
||||
fn compress(&self, from: Entry) -> crate::Result<Vec<u8>> {
|
||||
match from {
|
||||
Entry::Files(files) => Ok(Self::compress_files(files, CompressionFormat::Bzip)?),
|
||||
Entry::InMemory(file) => Ok(Self::compress_file_in_memory(file)?),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::file::File;
|
||||
|
||||
pub enum Entry<'a> {
|
||||
Files(Vec<PathBuf>),
|
||||
InMemory(File<'a>),
|
||||
}
|
||||
|
||||
pub trait Compressor {
|
||||
fn compress(&self, from: Entry) -> crate::Result<Vec<u8>>;
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
use std::{fs, io::Write, path::PathBuf};
|
||||
|
||||
use utils::colors;
|
||||
|
||||
use super::{Compressor, Entry};
|
||||
use crate::{extension::CompressionFormat, file::File, utils};
|
||||
|
||||
pub struct GzipCompressor;
|
||||
|
||||
impl GzipCompressor {
|
||||
pub fn compress_files(
|
||||
files: Vec<PathBuf>,
|
||||
format: CompressionFormat,
|
||||
) -> crate::Result<Vec<u8>> {
|
||||
utils::check_for_multiple_files(&files, &format)?;
|
||||
|
||||
let path = &files[0];
|
||||
utils::ensure_exists(path)?;
|
||||
|
||||
let bytes = {
|
||||
let bytes = fs::read(path)?;
|
||||
Self::compress_bytes(bytes)?
|
||||
};
|
||||
|
||||
println!(
|
||||
"{}[INFO]{} compressed {:?} into memory ({})",
|
||||
colors::yellow(),
|
||||
colors::reset(),
|
||||
&path,
|
||||
utils::Bytes::new(bytes.len() as u64)
|
||||
);
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub fn compress_file_in_memory(file: File) -> crate::Result<Vec<u8>> {
|
||||
let file_contents = match file.contents_in_memory {
|
||||
Some(bytes) => bytes,
|
||||
None => {
|
||||
unreachable!();
|
||||
},
|
||||
};
|
||||
|
||||
Self::compress_bytes(file_contents)
|
||||
}
|
||||
|
||||
pub fn compress_bytes(bytes_to_compress: Vec<u8>) -> crate::Result<Vec<u8>> {
|
||||
let buffer = vec![];
|
||||
let mut encoder = flate2::write::GzEncoder::new(buffer, flate2::Compression::default());
|
||||
encoder.write_all(&*bytes_to_compress)?;
|
||||
|
||||
Ok(encoder.finish()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Compressor for GzipCompressor {
|
||||
fn compress(&self, from: Entry) -> crate::Result<Vec<u8>> {
|
||||
let format = CompressionFormat::Gzip;
|
||||
match from {
|
||||
Entry::Files(files) => Ok(Self::compress_files(files, format)?),
|
||||
Entry::InMemory(file) => Ok(Self::compress_file_in_memory(file)?),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
use std::{fs, io::Write, path::PathBuf};
|
||||
|
||||
use utils::colors;
|
||||
|
||||
use super::{Compressor, Entry};
|
||||
use crate::{extension::CompressionFormat, file::File, utils};
|
||||
|
||||
pub struct LzmaCompressor;
|
||||
|
||||
impl LzmaCompressor {
|
||||
pub fn compress_files(
|
||||
files: Vec<PathBuf>,
|
||||
format: CompressionFormat,
|
||||
) -> crate::Result<Vec<u8>> {
|
||||
utils::check_for_multiple_files(&files, &format)?;
|
||||
|
||||
let path = &files[0];
|
||||
utils::ensure_exists(path)?;
|
||||
|
||||
let bytes = {
|
||||
let bytes = fs::read(path)?;
|
||||
Self::compress_bytes(bytes)?
|
||||
};
|
||||
|
||||
println!(
|
||||
"{}[INFO]{} compressed {:?} into memory ({})",
|
||||
colors::yellow(),
|
||||
colors::reset(),
|
||||
&path,
|
||||
utils::Bytes::new(bytes.len() as u64)
|
||||
);
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub fn compress_file_in_memory(file: File) -> crate::Result<Vec<u8>> {
|
||||
let file_contents = match file.contents_in_memory {
|
||||
Some(bytes) => bytes,
|
||||
None => {
|
||||
unreachable!();
|
||||
},
|
||||
};
|
||||
|
||||
Self::compress_bytes(file_contents)
|
||||
}
|
||||
|
||||
pub fn compress_bytes(bytes_to_compress: Vec<u8>) -> crate::Result<Vec<u8>> {
|
||||
let buffer = vec![];
|
||||
let mut encoder = xz2::write::XzEncoder::new(buffer, 6);
|
||||
encoder.write_all(&*bytes_to_compress)?;
|
||||
|
||||
Ok(encoder.finish()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Compressor for LzmaCompressor {
|
||||
fn compress(&self, from: Entry) -> crate::Result<Vec<u8>> {
|
||||
let format = CompressionFormat::Lzma;
|
||||
match from {
|
||||
Entry::Files(files) => Self::compress_files(files, format),
|
||||
Entry::InMemory(file) => Self::compress_file_in_memory(file),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
//! This module contains the Compressor trait and an implementor for each format.
|
||||
mod bzip;
|
||||
mod compressor;
|
||||
mod gzip;
|
||||
mod lzma;
|
||||
mod tar;
|
||||
mod zip;
|
||||
|
||||
pub use compressor::Compressor;
|
||||
|
||||
pub use self::{
|
||||
bzip::BzipCompressor, compressor::Entry, gzip::GzipCompressor, lzma::LzmaCompressor,
|
||||
tar::TarCompressor, zip::ZipCompressor,
|
||||
};
|
@ -1,53 +0,0 @@
|
||||
use std::{env, fs, path::PathBuf};
|
||||
|
||||
use tar::Builder;
|
||||
use utils::colors;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use super::compressor::Entry;
|
||||
use crate::{compressors::Compressor, file::File, utils};
|
||||
|
||||
pub struct TarCompressor;
|
||||
|
||||
impl TarCompressor {
|
||||
// TODO: implement this
|
||||
fn make_archive_from_memory(_input: File) -> crate::Result<Vec<u8>> {
|
||||
println!(
|
||||
"{}[ERROR]{} .tar.tar and .zip.tar is currently unimplemented.",
|
||||
colors::red(),
|
||||
colors::reset()
|
||||
);
|
||||
Err(crate::Error::InvalidZipArchive(""))
|
||||
}
|
||||
|
||||
fn make_archive_from_files(input_filenames: Vec<PathBuf>) -> crate::Result<Vec<u8>> {
|
||||
let buf = Vec::new();
|
||||
let mut b = Builder::new(buf);
|
||||
|
||||
for filename in input_filenames {
|
||||
let previous_location = utils::change_dir_and_return_parent(&filename)?;
|
||||
let filename = filename.file_name().unwrap();
|
||||
for entry in WalkDir::new(&filename) {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
println!("Compressing {:?}", path);
|
||||
if path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
b.append_file(path, &mut fs::File::open(path)?)?;
|
||||
}
|
||||
env::set_current_dir(previous_location)?;
|
||||
}
|
||||
|
||||
Ok(b.into_inner()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Compressor for TarCompressor {
|
||||
fn compress(&self, from: Entry) -> crate::Result<Vec<u8>> {
|
||||
match from {
|
||||
Entry::Files(filenames) => Self::make_archive_from_files(filenames),
|
||||
Entry::InMemory(file) => Self::make_archive_from_memory(file),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
use std::{
|
||||
io::{Cursor, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use super::compressor::Entry;
|
||||
use crate::{compressors::Compressor, file::File, utils};
|
||||
|
||||
pub struct ZipCompressor;
|
||||
|
||||
impl ZipCompressor {
|
||||
// TODO: this function does not seem to be working correctly ;/
|
||||
fn make_archive_from_memory(input: File) -> crate::Result<Vec<u8>> {
|
||||
let buffer = vec![];
|
||||
let mut writer = zip::ZipWriter::new(std::io::Cursor::new(buffer));
|
||||
|
||||
let inner_file_path: Box<str> = input
|
||||
.path
|
||||
.file_stem()
|
||||
.ok_or(
|
||||
// TODO: Is this reachable?
|
||||
crate::Error::InvalidInput,
|
||||
)?
|
||||
.to_string_lossy()
|
||||
.into();
|
||||
|
||||
let options =
|
||||
zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated);
|
||||
|
||||
writer.start_file(inner_file_path, options)?;
|
||||
|
||||
let input_bytes = match input.contents_in_memory {
|
||||
Some(bytes) => bytes,
|
||||
None => {
|
||||
// TODO: error description, although this block should not be
|
||||
// reachable
|
||||
return Err(crate::Error::InvalidInput);
|
||||
},
|
||||
};
|
||||
|
||||
writer.write_all(&*input_bytes)?;
|
||||
|
||||
let bytes = writer.finish().unwrap();
|
||||
|
||||
Ok(bytes.into_inner())
|
||||
}
|
||||
|
||||
fn make_archive_from_files(input_filenames: Vec<PathBuf>) -> crate::Result<Vec<u8>> {
|
||||
let buffer = vec![];
|
||||
let mut writer = zip::ZipWriter::new(Cursor::new(buffer));
|
||||
|
||||
let options =
|
||||
zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated);
|
||||
|
||||
for filename in input_filenames {
|
||||
let previous_location = utils::change_dir_and_return_parent(&filename)?;
|
||||
let filename = filename
|
||||
.file_name()
|
||||
// Safe unwrap since the function call above would fail in scenarios
|
||||
// where this unwrap would panic
|
||||
.unwrap();
|
||||
|
||||
for entry in WalkDir::new(filename) {
|
||||
let entry = entry?;
|
||||
let entry_path = &entry.path();
|
||||
if entry_path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
writer.start_file(entry_path.to_string_lossy(), options)?;
|
||||
println!("Compressing {:?}", entry_path);
|
||||
let file_bytes = std::fs::read(entry.path())?;
|
||||
writer.write_all(&*file_bytes)?;
|
||||
}
|
||||
|
||||
std::env::set_current_dir(previous_location)?;
|
||||
}
|
||||
|
||||
let bytes = writer.finish()?;
|
||||
|
||||
Ok(bytes.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl Compressor for ZipCompressor {
|
||||
fn compress(&self, from: Entry) -> crate::Result<Vec<u8>> {
|
||||
match from {
|
||||
Entry::Files(filenames) => Ok(Self::make_archive_from_files(filenames)?),
|
||||
Entry::InMemory(file) => Ok(Self::make_archive_from_memory(file)?),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,71 +1,7 @@
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fmt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use std::{fmt, path::Path};
|
||||
|
||||
use CompressionFormat::*;
|
||||
|
||||
use crate::utils;
|
||||
|
||||
/// Represents the extension of a file, but only really caring about
|
||||
/// compression formats (and .tar).
|
||||
/// Ex.: Extension::new("file.tar.gz") == Extension { first_ext: Some(Tar), second_ext: Gzip }
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Extension {
|
||||
pub first_ext: Option<CompressionFormat>,
|
||||
pub second_ext: CompressionFormat,
|
||||
}
|
||||
|
||||
pub fn get_extension_from_filename(file_name: &OsStr) -> Option<(&OsStr, &OsStr)> {
|
||||
let path = Path::new(file_name);
|
||||
|
||||
let ext = path.extension()?;
|
||||
|
||||
let previous_extension = path.file_stem().and_then(get_extension_from_filename);
|
||||
|
||||
if let Some((_, prev)) = previous_extension {
|
||||
Some((prev, ext))
|
||||
} else {
|
||||
Some((OsStr::new(""), ext))
|
||||
}
|
||||
}
|
||||
|
||||
impl Extension {
|
||||
pub fn from(file_name: &OsStr) -> crate::Result<Self> {
|
||||
let compression_format_from = |ext: &OsStr| match ext {
|
||||
_ if ext == "zip" => Ok(Zip),
|
||||
_ if ext == "tar" => Ok(Tar),
|
||||
_ if ext == "gz" => Ok(Gzip),
|
||||
_ if ext == "bz" || ext == "bz2" => Ok(Bzip),
|
||||
_ if ext == "xz" || ext == "lz" || ext == "lzma" => Ok(Lzma),
|
||||
other => Err(crate::Error::UnknownExtensionError(utils::to_utf(other))),
|
||||
};
|
||||
|
||||
let (first_ext, second_ext) = match get_extension_from_filename(file_name) {
|
||||
Some(extension_tuple) => match extension_tuple {
|
||||
(os_str, snd) if os_str.is_empty() => (None, snd),
|
||||
(fst, snd) => (Some(fst), snd),
|
||||
},
|
||||
None => return Err(crate::Error::MissingExtensionError(PathBuf::from(file_name))),
|
||||
};
|
||||
|
||||
let (first_ext, second_ext) = match (first_ext, second_ext) {
|
||||
(None, snd) => {
|
||||
let ext = compression_format_from(snd)?;
|
||||
(None, ext)
|
||||
},
|
||||
(Some(fst), snd) => {
|
||||
let snd = compression_format_from(snd)?;
|
||||
let fst = compression_format_from(fst).ok();
|
||||
(fst, snd)
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Self { first_ext, second_ext })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
/// Accepted extensions for input and output
|
||||
pub enum CompressionFormat {
|
||||
@ -120,3 +56,8 @@ pub fn separate_known_extensions_from_name(mut path: &Path) -> (&Path, Vec<Compr
|
||||
|
||||
(path, extensions)
|
||||
}
|
||||
|
||||
pub fn extensions_from_path(path: &Path) -> Vec<CompressionFormat> {
|
||||
let (_, extensions) = separate_known_extensions_from_name(path);
|
||||
extensions
|
||||
}
|
||||
|
26
src/file.rs
26
src/file.rs
@ -1,26 +0,0 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::extension::Extension;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct File<'a> {
|
||||
/// File's (relative) path
|
||||
pub path: &'a Path,
|
||||
/// The bytes that compose the file.
|
||||
/// Only used when the whole file is kept in-memory
|
||||
pub contents_in_memory: Option<Vec<u8>>,
|
||||
/// Note: extension here might be a misleading name since
|
||||
/// we don't really care about any extension other than supported compression ones.
|
||||
///
|
||||
/// So, for example, if a file has pathname "image.jpeg", it does have a JPEG extension but will
|
||||
/// be represented as a None over here since that's not an extension we're particularly interested in
|
||||
pub extension: Option<Extension>,
|
||||
}
|
||||
|
||||
impl<'a> File<'a> {
|
||||
pub fn from(path: &'a Path) -> crate::Result<Self> {
|
||||
let extension = Extension::from(path.as_ref()).ok();
|
||||
|
||||
Ok(File { path, contents_in_memory: None, extension })
|
||||
}
|
||||
}
|
@ -5,11 +5,9 @@ pub mod oof;
|
||||
|
||||
// Private modules
|
||||
pub mod archive;
|
||||
mod compressors;
|
||||
mod dialogs;
|
||||
mod error;
|
||||
mod extension;
|
||||
mod file;
|
||||
mod utils;
|
||||
|
||||
use dialogs::Confirmation;
|
||||
|
34
src/utils.rs
34
src/utils.rs
@ -5,7 +5,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::{extension::CompressionFormat, oof, OVERWRITE_CONFIRMATION};
|
||||
use crate::{oof, OVERWRITE_CONFIRMATION};
|
||||
|
||||
#[macro_export]
|
||||
#[cfg(debug_assertions)]
|
||||
@ -23,34 +23,6 @@ macro_rules! debug {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn ensure_exists(path: impl AsRef<Path>) -> crate::Result<()> {
|
||||
let exists = path.as_ref().exists();
|
||||
if !exists {
|
||||
return Err(crate::Error::FileNotFound(PathBuf::from(path.as_ref())));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_for_multiple_files(
|
||||
files: &[PathBuf],
|
||||
format: &CompressionFormat,
|
||||
) -> crate::Result<()> {
|
||||
if files.len() != 1 {
|
||||
eprintln!(
|
||||
"{}[ERROR]{} cannot compress multiple files directly to {:#?}.\n\
|
||||
Try using an intermediate archival method such as Tar.\n\
|
||||
Example: filename.tar{}",
|
||||
colors::red(),
|
||||
colors::reset(),
|
||||
format,
|
||||
format
|
||||
);
|
||||
return Err(crate::Error::InvalidInput);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_dir_if_non_existent(path: &Path) -> crate::Result<()> {
|
||||
if !path.exists() {
|
||||
println!(
|
||||
@ -70,7 +42,7 @@ pub fn create_dir_if_non_existent(path: &Path) -> crate::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn change_dir_and_return_parent(filename: &Path) -> crate::Result<PathBuf> {
|
||||
pub fn cd_into_same_dir_as(filename: &Path) -> crate::Result<PathBuf> {
|
||||
let previous_location = env::current_dir()?;
|
||||
|
||||
let parent = if let Some(parent) = filename.parent() {
|
||||
@ -79,6 +51,8 @@ pub fn change_dir_and_return_parent(filename: &Path) -> crate::Result<PathBuf> {
|
||||
return Err(crate::Error::CompressingRootFolder);
|
||||
};
|
||||
|
||||
// TODO: fix this error variant, as it is not the only possible error that can
|
||||
// come out of this operation
|
||||
env::set_current_dir(parent).ok().ok_or(crate::Error::CompressingRootFolder)?;
|
||||
|
||||
Ok(previous_location)
|
||||
|
@ -15,17 +15,20 @@ fn test_each_format() {
|
||||
test_compression_and_decompression("tar");
|
||||
test_compression_and_decompression("tar.gz");
|
||||
test_compression_and_decompression("tar.bz");
|
||||
// test_compression_and_decompression("tar.bz2");
|
||||
// test_compression_and_decompression("tar.xz");
|
||||
test_compression_and_decompression("tar.bz2");
|
||||
test_compression_and_decompression("tar.xz");
|
||||
test_compression_and_decompression("tar.lz");
|
||||
// test_compression_and_decompression("tar.lzma");
|
||||
test_compression_and_decompression("tar.lzma");
|
||||
test_compression_and_decompression("zip");
|
||||
test_compression_and_decompression("zip.gz");
|
||||
test_compression_and_decompression("zip.bz");
|
||||
// test_compression_and_decompression("zip.bz2");
|
||||
// test_compression_and_decompression("zip.xz");
|
||||
test_compression_and_decompression("zip.bz2");
|
||||
test_compression_and_decompression("zip.xz");
|
||||
test_compression_and_decompression("zip.lz");
|
||||
// test_compression_and_decompression("zip.lzma");
|
||||
test_compression_and_decompression("zip.lzma");
|
||||
|
||||
// Why not
|
||||
test_compression_and_decompression("tar.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.lz.lz.lz.lz.lz.lz.lz.lz.lz.lz.bz.bz.bz.bz.bz.bz.bz");
|
||||
}
|
||||
|
||||
type FileContent = Vec<u8>;
|
||||
@ -92,7 +95,7 @@ fn compress_files(at: &Path, paths_to_compress: &[PathBuf], format: &str) -> Pat
|
||||
|
||||
let command = Command::Compress {
|
||||
files: paths_to_compress.to_vec(),
|
||||
compressed_output_path: archive_path.to_path_buf(),
|
||||
output_path: archive_path.to_path_buf(),
|
||||
};
|
||||
run(command, &oof::Flags::default()).expect("Failed to compress test dummy files");
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user