Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cp/mv/ln: add support for the "will not overwrite just-created" #5699

Merged
merged 9 commits into from
Dec 25, 2023
48 changes: 37 additions & 11 deletions src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1171,6 +1171,9 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult
//
// key is the source file's information and the value is the destination filepath.
let mut copied_files: HashMap<FileInformation, PathBuf> = HashMap::with_capacity(sources.len());
// remember the copied destinations for further usage.
// we can't use copied_files as it is because the key is the source file's information.
let mut copied_destinations: HashSet<PathBuf> = HashSet::with_capacity(sources.len());
Copy link
Sponsor Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i will open a good first bug after to avoid "duplication" here


let progress_bar = if options.progress_bar {
let pb = ProgressBar::new(disk_usage(sources, options.recursive)?)
Expand All @@ -1191,17 +1194,40 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult
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());
} else if let Err(error) = copy_source(
&progress_bar,
source,
target,
target_type,
options,
&mut symlinked_files,
&mut copied_files,
) {
show_error_if_needed(&error);
non_fatal_errors = true;
} else {
// We need to compute the destination path

sylvestre marked this conversation as resolved.
Show resolved Hide resolved
let dest = construct_dest_path(source, target, target_type, options)
.unwrap_or_else(|_| target.to_path_buf());

if fs::metadata(&dest).is_ok() && !fs::symlink_metadata(&dest)?.file_type().is_symlink()
{
// There is already a file and it isn't a symlink (managed in a different place)
if copied_destinations.contains(&dest)
&& options.backup != BackupMode::NumberedBackup
{
// If the target file was already created in this cp call, do not overwrite
return Err(Error::Error(format!(
"will not overwrite just-created '{}' with '{}'",
dest.display(),
source.display()
)));
}
}

if let Err(error) = copy_source(
&progress_bar,
source,
target,
target_type,
options,
&mut symlinked_files,
&mut copied_files,
) {
show_error_if_needed(&error);
non_fatal_errors = true;
}
copied_destinations.insert(dest.clone());
}
seen_sources.insert(source);
}
Expand Down
15 changes: 14 additions & 1 deletion src/uu/ln/src/ln.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use uucore::fs::{make_path_relative_to, paths_refer_to_same_file};
use uucore::{format_usage, help_about, help_section, help_usage, prompt_yes, show_error};

use std::borrow::Cow;
use std::collections::HashSet;
use std::error::Error;
use std::ffi::OsString;
use std::fmt::Display;
Expand Down Expand Up @@ -295,6 +296,8 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings)
if !target_dir.is_dir() {
return Err(LnError::TargetIsDirectory(target_dir.to_owned()).into());
}
// remember the linked destinations for further usage
let mut linked_destinations: HashSet<PathBuf> = HashSet::with_capacity(files.len());

let mut all_successful = true;
for srcpath in files {
Expand Down Expand Up @@ -338,10 +341,20 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings)
}
};

if let Err(e) = link(srcpath, &targetpath, settings) {
if linked_destinations.contains(&targetpath) {
// If the target file was already created in this linked call, do not overwrite
sylvestre marked this conversation as resolved.
Show resolved Hide resolved
show_error!(
"will not overwrite just-created '{}' with '{}'",
sylvestre marked this conversation as resolved.
Show resolved Hide resolved
targetpath.display(),
srcpath.display()
);
all_successful = false;
} else if let Err(e) = link(srcpath, &targetpath, settings) {
show_error!("{}", e);
all_successful = false;
}

linked_destinations.insert(targetpath.clone());
}
if all_successful {
Ok(())
Expand Down
25 changes: 22 additions & 3 deletions src/uu/mv/src/mv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod error;
use clap::builder::ValueParser;
use clap::{crate_version, error::ErrorKind, Arg, ArgAction, ArgMatches, Command};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::collections::HashSet;
use std::env;
use std::ffi::OsString;
use std::fs;
Expand Down Expand Up @@ -433,7 +434,10 @@ pub fn mv(files: &[OsString], opts: &Options) -> UResult<()> {
}

#[allow(clippy::cognitive_complexity)]
fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, opts: &Options) -> UResult<()> {
fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, options: &Options) -> UResult<()> {
// remember the moved destinations for further usage
let mut moved_destinations: HashSet<PathBuf> = HashSet::with_capacity(files.len());

if !target_dir.is_dir() {
return Err(MvError::NotADirectory(target_dir.quote().to_string()).into());
}
Expand All @@ -442,7 +446,7 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, opts: &Options) ->
.canonicalize()
.unwrap_or_else(|_| target_dir.to_path_buf());

let multi_progress = opts.progress_bar.then(MultiProgress::new);
let multi_progress = options.progress_bar.then(MultiProgress::new);

let count_progress = if let Some(ref multi_progress) = multi_progress {
if files.len() > 1 {
Expand Down Expand Up @@ -471,6 +475,20 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, opts: &Options) ->
}
};

if moved_destinations.contains(&targetpath) && options.backup != BackupMode::NumberedBackup
{
// If the target file was already created in this mv call, do not overwrite
show!(USimpleError::new(
1,
format!(
"will not overwrite just-created '{}' with '{}'",
targetpath.display(),
sourcepath.display()
),
));
continue;
}

// Check if we have mv dir1 dir2 dir2
// And generate an error if this is the case
if let Ok(canonicalized_source) = sourcepath.canonicalize() {
Expand All @@ -493,7 +511,7 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, opts: &Options) ->
}
}

