Merge branch 'master' into Remove-tar-combinations-from-CompressionFormat

This commit is contained in:
João Marcos Bezerra 2021-11-02 01:00:18 -03:00 committed by GitHub
commit 75cad36c5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 370 additions and 407 deletions

87
.github/workflows/build-and-test.yml vendored Normal file
View File

@ -0,0 +1,87 @@
name: build-and-test
on: [push, pull_request]
jobs:
build:
name: build
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- target: x86_64-apple-darwin
os: macos-latest
# - target: x86_64-pc-windows-gnu
# os: windows-latest
# ext: .exe
- target: x86_64-pc-windows-msvc
os: windows-latest
ext: .exe
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install dependencies (musl)
if: ${{ matrix.target == 'x86_64-unknown-linux-musl' }}
run: |
sudo apt-get update
sudo apt-get install help2man musl-tools
- name: Build and test on stable
run: |
rustup toolchain install stable --profile minimal -t ${{ matrix.target }}
cargo +stable build --target ${{ matrix.target }}
cargo +stable test --target ${{ matrix.target }}
- name: Release on nightly
run: |
rustup toolchain install nightly --profile minimal -t ${{ matrix.target }}
cargo +nightly build --release --target ${{ matrix.target }}
env:
GEN_COMPLETIONS: 1
RUSTFLAGS: -Z strip=symbols
- name: Upload bianry
uses: actions/upload-artifact@v2
with:
name: ouch-${{ matrix.target }}${{ matrix.ext }}
path: target/${{ matrix.target }}/release/ouch${{ matrix.ext }}
- name: Build man page and find completions (musl)
if: ${{ matrix.target == 'x86_64-unknown-linux-musl' }}
run: |
help2man target/${{ matrix.target }}/release/ouch > ouch.1
cp -r target/${{ matrix.target }}/release/build/ouch-*/out/completions .
- name: Upload completions (musl)
if: ${{ matrix.target == 'x86_64-unknown-linux-musl' }}
uses: actions/upload-artifact@v2
with:
name: completions
path: completions
- name: Upload man page (musl)
if: ${{ matrix.target == 'x86_64-unknown-linux-musl' }}
uses: actions/upload-artifact@v2
with:
name: ouch.1
path: ouch.1
clippy-rustfmt:
name: clippy-rustfmt
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: "Cargo: clippy, fmt"
run: |
rustup toolchain install stable --profile minimal -c clippy
rustup toolchain install nightly --profile minimal -c rustfmt
cargo +stable clippy -- -D warnings
cargo +nightly fmt -- --check

View File

@ -1,276 +0,0 @@
on: [push, pull_request]
name: build-and-test
jobs:
# aarch64-glibc:
# name: Ubuntu 18.04 (for ARMv8 - glibc)
# runs-on: ubuntu-18.04
# steps:
# - uses: actions/checkout@v2
# - uses: actions-rs/toolchain@v1
# with:
# toolchain: stable
# target: aarch64-unknown-linux-gnu
# override: true
# - name: Install binutils-arm-none-eabi
# run: |
# sudo apt-get update
# sudo apt-get install binutils-aarch64-linux-gnu
# - uses: actions-rs/cargo@v1
# with:
# use-cross: true
# command: build
# args: --target=aarch64-unknown-linux-gnu
# - name: Run cargo test
# uses: actions-rs/cargo@v1
# with:
# use-cross: true
# command: test
# args: --target=aarch64-unknown-linux-gnu
# - name: Strip binary
# run: aarch64-linux-gnu-strip target/aarch64-unknown-linux-gnu/release/ouch
# - name: Upload binary
# uses: actions/upload-artifact@v2
# with:
# name: 'ouch-aarch64-linux-gnu'
# path: target/aarch64-unknown-linux-gnu/release/ouch
# armv7-glibc:
# name: Ubuntu 18.04 (for ARMv7 - glibc)
# continue-on-error: true
# runs-on: ubuntu-18.04
# steps:
# - uses: actions/checkout@v2
# - uses: actions-rs/toolchain@v1
# with:
# toolchain: stable
# target: armv7-unknown-linux-gnueabihf
# override: true
# - name: Install binutils-arm-none-eabi
# run: |
# sudo apt-get update
# sudo apt-get install binutils-arm-none-eabi
# - uses: actions-rs/cargo@v1
# with:
# use-cross: true
# command: build
# args: --target=armv7-unknown-linux-gnueabihf
# - name: Run cargo test
# uses: actions-rs/cargo@v1
# with:
# use-cross: true
# command: test
# args: --target=armv7-unknown-linux-gnueabihf
# - name: Strip binary
# run: arm-none-eabi-strip target/armv7-unknown-linux-gnueabihf/release/ouch
# - name: Upload binary
# uses: actions/upload-artifact@v2
# with:
# name: 'ouch-armv7-linux-gnueabihf'
# path: target/armv7-unknown-linux-gnueabihf/release/ouch
x86_64_musl:
name: Ubuntu 20.04 (musl)
runs-on: ubuntu-20.04
strategy:
matrix:
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: x86_64-unknown-linux-musl
override: true
- name: Install dependencies for musl libc
run: |
sudo apt-get update
sudo apt-get install musl-tools
- name: Run cargo build
uses: actions-rs/cargo@v1
with:
command: build
args: --release --target x86_64-unknown-linux-musl
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
command: test
args: --target x86_64-unknown-linux-musl
- name: Strip binary
run: strip target/x86_64-unknown-linux-musl/release/ouch
- name: Upload binary
uses: actions/upload-artifact@v2
with:
name: 'ouch-x86_64-linux-musl'
path: target/x86_64-unknown-linux-musl/release/ouch
x86_64_glibc:
name: Ubuntu 20.04 (glibc)
runs-on: ubuntu-20.04
strategy:
matrix:
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Run cargo build
uses: actions-rs/cargo@v1
with:
command: build
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
command: test
# - name: Strip binary
# run: strip target/release/ouch
# - name: Upload binary
# uses: actions/upload-artifact@v2
# with:
# name: 'ouch-x86_64-linux-gnu'
# path: target/release/ouch
x86_64_macos:
name: macOS (x86_64)
runs-on: macos-latest
strategy:
matrix:
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: x86_64-apple-darwin
override: true
- name: Run cargo build
uses: actions-rs/cargo@v1
with:
command: build
args: --release
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
command: test
- name: Strip binary
run: strip target/release/ouch
- name: Upload binary
uses: actions/upload-artifact@v2
with:
name: 'ouch-x86_64-apple-darwin'
path: target/release/ouch
windows-msvc:
name: Windows Server (MSVC)
runs-on: windows-latest
strategy:
matrix:
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
- name: Run cargo build
uses: actions-rs/cargo@v1
with:
command: build
args: --release
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
command: test
- name: Upload binary
uses: actions/upload-artifact@v2
with:
name: 'ouch-x86_64-pc-windows-msvc'
path: target\release\ouch.exe
# windows-mingw:
# name: Windows Server (MinGW)
# runs-on: windows-2019
# strategy:
# matrix:
# rust:
# - stable
# steps:
# - name: Checkout sources
# uses: actions/checkout@v2
# - name: Install toolchain
# uses: actions-rs/toolchain@v1
# with:
# toolchain: stable
# target: x86_64-pc-windows-gnu
# override: true
# - name: Run cargo build
# uses: actions-rs/cargo@v1
# with:
# command: build
# args: --target x86_64-pc-windows-gnu
# - name: Run cargo test
# uses: actions-rs/cargo@v1
# with:
# command: test
# args: --target x86_64-pc-windows-gnu
# - name: Upload binary
# uses: actions/upload-artifact@v2
# with:
# name: 'ouch-x86_64-pc-windows-gnu'
# path: target\x86_64-pc-windows-gnu\release\ouch.exe

