mirror of
https://github.com/ouch-org/ouch.git
synced 2025-06-07 12:05:46 +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::{
|
use std::{
|
||||||
io::Read,
|
env, fs,
|
||||||
|
io::prelude::*,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use tar;
|
use tar;
|
||||||
use utils::colors;
|
use utils::colors;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use crate::{oof, utils};
|
use crate::{oof, utils};
|
||||||
|
|
||||||
pub fn unpack_archive(
|
pub fn unpack_archive(
|
||||||
reader: Box<dyn Read>,
|
reader: Box<dyn Read>,
|
||||||
output_folder: &Path,
|
output_folder: &Path,
|
||||||
flags: &oof::Flags,
|
flags: &oof::Flags,
|
||||||
) -> crate::Result<Vec<PathBuf>> {
|
) -> 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 archive = tar::Archive::new(reader);
|
||||||
|
|
||||||
let mut files_unpacked = vec![];
|
let mut files_unpacked = vec![];
|
||||||
@ -47,3 +42,31 @@ pub fn unpack_archive(
|
|||||||
|
|
||||||
Ok(files_unpacked)
|
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::{
|
use std::{
|
||||||
fs,
|
env, fs,
|
||||||
io::{self, Read, Seek},
|
io::{self, prelude::*},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use walkdir::WalkDir;
|
||||||
use zip::{self, read::ZipFile, ZipArchive};
|
use zip::{self, read::ZipFile, ZipArchive};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -90,3 +91,54 @@ where
|
|||||||
|
|
||||||
Ok(unpacked_files)
|
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
|
/// Files to be compressed
|
||||||
Compress {
|
Compress {
|
||||||
files: Vec<PathBuf>,
|
files: Vec<PathBuf>,
|
||||||
compressed_output_path: PathBuf,
|
output_path: PathBuf,
|
||||||
},
|
},
|
||||||
/// Files to be decompressed and their extensions
|
/// Files to be decompressed and their extensions
|
||||||
Decompress {
|
Decompress {
|
||||||
@ -103,9 +103,9 @@ pub fn parse_args_from(mut args: Vec<OsString>) -> crate::Result<ParsedArgs> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Safety: we checked that args.len() >= 2
|
// 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 }
|
ParsedArgs { command, flags }
|
||||||
},
|
},
|
||||||
// Defaults to decompression when there is no subcommand
|
// 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 {
|
assert_eq!(test_cli("compress foo bar baz.zip").unwrap().command, Command::Compress {
|
||||||
files: vec!["foo".into(), "bar".into()],
|
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);
|
assert_eq!(test_cli("compress").unwrap_err(), crate::Error::MissingArgumentsForCompression);
|
||||||
}
|
}
|
||||||
|
144
src/commands.rs
144
src/commands.rs
@ -1,29 +1,31 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
io::{self, BufReader, Read},
|
io::{self, BufReader, BufWriter, Read, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use utils::colors;
|
use utils::colors;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
archive,
|
||||||
cli::Command,
|
cli::Command,
|
||||||
compressors::{
|
|
||||||
BzipCompressor, Compressor, Entry, GzipCompressor, LzmaCompressor, TarCompressor,
|
|
||||||
ZipCompressor,
|
|
||||||
},
|
|
||||||
extension::{
|
extension::{
|
||||||
self,
|
self,
|
||||||
CompressionFormat::{self, *},
|
CompressionFormat::{self, *},
|
||||||
},
|
},
|
||||||
file, oof, utils,
|
oof, utils,
|
||||||
utils::to_utf,
|
utils::to_utf,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> {
|
pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> {
|
||||||
match command {
|
match command {
|
||||||
Command::Compress { files, compressed_output_path } => {
|
Command::Compress { files, output_path } => {
|
||||||
compress_files(files, &compressed_output_path, flags)?
|
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 } => {
|
Command::Decompress { files, output_folder } => {
|
||||||
let mut output_paths = vec![];
|
let mut output_paths = vec![];
|
||||||
@ -67,80 +69,72 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> {
|
|||||||
Ok(())
|
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(
|
fn compress_files(
|
||||||
files: Vec<PathBuf>,
|
files: Vec<PathBuf>,
|
||||||
output_path: &Path,
|
formats: Vec<CompressionFormat>,
|
||||||
flags: &oof::Flags,
|
output_file: fs::File,
|
||||||
|
_flags: &oof::Flags,
|
||||||
) -> crate::Result<()> {
|
) -> 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] {
|
||||||
if output_path.exists() && !utils::permission_for_overwriting(output_path, flags)? {
|
Tar => archive::tar::build_archive_from_paths,
|
||||||
// The user does not want to overwrite the file
|
Zip => archive::zip::build_archive_from_paths,
|
||||||
return Ok(());
|
_ => unreachable!(),
|
||||||
}
|
|
||||||
|
|
||||||
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!(
|
let mut bufwriter = build_archive_from_paths(&files, file_writer)?;
|
||||||
"{}[INFO]{} writing to {:?}. ({})",
|
bufwriter.flush()?;
|
||||||
colors::yellow(),
|
} else {
|
||||||
colors::reset(),
|
let mut writer: Box<dyn Write> = Box::new(file_writer);
|
||||||
output_path,
|
|
||||||
utils::Bytes::new(bytes.len() as u64)
|
// 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()
|
||||||
);
|
);
|
||||||
fs::write(output_path, bytes)?;
|
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)?;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
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::{
|
use std::{fmt, path::Path};
|
||||||
ffi::OsStr,
|
|
||||||
fmt,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use CompressionFormat::*;
|
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)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
/// Accepted extensions for input and output
|
/// Accepted extensions for input and output
|
||||||
pub enum CompressionFormat {
|
pub enum CompressionFormat {
|
||||||
@ -120,3 +56,8 @@ pub fn separate_known_extensions_from_name(mut path: &Path) -> (&Path, Vec<Compr
|
|||||||
|
|
||||||
(path, extensions)
|
(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
|
// Private modules
|
||||||
pub mod archive;
|
pub mod archive;
|
||||||
mod compressors;
|
|
||||||
mod dialogs;
|
mod dialogs;
|
||||||
mod error;
|
mod error;
|
||||||
mod extension;
|
mod extension;
|
||||||
mod file;
|
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use dialogs::Confirmation;
|
use dialogs::Confirmation;
|
||||||
|
34
src/utils.rs
34
src/utils.rs
@ -5,7 +5,7 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{extension::CompressionFormat, oof, OVERWRITE_CONFIRMATION};
|
use crate::{oof, OVERWRITE_CONFIRMATION};
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
#[cfg(debug_assertions)]
|
#[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<()> {
|
pub fn create_dir_if_non_existent(path: &Path) -> crate::Result<()> {
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
println!(
|
println!(
|
||||||
@ -70,7 +42,7 @@ pub fn create_dir_if_non_existent(path: &Path) -> crate::Result<()> {
|
|||||||
Ok(())
|
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 previous_location = env::current_dir()?;
|
||||||
|
|
||||||
let parent = if let Some(parent) = filename.parent() {
|
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);
|
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)?;
|
env::set_current_dir(parent).ok().ok_or(crate::Error::CompressingRootFolder)?;
|
||||||
|
|
||||||
Ok(previous_location)
|
Ok(previous_location)
|
||||||
|
@ -15,17 +15,20 @@ fn test_each_format() {
|
|||||||
test_compression_and_decompression("tar");
|
test_compression_and_decompression("tar");
|
||||||
test_compression_and_decompression("tar.gz");
|
test_compression_and_decompression("tar.gz");
|
||||||
test_compression_and_decompression("tar.bz");
|
test_compression_and_decompression("tar.bz");
|
||||||
// test_compression_and_decompression("tar.bz2");
|
test_compression_and_decompression("tar.bz2");
|
||||||
// test_compression_and_decompression("tar.xz");
|
test_compression_and_decompression("tar.xz");
|
||||||
test_compression_and_decompression("tar.lz");
|
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");
|
||||||
test_compression_and_decompression("zip.gz");
|
test_compression_and_decompression("zip.gz");
|
||||||
test_compression_and_decompression("zip.bz");
|
test_compression_and_decompression("zip.bz");
|
||||||
// test_compression_and_decompression("zip.bz2");
|
test_compression_and_decompression("zip.bz2");
|
||||||
// test_compression_and_decompression("zip.xz");
|
test_compression_and_decompression("zip.xz");
|
||||||
test_compression_and_decompression("zip.lz");
|
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>;
|
type FileContent = Vec<u8>;
|
||||||
@ -92,7 +95,7 @@ fn compress_files(at: &Path, paths_to_compress: &[PathBuf], format: &str) -> Pat
|
|||||||
|
|
||||||
let command = Command::Compress {
|
let command = Command::Compress {
|
||||||
files: paths_to_compress.to_vec(),
|
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");
|
run(command, &oof::Flags::default()).expect("Failed to compress test dummy files");
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user