1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

cp: add progress bar

Adds the `-g` and `--progress` flags to enable a progress bar via
indicatif.
This commit is contained in:
Terts Diepraam 2022-09-05 10:56:04 +02:00
parent 1172a7e781
commit 21e691c3b9
5 changed files with 149 additions and 11 deletions

View file

@ -23,6 +23,7 @@ fsext
getopts getopts
getrandom getrandom
globset globset
indicatif
itertools itertools
lscolors lscolors
mdbook mdbook

48
Cargo.lock generated
View file

@ -281,7 +281,7 @@ dependencies = [
"once_cell", "once_cell",
"strsim", "strsim",
"termcolor", "termcolor",
"terminal_size", "terminal_size 0.2.2",
] ]
[[package]] [[package]]
@ -327,6 +327,20 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "120133d4db2ec47efe2e26502ee984747630c67f51974fca0b6c1340cf2368d3" 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]] [[package]]
name = "constant_time_eq" name = "constant_time_eq"
version = "0.1.5" version = "0.1.5"
@ -791,6 +805,12 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.8.4" version = "0.8.4"
@ -1047,6 +1067,17 @@ dependencies = [
"hashbrown", "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]] [[package]]
name = "inotify" name = "inotify"
version = "0.9.6" version = "0.9.6"
@ -2033,6 +2064,16 @@ dependencies = [
"winapi-util", "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]] [[package]]
name = "terminal_size" name = "terminal_size"
version = "0.2.2" version = "0.2.2"
@ -2050,7 +2091,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
dependencies = [ dependencies = [
"smawk", "smawk",
"terminal_size", "terminal_size 0.2.2",
"unicode-linebreak", "unicode-linebreak",
"unicode-width", "unicode-width",
] ]
@ -2293,6 +2334,7 @@ dependencies = [
"clap 4.0.22", "clap 4.0.22",
"exacl", "exacl",
"filetime", "filetime",
"indicatif",
"libc", "libc",
"quick-error", "quick-error",
"selinux", "selinux",
@ -2598,7 +2640,7 @@ dependencies = [
"once_cell", "once_cell",
"selinux", "selinux",
"term_grid", "term_grid",
"terminal_size", "terminal_size 0.2.2",
"unicode-width", "unicode-width",
"uucore", "uucore",
] ]

View file

@ -26,6 +26,7 @@ quick-error = "2.0.1"
selinux = { version="0.3", optional=true } selinux = { version="0.3", optional=true }
uucore = { version=">=0.0.16", package="uucore", path="../../uucore", features=["entries", "fs", "perms", "mode"] } uucore = { version=">=0.0.16", package="uucore", path="../../uucore", features=["entries", "fs", "perms", "mode"] }
walkdir = "2.2" walkdir = "2.2"
indicatif = "0.17"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
xattr="0.2.3" xattr="0.2.3"

View file

@ -14,6 +14,7 @@ use std::fs;
use std::io; use std::io;
use std::path::{Path, PathBuf, StripPrefixError}; use std::path::{Path, PathBuf, StripPrefixError};
use indicatif::ProgressBar;
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::UIoError; use uucore::error::UIoError;
use uucore::fs::{canonicalize, FileInformation, MissingHandling, ResolveMode}; use uucore::fs::{canonicalize, FileInformation, MissingHandling, ResolveMode};
@ -170,6 +171,7 @@ impl Entry {
/// Copy a single entry during a directory traversal. /// Copy a single entry during a directory traversal.
fn copy_direntry( fn copy_direntry(
progress_bar: &Option<ProgressBar>,
entry: Entry, entry: Entry,
options: &Options, options: &Options,
symlinked_files: &mut HashSet<FileInformation>, symlinked_files: &mut HashSet<FileInformation>,
@ -213,6 +215,7 @@ fn copy_direntry(
preserve_hardlinks(hard_links, &source_absolute, &dest, &mut found_hard_link)?; preserve_hardlinks(hard_links, &source_absolute, &dest, &mut found_hard_link)?;
if !found_hard_link { if !found_hard_link {
match copy_file( match copy_file(
progress_bar,
&source_absolute, &source_absolute,
local_to_target.as_path(), local_to_target.as_path(),
options, options,
@ -240,6 +243,7 @@ fn copy_direntry(
// TODO What other kinds of errors, if any, should // TODO What other kinds of errors, if any, should
// cause us to continue walking the directory? // cause us to continue walking the directory?
match copy_file( match copy_file(
progress_bar,
&source_absolute, &source_absolute,
local_to_target.as_path(), local_to_target.as_path(),
options, options,
@ -272,6 +276,7 @@ fn copy_direntry(
/// Any errors encountered copying files in the tree will be logged but /// Any errors encountered copying files in the tree will be logged but
/// will not cause a short-circuit. /// will not cause a short-circuit.
pub(crate) fn copy_directory( pub(crate) fn copy_directory(
progress_bar: &Option<ProgressBar>,
root: &Path, root: &Path,
target: &TargetSlice, target: &TargetSlice,
options: &Options, 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 no-dereference is enabled and this is a symlink, copy it as a file
if !options.dereference(source_in_command_line) && root.is_symlink() { if !options.dereference(source_in_command_line) && root.is_symlink() {
return copy_file( return copy_file(
progress_bar,
root, root,
target, target,
options, options,
@ -344,6 +350,7 @@ pub(crate) fn copy_directory(
Ok(direntry) => { Ok(direntry) => {
let entry = Entry::new(&context, &direntry)?; let entry = Entry::new(&context, &direntry)?;
copy_direntry( copy_direntry(
progress_bar,
entry, entry,
options, options,
symlinked_files, symlinked_files,

View file

@ -9,7 +9,7 @@
// For the full copyright and license information, please view the LICENSE file // For the full copyright and license information, please view the LICENSE file
// that was distributed with this source code. // 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] #[macro_use]
extern crate quick_error; extern crate quick_error;
@ -33,6 +33,7 @@ use std::string::ToString;
use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use clap::{crate_version, Arg, ArgAction, ArgMatches, Command};
use filetime::FileTime; use filetime::FileTime;
use indicatif::{ProgressBar, ProgressStyle};
#[cfg(unix)] #[cfg(unix)]
use libc::mkfifo; use libc::mkfifo;
use quick_error::ResultExt; use quick_error::ResultExt;
@ -214,6 +215,7 @@ pub struct Options {
target_dir: Option<String>, target_dir: Option<String>,
update: bool, update: bool,
verbose: bool, verbose: bool,
progress_bar: bool,
} }
static ABOUT: &str = "Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY."; 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 PARENT: &str = "parent";
pub const PARENTS: &str = "parents"; pub const PARENTS: &str = "parents";
pub const PATHS: &str = "paths"; pub const PATHS: &str = "paths";
pub const PROGRESS_BAR: &str = "progress";
pub const PRESERVE: &str = "preserve"; pub const PRESERVE: &str = "preserve";
pub const PRESERVE_DEFAULT_ATTRIBUTES: &str = "preserve-default-attributes"; pub const PRESERVE_DEFAULT_ATTRIBUTES: &str = "preserve-default-attributes";
pub const RECURSIVE: &str = "recursive"; pub const RECURSIVE: &str = "recursive";
@ -538,6 +541,18 @@ pub fn uu_app() -> Command {
), ),
) )
// END TODO // 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(
Arg::new(options::PATHS) Arg::new(options::PATHS)
.action(ArgAction::Append) .action(ArgAction::Append)
@ -817,6 +832,7 @@ impl Options {
preserve_attributes, preserve_attributes,
recursive, recursive,
target_dir, target_dir,
progress_bar: matches.get_flag(options::PROGRESS_BAR),
}; };
Ok(options) Ok(options)
@ -935,7 +951,7 @@ fn preserve_hardlinks(
/// `Err(Error::NotAllFilesCopied)` if at least one non-fatal error was /// `Err(Error::NotAllFilesCopied)` if at least one non-fatal error was
/// encountered. /// encountered.
/// ///
/// Behavior depends on `options`, see [`Options`] for details. /// Behavior depends on path`options`, see [`Options`] for details.
/// ///
/// [`Options`]: ./struct.Options.html /// [`Options`]: ./struct.Options.html
fn copy(sources: &[Source], target: &TargetSlice, options: &Options) -> CopyResult<()> { 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 non_fatal_errors = false;
let mut seen_sources = HashSet::with_capacity(sources.len()); let mut seen_sources = HashSet::with_capacity(sources.len());
let mut symlinked_files = HashSet::new(); 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) { 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) // 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()); 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)?; preserve_hardlinks(&mut hard_links, source, &dest, &mut found_hard_link)?;
} }
if !found_hard_link { if !found_hard_link {
if let Err(error) = if let Err(error) = copy_source(
copy_source(source, target, &target_type, options, &mut symlinked_files) &progress_bar,
{ source,
target,
&target_type,
options,
&mut symlinked_files,
) {
match error { match error {
// When using --no-clobber, we don't want to show // When using --no-clobber, we don't want to show
// an error message // an error message
@ -1017,6 +1054,7 @@ fn construct_dest_path(
} }
fn copy_source( fn copy_source(
progress_bar: &Option<ProgressBar>,
source: &SourceSlice, source: &SourceSlice,
target: &TargetSlice, target: &TargetSlice,
target_type: &TargetType, target_type: &TargetType,
@ -1026,11 +1064,18 @@ fn copy_source(
let source_path = Path::new(&source); let source_path = Path::new(&source);
if source_path.is_dir() { if source_path.is_dir() {
// Copy as directory // Copy as directory
copy_directory(source, target, options, symlinked_files, true) copy_directory(progress_bar, source, target, options, symlinked_files, true)
} else { } else {
// Copy as file // Copy as file
let dest = construct_dest_path(source_path, target, target_type, options)?; 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` /// The original permissions of `source` will be copied to `dest`
/// after a successful copy. /// after a successful copy.
fn copy_file( fn copy_file(
progress_bar: &Option<ProgressBar>,
source: &Path, source: &Path,
dest: &Path, dest: &Path,
options: &Options, options: &Options,
@ -1452,6 +1498,11 @@ fn copy_file(
fs::set_permissions(dest, dest_permissions).ok(); fs::set_permissions(dest, dest_permissions).ok();
} }
copy_attributes(source, dest, &options.preserve_attributes)?; copy_attributes(source, dest, &options.preserve_attributes)?;
if let Some(progress_bar) = progress_bar {
progress_bar.inc(fs::metadata(&source)?.len());
}
Ok(()) Ok(())
} }
@ -1571,6 +1622,42 @@ pub fn localize_to_target(root: &Path, source: &Path, target: &Path) -> CopyResu
Ok(target.join(local_to_root)) 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<u64> {
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<u64> {
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] #[test]
fn test_cp_localize_to_target() { fn test_cp_localize_to_target() {
assert!( assert!(