diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index 99ac20ea2..fd150a8af 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -23,6 +23,7 @@ fsext getopts getrandom globset +indicatif itertools lscolors mdbook diff --git a/Cargo.lock b/Cargo.lock index f91f6e62a..ae5096f86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -281,7 +281,7 @@ dependencies = [ "once_cell", "strsim", "termcolor", - "terminal_size", + "terminal_size 0.2.2", ] [[package]] @@ -327,6 +327,20 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "120133d4db2ec47efe2e26502ee984747630c67f51974fca0b6c1340cf2368d3" +[[package]] +name = "console" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "terminal_size 0.1.17", + "unicode-width", + "winapi", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -791,6 +805,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "env_logger" version = "0.8.4" @@ -1047,6 +1067,17 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indicatif" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc42b206e70d86ec03285b123e65a5458c92027d1fb2ae3555878b8113b3ddf" +dependencies = [ + "console", + "number_prefix", + "unicode-width", +] + [[package]] name = "inotify" version = "0.9.6" @@ -2033,6 +2064,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "terminal_size" version = "0.2.2" @@ -2050,7 +2091,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" dependencies = [ "smawk", - "terminal_size", + "terminal_size 0.2.2", "unicode-linebreak", "unicode-width", ] @@ -2293,6 +2334,7 @@ dependencies = [ "clap 4.0.22", "exacl", "filetime", + "indicatif", "libc", "quick-error", "selinux", @@ -2598,7 +2640,7 @@ dependencies = [ "once_cell", "selinux", "term_grid", - "terminal_size", + "terminal_size 0.2.2", "unicode-width", "uucore", ] diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index b54e68464..189ef1609 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -26,6 +26,7 @@ quick-error = "2.0.1" selinux = { version="0.3", optional=true } uucore = { version=">=0.0.16", package="uucore", path="../../uucore", features=["entries", "fs", "perms", "mode"] } walkdir = "2.2" +indicatif = "0.17" [target.'cfg(unix)'.dependencies] xattr="0.2.3" diff --git a/src/uu/cp/src/copydir.rs b/src/uu/cp/src/copydir.rs index 3527e4827..23e7b52e5 100644 --- a/src/uu/cp/src/copydir.rs +++ b/src/uu/cp/src/copydir.rs @@ -14,6 +14,7 @@ use std::fs; use std::io; use std::path::{Path, PathBuf, StripPrefixError}; +use indicatif::ProgressBar; use uucore::display::Quotable; use uucore::error::UIoError; use uucore::fs::{canonicalize, FileInformation, MissingHandling, ResolveMode}; @@ -170,6 +171,7 @@ impl Entry { /// Copy a single entry during a directory traversal. fn copy_direntry( + progress_bar: &Option, entry: Entry, options: &Options, symlinked_files: &mut HashSet, @@ -213,6 +215,7 @@ fn copy_direntry( preserve_hardlinks(hard_links, &source_absolute, &dest, &mut found_hard_link)?; if !found_hard_link { match copy_file( + progress_bar, &source_absolute, local_to_target.as_path(), options, @@ -240,6 +243,7 @@ fn copy_direntry( // TODO What other kinds of errors, if any, should // cause us to continue walking the directory? match copy_file( + progress_bar, &source_absolute, local_to_target.as_path(), options, @@ -272,6 +276,7 @@ fn copy_direntry( /// Any errors encountered copying files in the tree will be logged but /// will not cause a short-circuit. pub(crate) fn copy_directory( + progress_bar: &Option, root: &Path, target: &TargetSlice, options: &Options, @@ -285,6 +290,7 @@ pub(crate) fn copy_directory( // if no-dereference is enabled and this is a symlink, copy it as a file if !options.dereference(source_in_command_line) && root.is_symlink() { return copy_file( + progress_bar, root, target, options, @@ -344,6 +350,7 @@ pub(crate) fn copy_directory( Ok(direntry) => { let entry = Entry::new(&context, &direntry)?; copy_direntry( + progress_bar, entry, options, symlinked_files, diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 4f3917262..f2e4ae211 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -9,7 +9,7 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -// spell-checker:ignore (ToDO) copydir ficlone ftruncate linkgs lstat nlink nlinks pathbuf pwrite reflink strs xattrs symlinked fiemap +// spell-checker:ignore (ToDO) copydir ficlone fiemap ftruncate linkgs lstat nlink nlinks pathbuf pwrite reflink strs xattrs symlinked deduplicated advcpmv #[macro_use] extern crate quick_error; @@ -33,6 +33,7 @@ use std::string::ToString; use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use filetime::FileTime; +use indicatif::{ProgressBar, ProgressStyle}; #[cfg(unix)] use libc::mkfifo; use quick_error::ResultExt; @@ -214,6 +215,7 @@ pub struct Options { target_dir: Option, update: bool, verbose: bool, + progress_bar: bool, } static ABOUT: &str = "Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY."; @@ -244,6 +246,7 @@ mod options { pub const PARENT: &str = "parent"; pub const PARENTS: &str = "parents"; pub const PATHS: &str = "paths"; + pub const PROGRESS_BAR: &str = "progress"; pub const PRESERVE: &str = "preserve"; pub const PRESERVE_DEFAULT_ATTRIBUTES: &str = "preserve-default-attributes"; pub const RECURSIVE: &str = "recursive"; @@ -538,6 +541,18 @@ pub fn uu_app() -> Command { ), ) // END TODO + .arg( + // The 'g' short flag is modeled after advcpmv + // See this repo: https://github.com/jarun/advcpmv + Arg::new(options::PROGRESS_BAR) + .long(options::PROGRESS_BAR) + .short('g') + .action(clap::ArgAction::SetTrue) + .help( + "Display a progress bar. \n\ + Note: this feature is not supported by GNU coreutils.", + ), + ) .arg( Arg::new(options::PATHS) .action(ArgAction::Append) @@ -817,6 +832,7 @@ impl Options { preserve_attributes, recursive, target_dir, + progress_bar: matches.get_flag(options::PROGRESS_BAR), }; Ok(options) @@ -935,7 +951,7 @@ fn preserve_hardlinks( /// `Err(Error::NotAllFilesCopied)` if at least one non-fatal error was /// encountered. /// -/// Behavior depends on `options`, see [`Options`] for details. +/// Behavior depends on path`options`, see [`Options`] for details. /// /// [`Options`]: ./struct.Options.html fn copy(sources: &[Source], target: &TargetSlice, options: &Options) -> CopyResult<()> { @@ -949,7 +965,23 @@ fn copy(sources: &[Source], target: &TargetSlice, options: &Options) -> CopyResu let mut non_fatal_errors = false; let mut seen_sources = HashSet::with_capacity(sources.len()); let mut symlinked_files = HashSet::new(); - for source in sources { + + let progress_bar = if options.progress_bar { + Some( + ProgressBar::new(disk_usage(sources, options.recursive)?) + .with_style( + ProgressStyle::with_template( + "{msg}: [{elapsed_precise}] {wide_bar} {bytes:>7}/{total_bytes:7}", + ) + .unwrap(), + ) + .with_message(uucore::util_name()), + ) + } else { + None + }; + + for source in sources.iter() { if seen_sources.contains(source) { // FIXME: compare sources by the actual file they point to, not their path. (e.g. dir/file == dir/../dir/file in most cases) show_warning!("source {} specified more than once", source.quote()); @@ -960,9 +992,14 @@ fn copy(sources: &[Source], target: &TargetSlice, options: &Options) -> CopyResu preserve_hardlinks(&mut hard_links, source, &dest, &mut found_hard_link)?; } if !found_hard_link { - if let Err(error) = - copy_source(source, target, &target_type, options, &mut symlinked_files) - { + if let Err(error) = copy_source( + &progress_bar, + source, + target, + &target_type, + options, + &mut symlinked_files, + ) { match error { // When using --no-clobber, we don't want to show // an error message @@ -1017,6 +1054,7 @@ fn construct_dest_path( } fn copy_source( + progress_bar: &Option, source: &SourceSlice, target: &TargetSlice, target_type: &TargetType, @@ -1026,11 +1064,18 @@ fn copy_source( let source_path = Path::new(&source); if source_path.is_dir() { // Copy as directory - copy_directory(source, target, options, symlinked_files, true) + copy_directory(progress_bar, source, target, options, symlinked_files, true) } else { // Copy as file let dest = construct_dest_path(source_path, target, target_type, options)?; - copy_file(source_path, dest.as_path(), options, symlinked_files, true) + copy_file( + progress_bar, + source_path, + dest.as_path(), + options, + symlinked_files, + true, + ) } } @@ -1277,6 +1322,7 @@ fn file_or_link_exists(path: &Path) -> bool { /// The original permissions of `source` will be copied to `dest` /// after a successful copy. fn copy_file( + progress_bar: &Option, source: &Path, dest: &Path, options: &Options, @@ -1452,6 +1498,11 @@ fn copy_file( fs::set_permissions(dest, dest_permissions).ok(); } copy_attributes(source, dest, &options.preserve_attributes)?; + + if let Some(progress_bar) = progress_bar { + progress_bar.inc(fs::metadata(&source)?.len()); + } + Ok(()) } @@ -1571,6 +1622,42 @@ pub fn localize_to_target(root: &Path, source: &Path, target: &Path) -> CopyResu Ok(target.join(local_to_root)) } +/// Get the total size of a slice of files and directories. +/// +/// This function is much like the `du` utility, by recursively getting the sizes of files in directories. +/// Files are not deduplicated when appearing in multiple sources. If `recursive` is set to `false`, the +/// directories in `paths` will be ignored. +fn disk_usage(paths: &[PathBuf], recursive: bool) -> io::Result { + let mut total = 0; + for p in paths { + let md = fs::metadata(p)?; + if md.file_type().is_dir() { + if recursive { + total += disk_usage_directory(p)?; + } + } else { + total += md.len(); + } + } + Ok(total) +} + +/// A helper for `disk_usage` specialized for directories. +fn disk_usage_directory(p: &Path) -> io::Result { + let mut total = 0; + + for entry in fs::read_dir(p)? { + let entry = entry?; + if entry.file_type()?.is_dir() { + total += disk_usage_directory(&entry.path())?; + } else { + total += entry.metadata()?.len(); + } + } + + Ok(total) +} + #[test] fn test_cp_localize_to_target() { assert!(