Implement command 'list' to show archive contents

This commit is contained in:
Anton Hermann 2021-10-31 18:18:50 +01:00
parent a02eb452c0
commit 30ebcf4f9e
8 changed files with 195 additions and 1 deletions

View File

@ -13,6 +13,7 @@ use walkdir::WalkDir;
use crate::{ use crate::{
error::FinalError, error::FinalError,
info, info,
list::FileInArchive,
utils::{self, Bytes}, utils::{self, Bytes},
QuestionPolicy, QuestionPolicy,
}; };
@ -42,6 +43,18 @@ pub fn unpack_archive(
Ok(files_unpacked) Ok(files_unpacked)
} }
pub fn list_archive(reader: Box<dyn Read>) -> crate::Result<Vec<FileInArchive>> {
let mut archive = tar::Archive::new(reader);
let mut files = vec![];
for file in archive.entries()? {
let file = file?;
files.push(FileInArchive { path: file.path()?.into_owned() });
}
Ok(files)
}
pub fn build_archive_from_paths<W>(input_filenames: &[PathBuf], writer: W) -> crate::Result<W> pub fn build_archive_from_paths<W>(input_filenames: &[PathBuf], writer: W) -> crate::Result<W>
where where

View File

@ -13,6 +13,7 @@ use zip::{self, read::ZipFile, ZipArchive};
use crate::{ use crate::{
info, info,
list::FileInArchive,
utils::{self, dir_is_empty, strip_cur_dir, Bytes}, utils::{self, dir_is_empty, strip_cur_dir, Bytes},
QuestionPolicy, QuestionPolicy,
}; };
@ -73,6 +74,22 @@ where
Ok(unpacked_files) Ok(unpacked_files)
} }
pub fn list_archive<R>(mut archive: ZipArchive<R>) -> crate::Result<Vec<FileInArchive>>
where
R: Read + Seek,
{
let mut files = vec![];
for idx in 0..archive.len() {
let file = archive.by_index(idx)?;
let path = match file.enclosed_name() {
Some(path) => path.to_owned(),
None => continue,
};
files.push(FileInArchive { path });
}
Ok(files)
}
pub fn build_archive_from_paths<W>(input_filenames: &[PathBuf], writer: W) -> crate::Result<W> pub fn build_archive_from_paths<W>(input_filenames: &[PathBuf], writer: W) -> crate::Result<W>
where where
W: Write + Seek, W: Write + Seek,

View File

