Skip to content

Commit b883bf1

Browse files
committed
mv: support moving folder containing symlinks to different filesystem
1 parent 32eef06 commit b883bf1

File tree

2 files changed

+64
-9
lines changed

2 files changed

+64
-9
lines changed

src/uu/mv/src/mv.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,7 +1044,13 @@ fn copy_dir_contents_recursive(
10441044
}
10451045
#[cfg(not(unix))]
10461046
{
1047-
fs::copy(&from_path, &to_path)?;
1047+
if from_path.is_symlink() {
1048+
// Copy a symlink file (no-follow).
1049+
rename_symlink_fallback(&from_path, &to_path)?;
1050+
} else {
1051+
// Copy a regular file.
1052+
fs::copy(&from_path, &to_path)?;
1053+
}
10481054
}
10491055
}
10501056

@@ -1076,14 +1082,19 @@ fn copy_file_with_hardlinks_helper(
10761082
return Ok(());
10771083
}
10781084

1079-
// Regular file copy
1080-
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
1081-
{
1082-
fs::copy(from, to).and_then(|_| fsxattr::copy_xattrs(&from, &to))?;
1083-
}
1084-
#[cfg(any(target_os = "macos", target_os = "redox"))]
1085-
{
1086-
fs::copy(from, to)?;
1085+
if from.is_symlink() {
1086+
// Copy a symlink file (no-follow).
1087+
rename_symlink_fallback(from, to)?;
1088+
} else {
1089+
// Copy a regular file.
1090+
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
1091+
{
1092+
fs::copy(from, to).and_then(|_| fsxattr::copy_xattrs(&from, &to))?;
1093+
}
1094+
#[cfg(any(target_os = "macos", target_os = "redox"))]
1095+
{
1096+
fs::copy(from, to)?;
1097+
}
10871098
}
10881099

10891100
Ok(())

tests/by-util/test_mv.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,50 @@ fn test_mv_symlink_into_target() {
621621
ucmd.arg("dir-link").arg("dir").succeeds();
622622
}
623623

624+
#[cfg(all(unix, not(target_os = "android")))]
625+
#[test]
626+
fn test_mv_broken_symlink_to_another_fs() {
627+
let scene = TestScenario::new(util_name!());
628+
629+
scene.fixtures.mkdir("foo");
630+
631+
let mount = scene
632+
.cmd("sudo")
633+
.env("PATH", env!("PATH"))
634+
.args(&[
635+
"-E",
636+
"--non-interactive",
637+
"mount",
638+
"none",
639+
"-t",
640+
"tmpfs",
641+
"foo",
642+
])
643+
.run();
644+
645+
if !mount.succeeded() {
646+
print!("Test skipped; requires root user");
647+
return;
648+
}
649+
650+
scene.fixtures.mkdir("bar");
651+
scene.fixtures.symlink_file("nonexistent", "bar/baz");
652+
653+
scene
654+
.ucmd()
655+
.arg("bar")
656+
.arg("foo")
657+
.succeeds()
658+
.no_stderr()
659+
.no_stdout();
660+
661+
scene
662+
.cmd("sudo")
663+
.env("PATH", env!("PATH"))
664+
.args(&["-E", "--non-interactive", "umount", "foo"])
665+
.succeeds();
666+
}
667+
624668
#[test]
625669
#[cfg(all(unix, not(target_os = "android")))]
626670
fn test_mv_hardlink_to_symlink() {

0 commit comments

Comments
 (0)