mirror of
https://github.com/ouch-org/ouch.git
synced 2025-06-07 20:15:27 +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::{
|
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
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
117
src/commands.rs
117
src/commands.rs
@ -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 ¬_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(())
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
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)]
|
#[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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user