@ -16,7 +16,9 @@ impl Opts {
pub fn parse_args() -> crate::Result<(Self, QuestionPolicy)> { pub fn parse_args() -> crate::Result<(Self, QuestionPolicy)> {
let mut opts: Self = Self::parse(); let mut opts: Self = Self::parse();
let (Subcommand::Compress { files, .. } | Subcommand::Decompress { files, .. }) = &mut opts.cmd; let (Subcommand::Compress { files, .. }
| Subcommand::Decompress { files, .. }
| Subcommand::List { archives: files, .. }) = &mut opts.cmd;
*files = canonicalize_files(files)?; *files = canonicalize_files(files)?;
let skip_questions_positively = if opts.yes { let skip_questions_positively = if opts.yes {

View File

@ -18,6 +18,7 @@ use crate::{
CompressionFormat::{self, *}, CompressionFormat::{self, *},
}, },
info, info,
list::{self, ListOptions},
utils::{self, dir_is_empty, nice_directory_display, to_utf}, utils::{self, dir_is_empty, nice_directory_display, to_utf},
Error, Opts, QuestionPolicy, Subcommand, Error, Opts, QuestionPolicy, Subcommand,
}; };
@ -178,6 +179,40 @@ pub fn run(args: Opts, question_policy: QuestionPolicy) -> crate::Result<()> {
decompress_file(input_path, formats, output_dir, file_name, question_policy)?; decompress_file(input_path, formats, output_dir, file_name, question_policy)?;
} }
} }
Subcommand::List { archives: files, tree } => {
let mut formats = vec![];
for path in files.iter() {
let (_, file_formats) = extension::separate_known_extensions_from_name(path);
formats.push(file_formats);
}
let not_archives: Vec<PathBuf> = files
.iter()
.zip(&formats)
.filter(|(_, formats)| formats.is_empty() || !formats[0].is_archive())
.map(|(path, _)| path.clone())
.collect();
// Error
if !not_archives.is_empty() {
eprintln!("Some file you asked ouch to list the contents of is not an archive.");
for file in &not_archives {
eprintln!("Could not list {}.", to_utf(file));
}
todo!(
"Dev note: add this error variant and pass the Vec to it, all the files \
lacking extension shall be shown: {:#?}.",
not_archives
);
}
let list_options = ListOptions { tree };
for (archive_path, formats) in files.iter().zip(formats) {
list_archive_contents(archive_path, formats, list_options)?;
}
}
} }
Ok(()) Ok(())
} }
@ -369,3 +404,85 @@ fn decompress_file(
Ok(()) Ok(())
} }
// File at input_file_path is opened for reading, example: "archive.tar.gz"
// formats contains each format necessary for decompression, example: [Gz, Tar] (in decompression order)
fn list_archive_contents(
archive_path: &Path,
formats: Vec<CompressionFormat>,
list_options: ListOptions,
) -> crate::Result<()> {
// TODO: improve error message
let reader = fs::File::open(&archive_path)?;
// Zip archives are special, because they require io::Seek, so it requires it's logic separated
// from decoder chaining.
//
// This is the only case where we can read and unpack it directly, without having to do
// in-memory decompression/copying first.
//
// Any other Zip decompression done can take up the whole RAM and freeze ouch.
if let [Zip] = *formats.as_slice() {
let zip_archive = zip::ZipArchive::new(reader)?;
let files = crate::archive::zip::list_archive(zip_archive)?;
list::list_files(files, list_options);
return Ok(());
}
// Will be used in decoder chaining
let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader);
let mut reader: Box<dyn Read> = Box::new(reader);
// Grab previous decoder and wrap it inside of a new one
let chain_reader_decoder = |format: &CompressionFormat, decoder: Box<dyn Read>| -> crate::Result<Box<dyn Read>> {
let decoder: Box<dyn Read> = match format {
Gzip => Box::new(flate2::read::GzDecoder::new(decoder)),
Bzip => Box::new(bzip2::read::BzDecoder::new(decoder)),
Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
_ => unreachable!(),
};
Ok(decoder)
};
for format in formats.iter().skip(1).rev() {
reader = chain_reader_decoder(format, reader)?;
}
let files = match formats[0] {
Tar => crate::archive::tar::list_archive(reader)?,
Tgz => {
let reader = chain_reader_decoder(&Gzip, reader)?;
crate::archive::tar::list_archive(reader)?
}
Tbz => {
let reader = chain_reader_decoder(&Bzip, reader)?;
crate::archive::tar::list_archive(reader)?
}
Tlzma => {
let reader = chain_reader_decoder(&Lzma, reader)?;
crate::archive::tar::list_archive(reader)?
}
Tzst => {
let reader = chain_reader_decoder(&Zstd, reader)?;
crate::archive::tar::list_archive(reader)?
}
Zip => {
eprintln!("Listing files from zip archive.");
eprintln!("Warning: .zip archives with extra extensions have a downside.");
eprintln!("The only way is loading everything into the RAM while compressing, and then reading the archive contents.");
eprintln!("this means that by compressing .zip with extra compression formats, you can run out of RAM if the file is too large!");
let mut vec = vec![];
io::copy(&mut reader, &mut vec)?;
let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
crate::archive::zip::list_archive(zip_archive)?
}
Gzip | Bzip | Lzma | Zstd => {
panic!("Not an archive! This should never happen, if it does, something is wrong with `CompressionFormat::is_archive()`. Please report this error!");
}
};
list::list_files(files, list_options);
Ok(())
}

View File

@ -45,6 +45,11 @@ impl fmt::Display for CompressionFormat {
) )
} }
} }
impl CompressionFormat {
pub fn is_archive(&self) -> bool {
matches!(self, Tar | Tgz | Tbz | Tlzma | Tzst | Zip)
}
}
pub fn separate_known_extensions_from_name(mut path: &Path) -> (&Path, Vec<CompressionFormat>) { pub fn separate_known_extensions_from_name(mut path: &Path) -> (&Path, Vec<CompressionFormat>) {
// // TODO: check for file names with the name of an extension // // TODO: check for file names with the name of an extension

View File

@ -13,6 +13,7 @@ mod cli;
mod dialogs; mod dialogs;
mod error; mod error;
mod extension; mod extension;
mod list;
mod macros; mod macros;
mod opts; mod opts;
mod utils; mod utils;

28
src/list.rs Normal file
View File

@ -0,0 +1,28 @@
//! Implementation of the 'list' command, print list of files in an archive
use std::path::PathBuf;
/// Options controlling how archive contents should be listed
#[derive(Debug, Clone, Copy)]
pub struct ListOptions {
/// Whether to show a tree view
pub tree: bool,
}
/// Represents a single file in an archive, used in `list::list_files()`
#[derive(Debug, Clone)]
pub struct FileInArchive {
/// The file path
pub path: PathBuf,
}
/// Actually print the files
pub fn list_files(files: Vec<FileInArchive>, list_options: ListOptions) {
if list_options.tree {
todo!("Implement tree view");
} else {
for file in files {
println!("{}", file.path.display());
}
}
}

View File

@ -41,4 +41,15 @@ pub enum Subcommand {
#[clap(short, long = "dir", value_hint = ValueHint::DirPath)] #[clap(short, long = "dir", value_hint = ValueHint::DirPath)]
output_dir: Option<PathBuf>, output_dir: Option<PathBuf>,
}, },
/// List contents. Alias: l
#[clap(alias = "l")]
List {
/// Archives whose contents should be listed
#[clap(required = true, min_values = 1)]
archives: Vec<PathBuf>,
/// Show archive contents as a tree
#[clap(short, long)]
tree: bool,
},
} }