match rename(sourcepath, &targetpath, opts, multi_progress.as_ref()) {
match rename(sourcepath, &targetpath, options, multi_progress.as_ref()) {
Err(e) if e.to_string().is_empty() => set_exit_code(1),
Err(e) => {
let e = e.map_err_context(|| {
Expand All @@ -513,6 +531,7 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, opts: &Options) ->
if let Some(ref pb) = count_progress {
pb.inc(1);
}
moved_destinations.insert(targetpath.clone());
}
Ok(())
}
Expand Down
33 changes: 33 additions & 0 deletions tests/by-util/test_cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3559,3 +3559,36 @@
assert_eq!(mode_a, at.metadata(a).mode());
assert_eq!(mode_b, at.metadata(b).mode());
}

#[test]
fn test_cp_seen_file() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;

at.mkdir("a");
at.mkdir("b");
at.mkdir("c");
at.write("a/f", "a");
at.write("b/f", "b");

let result = ts.ucmd().arg("a/f").arg("b/f").arg("c").fails();
#[cfg(not(unix))]
assert!(result
.stderr_str()
.contains("will not overwrite just-created 'c\\f' with 'b/f'"));
#[cfg(unix)]
assert!(result
.stderr_str()
.contains("will not overwrite just-created 'c/f' with 'b/f'"));

assert!(at.plus("c").join("f").exists());

ts.ucmd()
.arg("--backup=numbered")
.arg("a/f")
.arg("b/f")
.arg("c")
.succeeds();

Check warning on line 3591 in tests/by-util/test_cp.rs

View check run for this annotation

Codecov / codecov/patch

tests/by-util/test_cp.rs#L3591

Added line #L3591 was not covered by tests
assert!(at.plus("c").join("f").exists());
assert!(at.plus("c").join("f.~1~").exists());
}
47 changes: 47 additions & 0 deletions tests/by-util/test_ln.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use crate::common::util::TestScenario;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;

#[test]
Expand Down Expand Up @@ -719,3 +721,48 @@
assert!(at.file_exists("a") && !at.symlink_exists("a"));
assert_eq!(at.read("a"), "sample");
}

#[test]
fn test_ln_seen_file() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;

at.mkdir("a");
at.mkdir("b");
at.mkdir("c");
at.write("a/f", "a");
at.write("b/f", "b");

let result = ts.ucmd().arg("a/f").arg("b/f").arg("c").fails();

#[cfg(not(unix))]
assert!(result

Check warning on line 739 in tests/by-util/test_ln.rs

View check run for this annotation

Codecov / codecov/patch

tests/by-util/test_ln.rs#L739

Added line #L739 was not covered by tests
.stderr_str()
.contains("will not overwrite just-created 'c\\f' with 'b/f'"));
#[cfg(unix)]
assert!(result
.stderr_str()
.contains("will not overwrite just-created 'c/f' with 'b/f'"));

assert!(at.plus("c").join("f").exists());
// b/f still exists
assert!(at.plus("b").join("f").exists());
// a/f no longer exists
sylvestre marked this conversation as resolved.
Show resolved Hide resolved
assert!(at.plus("a").join("f").exists());
#[cfg(unix)]
{
// Check inode numbers
let inode_a_f = at.plus("a").join("f").metadata().unwrap().ino();
let inode_b_f = at.plus("b").join("f").metadata().unwrap().ino();
let inode_c_f = at.plus("c").join("f").metadata().unwrap().ino();

assert_eq!(
inode_a_f, inode_c_f,
"Inode numbers of a/f and c/f should be equal"
);
assert_ne!(
inode_b_f, inode_c_f,
"Inode numbers of b/f and c/f should not be equal"
);
}
}
59 changes: 59 additions & 0 deletions tests/by-util/test_mv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1469,6 +1469,65 @@
.stderr_contains("mv: failed to access 'b/': Not a directory");
}

#[test]
fn test_mv_seen_file() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;

at.mkdir("a");
at.mkdir("b");
at.mkdir("c");
at.write("a/f", "a");
at.write("b/f", "b");

let result = ts.ucmd().arg("a/f").arg("b/f").arg("c").fails();

#[cfg(not(unix))]
assert!(result

Check warning on line 1486 in tests/by-util/test_mv.rs

View check run for this annotation

Codecov / codecov/patch

tests/by-util/test_mv.rs#L1486

Added line #L1486 was not covered by tests
.stderr_str()
.contains("will not overwrite just-created 'c\\f' with 'b/f'"));
#[cfg(unix)]
assert!(result
.stderr_str()
.contains("will not overwrite just-created 'c/f' with 'b/f'"));

// a/f has been moved into c/f
assert!(at.plus("c").join("f").exists());
// b/f still exists
assert!(at.plus("b").join("f").exists());
// a/f no longer exists
assert!(!at.plus("a").join("f").exists());
}

#[test]
fn test_mv_seen_multiple_files_to_directory() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;

at.mkdir("a");
at.mkdir("b");
at.mkdir("c");
at.write("a/f", "a");
at.write("b/f", "b");
at.write("b/g", "g");

let result = ts.ucmd().arg("a/f").arg("b/f").arg("b/g").arg("c").fails();
#[cfg(not(unix))]
assert!(result

Check warning on line 1516 in tests/by-util/test_mv.rs

View check run for this annotation

Codecov / codecov/patch

tests/by-util/test_mv.rs#L1516

Added line #L1516 was not covered by tests
.stderr_str()
.contains("will not overwrite just-created 'c\\f' with 'b/f'"));
#[cfg(unix)]
assert!(result
.stderr_str()
.contains("will not overwrite just-created 'c/f' with 'b/f'"));

assert!(!at.plus("a").join("f").exists());
assert!(at.plus("b").join("f").exists());
assert!(!at.plus("b").join("g").exists());
assert!(at.plus("c").join("f").exists());
assert!(at.plus("c").join("g").exists());
}

#[test]
fn test_mv_dir_into_file_where_both_are_files() {
let scene = TestScenario::new(util_name!());
Expand Down
Loading