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
getrandom
globset
indicatif
itertools
lscolors
mdbook

48
Cargo.lock generated
View file

@ -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",
]

View file

@ -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"

View file

@ -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<ProgressBar>,
entry: Entry,
options: &Options,
symlinked_files: &mut HashSet<FileInformation>,
@ -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<ProgressBar>,
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,

View file

@ -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<String>,
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<ProgressBar>,
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<ProgressBar>,
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<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]
fn test_cp_localize_to_target() {
assert!(