mirror of
https://github.com/ouch-org/ouch.git
synced 2025-06-07 12:05:46 +00:00
Implement command 'list' to show archive contents
This commit is contained in:
parent
a02eb452c0
commit
30ebcf4f9e
@ -13,6 +13,7 @@ use walkdir::WalkDir;
|
||||
use crate::{
|
||||
error::FinalError,
|
||||
info,
|
||||
list::FileInArchive,
|
||||
utils::{self, Bytes},
|
||||
QuestionPolicy,
|
||||
};
|
||||
@ -42,6 +43,18 @@ pub fn unpack_archive(
|
||||
|
||||
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>
|
||||
where
|
||||
|
@ -13,6 +13,7 @@ use zip::{self, read::ZipFile, ZipArchive};
|
||||
|
||||
use crate::{
|
||||
info,
|
||||
list::FileInArchive,
|
||||
utils::{self, dir_is_empty, strip_cur_dir, Bytes},
|
||||
QuestionPolicy,
|
||||
};
|
||||
@ -73,6 +74,22 @@ where
|
||||
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>
|
||||
where
|
||||
W: Write + Seek,
|
||||
|
@ -16,7 +16,9 @@ impl Opts {
|
||||
pub fn parse_args() -> crate::Result<(Self, QuestionPolicy)> {
|
||||
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)?;
|
||||
|
||||
let skip_questions_positively = if opts.yes {
|
||||
|
117
src/commands.rs
117
src/commands.rs
@ -18,6 +18,7 @@ use crate::{
|
||||
CompressionFormat::{self, *},
|
||||
},
|
||||
info,
|
||||
list::{self, ListOptions},
|
||||
utils::{self, dir_is_empty, nice_directory_display, to_utf},
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
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 ¬_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(())
|
||||
}
|
||||
@ -369,3 +404,85 @@ fn decompress_file(
|
||||
|
||||
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(())
|
||||
}
|
||||
|
@ -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>) {
|
||||
// // TODO: check for file names with the name of an extension
|
||||
|
@ -13,6 +13,7 @@ mod cli;
|
||||
mod dialogs;
|
||||
mod error;
|
||||
mod extension;
|
||||
mod list;
|
||||
mod macros;
|
||||
mod opts;
|
||||
mod utils;
|
||||
|
28
src/list.rs
Normal file
28
src/list.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
11
src/opts.rs
11
src/opts.rs
@ -41,4 +41,15 @@ pub enum Subcommand {
|
||||
#[clap(short, long = "dir", value_hint = ValueHint::DirPath)]
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user