View File

@ -4,12 +4,12 @@ Feel free to open an issue anytime you wish to ask a question, suggest a feature
# Requirements # Requirements
1. Be kind, considerate and respectfull. 1. Be nice to other people.
2. If editing .rs files, run `rustfmt` on them before commiting. 2. If editing the Rust source code, remember to run `rustfmt` (otherwise, CI will warn you the code was not properly formatted).
Note that we are using `unstable` features of `rustfmt`, so you will need to change your toolchain to nightly. Note: we are using `unstable` features of `rustfmt`! Nightly toolchain is required (will likely be installed automatically, cause the toolchain was specified in the project root).
# Suggestions # Suggestions
1. Ask for some guidance before solving an error if you feel like it. 1. If you wish to, you can ask for some guidance before solving an issue.
2. If editing Rust code, run `clippy` before commiting. 2. Run `cargo clippy` too.

17
Cargo.lock generated
View File

@ -114,6 +114,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "clap_generate"
version = "3.0.0-beta.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "097ab5db1c3417442270cd57c8dd39f6c3114d3ce09d595f9efddbb1fcfaa799"
dependencies = [
"clap",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.2.1" version = "1.2.1"
@ -148,6 +157,12 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "fs-err"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ebd3504ad6116843b8375ad70df74e7bfe83cac77a1f3fe73200c844d43bfe0"
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.3" version = "0.2.3"
@ -283,7 +298,9 @@ dependencies = [
"atty", "atty",
"bzip2", "bzip2",
"clap", "clap",
"clap_generate",
"flate2", "flate2",
"fs-err",
"infer", "infer",
"libc", "libc",
"once_cell", "once_cell",

View File

@ -4,7 +4,7 @@ version = "0.2.0"
authors = ["Vinícius Rodrigues Miguel <vrmiguel99@gmail.com>", "João M. Bezerra <marcospb19@hotmail.com>"] authors = ["Vinícius Rodrigues Miguel <vrmiguel99@gmail.com>", "João M. Bezerra <marcospb19@hotmail.com>"]
edition = "2018" edition = "2018"
readme = "README.md" readme = "README.md"
repository = "https://github.com/vrmiguel/ouch" repository = "https://github.com/ouch-org/ouch"
license = "MIT" license = "MIT"
keywords = ["decompression", "compression", "zip", "tar", "gzip"] keywords = ["decompression", "compression", "zip", "tar", "gzip"]
categories = ["command-line-utilities", "compression", "encoding"] categories = ["command-line-utilities", "compression", "encoding"]
@ -15,6 +15,7 @@ description = "A command-line utility for easily compressing and decompressing f
[dependencies] [dependencies]
clap = "=3.0.0-beta.5" # Keep it pinned while in beta! clap = "=3.0.0-beta.5" # Keep it pinned while in beta!
atty = "0.2.14" atty = "0.2.14"
fs-err = "2.6.0"
once_cell = "1.8.0" once_cell = "1.8.0"
walkdir = "2.3.2" walkdir = "2.3.2"
bzip2 = "0.4.3" bzip2 = "0.4.3"
@ -25,6 +26,10 @@ zip = { version = "0.5.13", default-features = false, features = ["defl
flate2 = { version = "1.0.22", default-features = false, features = ["zlib"] } flate2 = { version = "1.0.22", default-features = false, features = ["zlib"] }
zstd = { version = "0.9.0", default-features = false, features = ["thin"] } zstd = { version = "0.9.0", default-features = false, features = ["thin"] }
[build-dependencies]
clap = "=3.0.0-beta.5"
clap_generate = "=3.0.0-beta.5"
[dev-dependencies] [dev-dependencies]
tempfile = "3.2.0" tempfile = "3.2.0"
infer = "0.5.0" infer = "0.5.0"

View File

@ -35,11 +35,11 @@ ouch decompress a.zip b.tar.gz c.tar
ouch d a.zip ouch d a.zip
``` ```
You can redirect the decompression results to another folder with the `-o/--output` flag. You can redirect the decompression results to another folder with the `-d/--dir` flag.
```sh ```sh
# Decompress 'summer_vacation.zip' inside of new folder 'pictures' # Decompress 'summer_vacation.zip' inside of new folder 'pictures'
ouch decompress summer_vacation.zip -o pictures ouch decompress summer_vacation.zip -d pictures
``` ```
### Compressing ### Compressing

22
build.rs Normal file
View File

@ -0,0 +1,22 @@
use clap::{ArgEnum, IntoApp};
use clap_generate::{generate_to, Shell};
use std::{env, fs::create_dir_all, path::Path};
include!("src/opts.rs");
fn main() {
println!("cargo:rerun-if-env-changed=GEN_COMPLETIONS");
if env::var_os("GEN_COMPLETIONS") != Some("1".into()) {
return;
}
let out = &Path::new(&env::var_os("OUT_DIR").unwrap()).join("completions");
create_dir_all(out).unwrap();
let app = &mut Opts::into_app();
for shell in Shell::value_variants() {
generate_to(*shell, app, "ouch", out).unwrap();
}
}

View File

@ -1,2 +1,4 @@
//! Archive compression algorithms
pub mod tar; pub mod tar;
pub mod zip; pub mod zip;

View File

@ -1,23 +1,27 @@
//! Contains Tar-specific building and unpacking functions //! Contains Tar-specific building and unpacking functions
use std::{ use std::{
env, fs, env,
io::prelude::*, io::prelude::*,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use fs_err as fs;
use tar; use tar;
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::{ use crate::{
error::FinalError,
info, info,
utils::{self, Bytes}, utils::{self, Bytes},
QuestionPolicy,
}; };
/// Unpacks the archive given by `archive` into the folder given by `into`.
pub fn unpack_archive( pub fn unpack_archive(
reader: Box<dyn Read>, reader: Box<dyn Read>,
output_folder: &Path, output_folder: &Path,
skip_questions_positively: Option<bool>, question_policy: QuestionPolicy,
) -> crate::Result<Vec<PathBuf>> { ) -> crate::Result<Vec<PathBuf>> {
let mut archive = tar::Archive::new(reader); let mut archive = tar::Archive::new(reader);
@ -26,7 +30,7 @@ pub fn unpack_archive(
let mut file = file?; let mut file = file?;
let file_path = output_folder.join(file.path()?); let file_path = output_folder.join(file.path()?);
if file_path.exists() && !utils::user_wants_to_overwrite(&file_path, skip_questions_positively)? { if file_path.exists() && !utils::user_wants_to_overwrite(&file_path, question_policy)? {
continue; continue;
} }
@ -40,6 +44,7 @@ pub fn unpack_archive(
Ok(files_unpacked) Ok(files_unpacked)
} }
/// Compresses the archives given by `input_filenames` into the file given previously to `writer`.
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, W: Write,
@ -62,7 +67,12 @@ where
builder.append_dir(path, path)?; builder.append_dir(path, path)?;
} else { } else {
let mut file = fs::File::open(path)?; let mut file = fs::File::open(path)?;
builder.append_file(path, &mut file)?; builder.append_file(path, file.file_mut()).map_err(|err| {
FinalError::with_title("Could not create archive")
.detail("Unexpected error while trying to read file")
.detail(format!("Error: {}.", err))
.into_owned()
})?;
} }
} }
env::set_current_dir(previous_location)?; env::set_current_dir(previous_location)?;

View File

@ -1,17 +1,20 @@
//! Contains Zip-specific building and unpacking functions //! Contains Zip-specific building and unpacking functions
use std::{ use std::{
env, fs, env,
io::{self, prelude::*}, io::{self, prelude::*},
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use fs_err as fs;
use walkdir::WalkDir; use walkdir::WalkDir;
use zip::{self, read::ZipFile, ZipArchive}; use zip::{self, read::ZipFile, ZipArchive};
use crate::{ use crate::{
info, info,
utils::{self, dir_is_empty, strip_cur_dir, Bytes}, utils::{self, dir_is_empty, strip_cur_dir, Bytes},
QuestionPolicy,
}; };
use self::utf8::get_invalid_utf8_paths; use self::utf8::get_invalid_utf8_paths;
@ -20,7 +23,7 @@ use self::utf8::get_invalid_utf8_paths;
pub fn unpack_archive<R>( pub fn unpack_archive<R>(
mut archive: ZipArchive<R>, mut archive: ZipArchive<R>,
into: &Path, into: &Path,
skip_questions_positively: Option<bool>, question_policy: QuestionPolicy,
) -> crate::Result<Vec<PathBuf>> ) -> crate::Result<Vec<PathBuf>>
where where
R: Read + Seek, R: Read + Seek,
@ -34,7 +37,7 @@ where
}; };
let file_path = into.join(file_path); let file_path = into.join(file_path);
if file_path.exists() && !utils::user_wants_to_overwrite(&file_path, skip_questions_positively)? { if file_path.exists() && !utils::user_wants_to_overwrite(&file_path, question_policy)? {
continue; continue;
} }
@ -70,6 +73,7 @@ where
Ok(unpacked_files) Ok(unpacked_files)
} }
/// Compresses the archives given by `input_filenames` into the file given previously to `writer`.
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,
@ -126,10 +130,11 @@ fn check_for_comments(file: &ZipFile) {
#[cfg(unix)] #[cfg(unix)]
fn __unix_set_permissions(file_path: &Path, file: &ZipFile) -> crate::Result<()> { fn __unix_set_permissions(file_path: &Path, file: &ZipFile) -> crate::Result<()> {
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
if let Some(mode) = file.unix_mode() { if let Some(mode) = file.unix_mode() {
fs::set_permissions(file_path, fs::Permissions::from_mode(mode))?; fs::set_permissions(file_path, Permissions::from_mode(mode))?;
} }
Ok(()) Ok(())

View File

@ -1,70 +1,32 @@
//! CLI arg parser configuration, command detection and input treatment. //! CLI configuration step, uses definitions from `opts.rs`.
//!
//! Also used to treat some inputs.
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
vec::Vec, vec::Vec,
}; };
use clap::{Parser, ValueHint}; use clap::Parser;
use fs_err as fs;
use crate::Error; use crate::{Error, Opts, QuestionPolicy, Subcommand};
#[derive(Parser, Debug)]
#[clap(version, about)]
pub struct Opts {
/// Skip overwrite questions positively.
#[clap(short, long, conflicts_with = "no")]
pub yes: bool,
/// Skip overwrite questions negatively.
#[clap(short, long)]
pub no: bool,
#[clap(subcommand)]
pub cmd: Subcommand,
}
#[derive(Parser, PartialEq, Eq, Debug)]
pub enum Subcommand {
/// Compress files. Alias: c
#[clap(alias = "c")]
Compress {
/// Files to be compressed
#[clap(required = true, min_values = 1)]
files: Vec<PathBuf>,
/// The resulting file. Its extensions specify how the files will be compressed and they need to be supported
#[clap(required = true, value_hint = ValueHint::FilePath)]
output: PathBuf,
},
/// Compress files. Alias: d
#[clap(alias = "d")]
Decompress {
/// Files to be decompressed
#[clap(required = true, min_values = 1)]
files: Vec<PathBuf>,
/// Decompress files in a directory other than the current
#[clap(short, long, value_hint = ValueHint::DirPath)]
output: Option<PathBuf>,
},
}
impl Opts { impl Opts {
/// A helper method that calls `clap::Parser::parse` and then translates relative paths to absolute. /// A helper method that calls `clap::Parser::parse` and then translates relative paths to absolute.
/// Also determines if the user wants to skip questions or not /// Also determines if the user wants to skip questions or not
pub fn parse_args() -> crate::Result<(Self, Option<bool>)> { 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, .. }) = &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 {
Some(true) QuestionPolicy::AlwaysYes
} else if opts.no { } else if opts.no {
Some(false) QuestionPolicy::AlwaysNo
} else { } else {
None QuestionPolicy::Ask
}; };
Ok((opts, skip_questions_positively)) Ok((opts, skip_questions_positively))
@ -72,7 +34,7 @@ impl Opts {
} }
fn canonicalize(path: impl AsRef<Path>) -> crate::Result<PathBuf> { fn canonicalize(path: impl AsRef<Path>) -> crate::Result<PathBuf> {
match std::fs::canonicalize(&path.as_ref()) { match fs::canonicalize(&path.as_ref()) {
Ok(abs_path) => Ok(abs_path), Ok(abs_path) => Ok(abs_path),
Err(io_err) => { Err(io_err) => {
if !path.as_ref().exists() { if !path.as_ref().exists() {

View File

@ -3,16 +3,15 @@
//! Also, where correctly call functions based on the detected `Command`. //! Also, where correctly call functions based on the detected `Command`.
use std::{ use std::{
fs,
io::{self, BufReader, BufWriter, Read, Write}, io::{self, BufReader, BufWriter, Read, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use fs_err as fs;
use utils::colors; use utils::colors;
use crate::{ use crate::{
archive, archive,
cli::{Opts, Subcommand},
error::FinalError, error::FinalError,
extension::{ extension::{
self, self,
@ -20,10 +19,8 @@ use crate::{
Extension, Extension,
}, },
info, info,
utils::nice_directory_display, utils::{self, dir_is_empty, nice_directory_display, to_utf},
utils::to_utf, Error, Opts, QuestionPolicy, Subcommand,
utils::{self, dir_is_empty},
Error,
}; };
// Used in BufReader and BufWriter to perform less syscalls // Used in BufReader and BufWriter to perform less syscalls
@ -39,7 +36,9 @@ fn represents_several_files(files: &[PathBuf]) -> bool {
files.iter().any(is_non_empty_dir) || files.len() > 1 files.iter().any(is_non_empty_dir) || files.len() > 1
} }
pub fn run(args: Opts, skip_questions_positively: Option<bool>) -> crate::Result<()> { /// Entrypoint of ouch, receives cli options and matches Subcommand
/// to decide current operation
pub fn run(args: Opts, question_policy: QuestionPolicy) -> crate::Result<()> {
match args.cmd { match args.cmd {
Subcommand::Compress { files, output: output_path } => { Subcommand::Compress { files, output: output_path } => {
// Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma] // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
@ -95,7 +94,7 @@ pub fn run(args: Opts, skip_questions_positively: Option<bool>) -> crate::Result
return Err(Error::with_reason(reason)); return Err(Error::with_reason(reason));
} }
if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, skip_questions_positively)? { if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, question_policy)? {
// User does not want to overwrite this file // User does not want to overwrite this file
return Ok(()); return Ok(());
} }
@ -160,7 +159,7 @@ pub fn run(args: Opts, skip_questions_positively: Option<bool>) -> crate::Result
compress_result?; compress_result?;
} }
Subcommand::Decompress { files, output: output_folder } => { Subcommand::Decompress { files, output_dir } => {
let mut output_paths = vec![]; let mut output_paths = vec![];
let mut formats = vec![]; let mut formats = vec![];
@ -189,16 +188,21 @@ pub fn run(args: Opts, skip_questions_positively: Option<bool>) -> crate::Result
} }
// From Option<PathBuf> to Option<&Path> // From Option<PathBuf> to Option<&Path>
let output_folder = output_folder.as_ref().map(|path| path.as_ref()); let output_dir = output_dir.as_ref().map(|path| path.as_ref());
for ((input_path, formats), file_name) in files.iter().zip(formats).zip(output_paths) { for ((input_path, formats), file_name) in files.iter().zip(formats).zip(output_paths) {
decompress_file(input_path, formats, output_folder, file_name, skip_questions_positively)?; decompress_file(input_path, formats, output_dir, file_name, question_policy)?;
} }
} }
} }
Ok(()) Ok(())
} }
// Compress files into an `output_file`
//
// files are the list of paths to be compressed: ["dir/file1.txt", "dir/file2.txt"]
// formats contains each format necessary for compression, example: [Tar, Gz] (in compression order)
// output_file is the resulting compressed file name, example: "compressed.tar.gz"
fn compress_files(files: Vec<PathBuf>, formats: Vec<Extension>, output_file: fs::File) -> crate::Result<()> { fn compress_files(files: Vec<PathBuf>, formats: Vec<Extension>, output_file: fs::File) -> crate::Result<()> {
let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file); let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file);
@ -256,26 +260,28 @@ fn compress_files(files: Vec<PathBuf>, formats: Vec<Extension>, output_file: fs:
Ok(()) Ok(())
} }
// Decompress a file
//
// File at input_file_path is opened for reading, example: "archive.tar.gz" // 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) // formats contains each format necessary for decompression, example: [Gz, Tar] (in decompression order)
// output_folder it's where the file will be decompressed to // output_dir it's where the file will be decompressed to
// file_name is only used when extracting single file formats, no archive formats like .tar or .zip // file_name is only used when extracting single file formats, no archive formats like .tar or .zip
fn decompress_file( fn decompress_file(
input_file_path: &Path, input_file_path: &Path,
formats: Vec<Extension>, formats: Vec<Extension>,
output_folder: Option<&Path>, output_dir: Option<&Path>,
file_name: &Path, file_name: &Path,
skip_questions_positively: Option<bool>, question_policy: QuestionPolicy,
) -> crate::Result<()> { ) -> crate::Result<()> {
// TODO: improve error message // TODO: improve error message
let reader = fs::File::open(&input_file_path)?; let reader = fs::File::open(&input_file_path)?;
// Output path is used by single file formats // Output path is used by single file formats
let output_path = let output_path =
if let Some(output_folder) = output_folder { output_folder.join(file_name) } else { file_name.to_path_buf() }; if let Some(output_dir) = output_dir { output_dir.join(file_name) } else { file_name.to_path_buf() };
// Output folder is used by archive file formats (zip and tar) // Output folder is used by archive file formats (zip and tar)
let output_folder = output_folder.unwrap_or_else(|| Path::new(".")); let output_dir = output_dir.unwrap_or_else(|| Path::new("."));
// Zip archives are special, because they require io::Seek, so it requires it's logic separated // Zip archives are special, because they require io::Seek, so it requires it's logic separated
// from decoder chaining. // from decoder chaining.
@ -285,10 +291,10 @@ fn decompress_file(
// //
// Any other Zip decompression done can take up the whole RAM and freeze ouch. // Any other Zip decompression done can take up the whole RAM and freeze ouch.
if formats.len() == 1 && *formats[0].compression_formats.as_slice() == [Zip] { if formats.len() == 1 && *formats[0].compression_formats.as_slice() == [Zip] {
utils::create_dir_if_non_existent(output_folder)?; utils::create_dir_if_non_existent(output_dir)?;
let zip_archive = zip::ZipArchive::new(reader)?; let zip_archive = zip::ZipArchive::new(reader)?;
let _files = crate::archive::zip::unpack_archive(zip_archive, output_folder, skip_questions_positively)?; let _files = crate::archive::zip::unpack_archive(zip_archive, output_dir, question_policy)?;
info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder)); info!("Successfully decompressed archive in {}.", nice_directory_display(output_dir));
return Ok(()); return Ok(());
} }
@ -312,7 +318,9 @@ fn decompress_file(
reader = chain_reader_decoder(format, reader)?; reader = chain_reader_decoder(format, reader)?;
} }
utils::create_dir_if_non_existent(output_folder)?; utils::create_dir_if_non_existent(output_dir)?;
let files_unpacked;
match formats[0].compression_formats[0] { match formats[0].compression_formats[0] {
Gzip | Bzip | Lzma | Zstd => { Gzip | Bzip | Lzma | Zstd => {
@ -322,11 +330,10 @@ fn decompress_file(
let mut writer = fs::File::create(&output_path)?; let mut writer = fs::File::create(&output_path)?;
io::copy(&mut reader, &mut writer)?; io::copy(&mut reader, &mut writer)?;
info!("Successfully decompressed archive in {}.", nice_directory_display(output_path)); files_unpacked = vec![output_path];
} }
Tar => { Tar => {
let _ = crate::archive::tar::unpack_archive(reader, output_folder, skip_questions_positively)?; files_unpacked = crate::archive::tar::unpack_archive(reader, output_dir, question_policy)?;
info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder));
} }
Zip => { Zip => {
eprintln!("Compressing first into .zip."); eprintln!("Compressing first into .zip.");
@ -340,11 +347,12 @@ fn decompress_file(
io::copy(&mut reader, &mut vec)?; io::copy(&mut reader, &mut vec)?;
let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?; let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
let _ = crate::archive::zip::unpack_archive(zip_archive, output_folder, skip_questions_positively)?; files_unpacked = crate::archive::zip::unpack_archive(zip_archive, output_dir, question_policy)?;
info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder));
} }
} }
info!("Successfully decompressed archive in {}.", nice_directory_display(output_dir));
info!("Files unpacked: {}", files_unpacked.len());
Ok(()) Ok(())
} }

View File

@ -12,15 +12,22 @@ use crate::utils::colors;
/// Represents a confirmation dialog /// Represents a confirmation dialog
pub struct Confirmation<'a> { pub struct Confirmation<'a> {
/// Represents the message to the displayed
/// e.g.: "Do you want to overwrite 'FILE'?"
pub prompt: &'a str, pub prompt: &'a str,
/// Represents a placeholder to be changed at runtime
/// e.g.: Some("FILE")
pub placeholder: Option<&'a str>, pub placeholder: Option<&'a str>,
} }
impl<'a> Confirmation<'a> { impl<'a> Confirmation<'a> {
/// New Confirmation
pub const fn new(prompt: &'a str, pattern: Option<&'a str>) -> Self { pub const fn new(prompt: &'a str, pattern: Option<&'a str>) -> Self {
Self { prompt, placeholder: pattern } Self { prompt, placeholder: pattern }
} }
/// Creates user message and receives a boolean input to be used on the program
pub fn ask(&self, substitute: Option<&'a str>) -> crate::Result<bool> { pub fn ask(&self, substitute: Option<&'a str>) -> crate::Result<bool> {
let message = match (self.placeholder, substitute) { let message = match (self.placeholder, substitute) {
(None, _) => Cow::Borrowed(self.prompt), (None, _) => Cow::Borrowed(self.prompt),

View File

@ -6,6 +6,8 @@
//! TODO: wrap `FinalError` in a variant to keep all `FinalError::display_and_crash()` function //! TODO: wrap `FinalError` in a variant to keep all `FinalError::display_and_crash()` function
//! calls inside of this module. //! calls inside of this module.
#![allow(missing_docs)]
use std::{ use std::{
fmt::{self, Display}, fmt::{self, Display},
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -13,6 +15,7 @@ use std::{
use crate::utils::colors::*; use crate::utils::colors::*;
/// Custom Ouch Errors
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum Error { pub enum Error {
UnknownExtensionError(String), UnknownExtensionError(String),
@ -21,7 +24,7 @@ pub enum Error {
FileNotFound(PathBuf), FileNotFound(PathBuf),
AlreadyExists, AlreadyExists,
InvalidZipArchive(&'static str), InvalidZipArchive(&'static str),
PermissionDenied, PermissionDenied { error_title: String },
UnsupportedZipArchive(&'static str), UnsupportedZipArchive(&'static str),
InternalError, InternalError,
CompressingRootFolder, CompressingRootFolder,
@ -78,6 +81,10 @@ impl FinalError {
self.hints.push(hint.to_string()); self.hints.push(hint.to_string());
self self
} }
pub fn into_owned(&mut self) -> Self {
std::mem::take(self)
}
} }
impl fmt::Display for Error { impl fmt::Display for Error {
@ -87,7 +94,7 @@ impl fmt::Display for Error {
FinalError::with_title(format!("Cannot compress to {:?}", filename)) FinalError::with_title(format!("Cannot compress to {:?}", filename))
.detail("Ouch could not detect the compression format") .detail("Ouch could not detect the compression format")
.hint("Use a supported format extension, like '.zip' or '.tar.gz'") .hint("Use a supported format extension, like '.zip' or '.tar.gz'")
.hint("Check https://github.com/vrmiguel/ouch for a full list of supported formats") .hint("Check https://github.com/ouch-org/ouch for a full list of supported formats")
} }
Error::WalkdirError { reason } => FinalError::with_title(reason), Error::WalkdirError { reason } => FinalError::with_title(reason),
Error::FileNotFound(file) => { Error::FileNotFound(file) => {
@ -124,7 +131,7 @@ impl fmt::Display for Error {
.detail("This should not have happened") .detail("This should not have happened")
.detail("It's probably our fault") .detail("It's probably our fault")
.detail("Please help us improve by reporting the issue at:") .detail("Please help us improve by reporting the issue at:")
.detail(format!(" {}https://github.com/vrmiguel/ouch/issues ", *CYAN)) .detail(format!(" {}https://github.com/ouch-org/ouch/issues ", *CYAN))
} }
Error::IoError { reason } => FinalError::with_title(reason), Error::IoError { reason } => FinalError::with_title(reason),
Error::CompressionTypo => { Error::CompressionTypo => {
@ -134,7 +141,7 @@ impl fmt::Display for Error {
Error::UnknownExtensionError(_) => todo!(), Error::UnknownExtensionError(_) => todo!(),
Error::AlreadyExists => todo!(), Error::AlreadyExists => todo!(),
Error::InvalidZipArchive(_) => todo!(), Error::InvalidZipArchive(_) => todo!(),
Error::PermissionDenied => todo!(), Error::PermissionDenied { error_title } => FinalError::with_title(error_title).detail("Permission denied"),
Error::UnsupportedZipArchive(_) => todo!(), Error::UnsupportedZipArchive(_) => todo!(),
Error::Custom { reason } => reason.clone(), Error::Custom { reason } => reason.clone(),
}; };
@ -152,8 +159,8 @@ impl Error {
impl From<std::io::Error> for Error { impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self { fn from(err: std::io::Error) -> Self {
match err.kind() { match err.kind() {
std::io::ErrorKind::NotFound => panic!("{}", err), std::io::ErrorKind::NotFound => todo!(),
std::io::ErrorKind::PermissionDenied => Self::PermissionDenied, std::io::ErrorKind::PermissionDenied => Self::PermissionDenied { error_title: err.to_string() },
std::io::ErrorKind::AlreadyExists => Self::AlreadyExists, std::io::ErrorKind::AlreadyExists => Self::AlreadyExists,
_other => Self::IoError { reason: err.to_string() }, _other => Self::IoError { reason: err.to_string() },
} }
@ -177,3 +184,9 @@ impl From<walkdir::Error> for Error {
Self::WalkdirError { reason: err.to_string() } Self::WalkdirError { reason: err.to_string() }
} }
} }
impl From<FinalError> for Error {
fn from(err: FinalError) -> Self {
Self::Custom { reason: err }
}
}

View File

@ -37,6 +37,7 @@ impl fmt::Display for Extension {
} }
} }
#[allow(missing_docs)]
#[derive(Copy, Clone, PartialEq, Eq, Debug)] #[derive(Copy, Clone, PartialEq, Eq, Debug)]
/// Accepted extensions for input and output /// Accepted extensions for input and output
pub enum CompressionFormat { pub enum CompressionFormat {
@ -49,6 +50,7 @@ pub enum CompressionFormat {
} }
impl CompressionFormat { impl CompressionFormat {
/// Currently supported archive formats are .tar (and aliases to it) and .zip
pub fn is_archive_format(&self) -> bool { pub fn is_archive_format(&self) -> bool {
// Keep this match like that without a wildcard `_` so we don't forget to update it // Keep this match like that without a wildcard `_` so we don't forget to update it
match self { match self {
@ -78,6 +80,19 @@ impl fmt::Display for CompressionFormat {
} }
} }
// use crate::extension::CompressionFormat::*;
//
/// Extracts extensions from a path,
/// return both the remaining path and the list of extension objects
///
/// ```rust
/// use ouch::extension::{separate_known_extensions_from_name, CompressionFormat};
/// use std::path::Path;
///
/// let mut path = Path::new("bolovo.tar.gz");
/// assert_eq!(separate_known_extensions_from_name(&path), (Path::new("bolovo"), vec![CompressionFormat::Tar, CompressionFormat::Gzip]));
/// ```
pub fn separate_known_extensions_from_name(mut path: &Path) -> (&Path, Vec<Extension>) { pub fn separate_known_extensions_from_name(mut path: &Path) -> (&Path, Vec<Extension>) {
// // TODO: check for file names with the name of an extension // // TODO: check for file names with the name of an extension
// // TODO2: warn the user that currently .tar.gz is a .gz file named .tar // // TODO2: warn the user that currently .tar.gz is a .gz file named .tar
@ -114,6 +129,15 @@ pub fn separate_known_extensions_from_name(mut path: &Path) -> (&Path, Vec<Exten
(path, extensions) (path, extensions)
} }
/// Extracts extensions from a path, return only the list of extension objects
///
/// ```rust
/// use ouch::extension::{extensions_from_path, CompressionFormat};
/// use std::path::Path;
///
/// let mut path = Path::new("bolovo.tar.gz");
/// assert_eq!(extensions_from_path(&path), vec![CompressionFormat::Tar, CompressionFormat::Gzip]);
/// ```
pub fn extensions_from_path(path: &Path) -> Vec<Extension> { pub fn extensions_from_path(path: &Path) -> Vec<Extension> {
let (_, extensions) = separate_known_extensions_from_name(path); let (_, extensions) = separate_known_extensions_from_name(path);
extensions extensions

View File

@ -4,19 +4,25 @@
//! 1. It's required by `main.rs`, or //! 1. It's required by `main.rs`, or
//! 2. It's required by some integration tests at tests/ folder. //! 2. It's required by some integration tests at tests/ folder.
// Public modules #![warn(missing_docs)]
// Macros should be declared before
pub mod macros;
pub mod archive;
pub mod cli; pub mod cli;
pub mod commands; pub mod commands;
pub mod dialogs;
pub mod error;
pub mod extension;
pub mod utils;
// Private modules /// CLI configuration step, uses definitions from `opts.rs`, also used to treat some inputs.
pub mod archive; pub mod opts;
mod dialogs;
mod error;
mod extension;
mod macros;
mod utils;
pub use error::{Error, Result}; pub use error::{Error, Result};
pub use opts::{Opts, Subcommand};
pub use utils::QuestionPolicy;
/// The status code ouch has when an error is encountered /// The status code ouch has when an error is encountered
pub const EXIT_FAILURE: i32 = libc::EXIT_FAILURE; pub const EXIT_FAILURE: i32 = libc::EXIT_FAILURE;

View File

@ -1,3 +1,6 @@
//! Macros used on ouch.
/// Macro that prints message in INFO mode
#[macro_export] #[macro_export]
macro_rules! info { macro_rules! info {
($($arg:tt)*) => { ($($arg:tt)*) => {
@ -6,6 +9,7 @@ macro_rules! info {
}; };
} }
/// Prints the `[Info]` tag
pub fn _info_helper() { pub fn _info_helper() {
use crate::utils::colors::{RESET, YELLOW}; use crate::utils::colors::{RESET, YELLOW};

View File

@ -1,4 +1,4 @@
use ouch::{cli::Opts, commands, Result}; use ouch::{commands, Opts, Result};
fn main() { fn main() {
if let Err(err) = run() { if let Err(err) = run() {

47
src/opts.rs Normal file
View File

@ -0,0 +1,47 @@
use clap::{Parser, ValueHint};
use std::path::PathBuf;
/// Command line options
#[derive(Parser, Debug)]
#[clap(version, about)]
pub struct Opts {
/// Skip overwrite questions positively.
#[clap(short, long, conflicts_with = "no")]
pub yes: bool,
/// Skip overwrite questions negatively.
#[clap(short, long)]
pub no: bool,
/// Action to take
#[clap(subcommand)]
pub cmd: Subcommand,
}
/// Actions to take
#[derive(Parser, PartialEq, Eq, Debug)]
pub enum Subcommand {
/// Compress files. Alias: c
#[clap(alias = "c")]
Compress {
/// Files to be compressed
#[clap(required = true, min_values = 1)]
files: Vec<PathBuf>,
/// The resulting file. Its extensions specify how the files will be compressed and they need to be supported
#[clap(required = true, value_hint = ValueHint::FilePath)]
output: PathBuf,
},
/// Compress files. Alias: d
#[clap(alias = "d")]
Decompress {
/// Files to be decompressed
#[clap(required = true, min_values = 1)]
files: Vec<PathBuf>,
/// Decompress files in a directory other than the current
#[clap(short, long = "dir", value_hint = ValueHint::DirPath)]
output_dir: Option<PathBuf>,
},
}

View File

@ -1,20 +1,24 @@
//! Utils used on ouch.
use std::{ use std::{
cmp, env, cmp, env,
ffi::OsStr, ffi::OsStr,
fs::{self, ReadDir},
path::Component, path::Component,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use fs_err as fs;
use crate::{dialogs::Confirmation, info}; use crate::{dialogs::Confirmation, info};
/// Checks if the given path represents an empty directory. /// Checks if the given path represents an empty directory.
pub fn dir_is_empty(dir_path: &Path) -> bool { pub fn dir_is_empty(dir_path: &Path) -> bool {
let is_empty = |mut rd: ReadDir| rd.next().is_none(); let is_empty = |mut rd: std::fs::ReadDir| rd.next().is_none();
dir_path.read_dir().map(is_empty).unwrap_or_default() dir_path.read_dir().map(is_empty).unwrap_or_default()
} }
/// Creates the dir if non existent.
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() {
fs::create_dir_all(path)?; fs::create_dir_all(path)?;
@ -23,6 +27,9 @@ pub fn create_dir_if_non_existent(path: &Path) -> crate::Result<()> {
Ok(()) Ok(())
} }
/// Removes the current dir from the beginning of a path
/// normally used for presentation sake.
/// If this function fails, it will return source path as a PathBuf.
pub fn strip_cur_dir(source_path: &Path) -> PathBuf { pub fn strip_cur_dir(source_path: &Path) -> PathBuf {
source_path source_path
.strip_prefix(Component::CurDir) .strip_prefix(Component::CurDir)
@ -43,11 +50,13 @@ pub fn cd_into_same_dir_as(filename: &Path) -> crate::Result<PathBuf> {
Ok(previous_location) Ok(previous_location)
} }
pub fn user_wants_to_overwrite(path: &Path, skip_questions_positively: Option<bool>) -> crate::Result<bool> { /// Centralizes the decision of overwriting a file or not,
match skip_questions_positively { /// whether the user has already passed a question_policy or not.
Some(true) => Ok(true), pub fn user_wants_to_overwrite(path: &Path, question_policy: QuestionPolicy) -> crate::Result<bool> {
Some(false) => Ok(false), match question_policy {
None => { QuestionPolicy::AlwaysYes => Ok(true),
QuestionPolicy::AlwaysNo => Ok(false),
QuestionPolicy::Ask => {
let path = to_utf(strip_cur_dir(path)); let path = to_utf(strip_cur_dir(path));
let path = Some(path.as_str()); let path = Some(path.as_str());
let placeholder = Some("FILE"); let placeholder = Some("FILE");
@ -56,11 +65,13 @@ pub fn user_wants_to_overwrite(path: &Path, skip_questions_positively: Option<bo
} }
} }
/// Converts an OsStr to utf8.
pub fn to_utf(os_str: impl AsRef<OsStr>) -> String { pub fn to_utf(os_str: impl AsRef<OsStr>) -> String {
let text = format!("{:?}", os_str.as_ref()); let text = format!("{:?}", os_str.as_ref());
text.trim_matches('"').to_string() text.trim_matches('"').to_string()
} }
/// Treats weird paths for better user messages.
pub fn nice_directory_display(os_str: impl AsRef<OsStr>) -> String { pub fn nice_directory_display(os_str: impl AsRef<OsStr>) -> String {
let text = to_utf(os_str); let text = to_utf(os_str);
if text == "." { if text == "." {
@ -70,6 +81,7 @@ pub fn nice_directory_display(os_str: impl AsRef<OsStr>) -> String {
} }
} }
/// Struct used to overload functionality onto Byte presentation.
pub struct Bytes { pub struct Bytes {
bytes: f64, bytes: f64,
} }
@ -86,6 +98,7 @@ pub mod colors {
macro_rules! color { macro_rules! color {
($name:ident = $value:literal) => { ($name:ident = $value:literal) => {
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
/// Inserts color onto text based on configuration
pub static $name: Lazy<&str> = Lazy::new(|| if *DISABLE_COLORED_TEXT { "" } else { $value }); pub static $name: Lazy<&str> = Lazy::new(|| if *DISABLE_COLORED_TEXT { "" } else { $value });
#[cfg(not(target_family = "unix"))] #[cfg(not(target_family = "unix"))]
pub static $name: &&str = &""; pub static $name: &&str = &"";
@ -106,6 +119,7 @@ pub mod colors {
impl Bytes { impl Bytes {
const UNIT_PREFIXES: [&'static str; 6] = ["", "k", "M", "G", "T", "P"]; const UNIT_PREFIXES: [&'static str; 6] = ["", "k", "M", "G", "T", "P"];
/// New Byte structure
pub fn new(bytes: u64) -> Self { pub fn new(bytes: u64) -> Self {
Self { bytes: bytes as f64 } Self { bytes: bytes as f64 }
} }
@ -126,6 +140,17 @@ impl std::fmt::Display for Bytes {
} }
} }
#[derive(Debug, PartialEq, Clone, Copy)]
/// How overwrite questions should be handled
pub enum QuestionPolicy {
/// Ask everytime
Ask,
/// Skip overwrite questions positively
AlwaysYes,
/// Skip overwrite questions negatively
AlwaysNo,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -1,16 +1,15 @@
mod utils; mod utils;
use std::{ use std::{
env, fs, env,
io::prelude::*, io::prelude::*,
path::{Path, PathBuf}, path::{Path, PathBuf},
time::Duration, time::Duration,
}; };
use ouch::{ use ouch::{commands::run, Opts, QuestionPolicy, Subcommand};
cli::{Opts, Subcommand},
commands::run, use fs_err as fs;
};
use rand::{rngs::SmallRng, RngCore, SeedableRng}; use rand::{rngs::SmallRng, RngCore, SeedableRng};
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use utils::*; use utils::*;
@ -180,10 +179,10 @@ fn extract_files(archive_path: &Path) -> Vec<PathBuf> {
no: false, no: false,
cmd: Subcommand::Decompress { cmd: Subcommand::Decompress {
files: vec![archive_path.to_owned()], files: vec![archive_path.to_owned()],
output: Some(extraction_output_folder.clone()), output_dir: Some(extraction_output_folder.clone()),
}, },
}; };
run(command, None).expect("Failed to extract"); run(command, QuestionPolicy::Ask).expect("Failed to extract");
fs::read_dir(extraction_output_folder).unwrap().map(Result::unwrap).map(|entry| entry.path()).collect() fs::read_dir(extraction_output_folder).unwrap().map(Result::unwrap).map(|entry| entry.path()).collect()
} }

View File

@ -2,15 +2,11 @@
#![allow(dead_code)] #![allow(dead_code)]
use std::{ use std::path::{Path, PathBuf};
fs,
path::{Path, PathBuf},
};
use ouch::{ use fs_err as fs;
cli::{Opts, Subcommand},
commands::run, use ouch::{commands::run, Opts, QuestionPolicy, Subcommand};
};
pub fn create_empty_dir(at: &Path, filename: &str) -> PathBuf { pub fn create_empty_dir(at: &Path, filename: &str) -> PathBuf {
let dirname = Path::new(filename); let dirname = Path::new(filename);
@ -30,7 +26,7 @@ pub fn compress_files(at: &Path, paths_to_compress: &[PathBuf], format: &str) ->
no: false, no: false,
cmd: Subcommand::Compress { files: paths_to_compress.to_vec(), output: archive_path.clone() }, cmd: Subcommand::Compress { files: paths_to_compress.to_vec(), output: archive_path.clone() },
}; };
run(command, None).expect("Failed to compress test dummy files"); run(command, QuestionPolicy::Ask).expect("Failed to compress test dummy files");
archive_path archive_path
} }
@ -52,10 +48,10 @@ pub fn extract_files(archive_path: &Path) -> Vec<PathBuf> {
no: false, no: false,
cmd: Subcommand::Decompress { cmd: Subcommand::Decompress {
files: vec![archive_path.to_owned()], files: vec![archive_path.to_owned()],
output: Some(extraction_output_folder.clone()), output_dir: Some(extraction_output_folder.clone()),
}, },
}; };
run(command, None).expect("Failed to extract"); run(command, QuestionPolicy::Ask).expect("Failed to extract");
fs::read_dir(extraction_output_folder).unwrap().map(Result::unwrap).map(|entry| entry.path()).collect() fs::read_dir(extraction_output_folder).unwrap().map(Result::unwrap).map(|entry| entry.path()).collect()
} }