Skip to content

Commit a01ee76

Browse files
committed
mv: support moving folder containing symlinks to different filesystem
1 parent ee39b35 commit a01ee76

File tree

2 files changed

+72
-9
lines changed

2 files changed

+72
-9
lines changed

src/uu/mv/src/mv.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,7 +1094,13 @@ fn copy_dir_contents_recursive(
10941094
}
10951095
#[cfg(not(unix))]
10961096
{
1097-
fs::copy(&from_path, &to_path)?;
1097+
if from_path.is_symlink() {
1098+
// Copy a symlink file (no-follow).
1099+
rename_symlink_fallback(&from_path, &to_path)?;
1100+
} else {
1101+
// Copy a regular file.
1102+
fs::copy(&from_path, &to_path)?;
1103+
}
10981104
}
10991105

11001106
// Print verbose message for file
@@ -1137,14 +1143,19 @@ fn copy_file_with_hardlinks_helper(
11371143
return Ok(());
11381144
}
11391145

1140-
// Regular file copy
1141-
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
1142-
{
1143-
fs::copy(from, to).and_then(|_| fsxattr::copy_xattrs(&from, &to))?;
1144-
}
1145-
#[cfg(any(target_os = "macos", target_os = "redox"))]
1146-
{
1147-
fs::copy(from, to)?;
1146+
if from.is_symlink() {
1147+
// Copy a symlink file (no-follow).
1148+
rename_symlink_fallback(from, to)?;
1149+
} else {
1150+
// Copy a regular file.
1151+
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
1152+
{
1153+
fs::copy(from, to).and_then(|_| fsxattr::copy_xattrs(&from, &to))?;
1154+
}
1155+
#[cfg(any(target_os = "macos", target_os = "redox"))]
1156+
{
1157+
fs::copy(from, to)?;
1158+
}
11481159
}
11491160

11501161
Ok(())

tests/by-util/test_mv.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,58 @@ 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+
#[ignore = "requires sudo"]
626+
#[test]
627+
fn test_mv_broken_symlink_to_another_fs() {
628+
let scene = TestScenario::new(util_name!());
629+
630+
scene.fixtures.mkdir("foo");
631+
632+
let output = scene
633+
.cmd("sudo")
634+
.env("PATH", env!("PATH"))
635+
.args(&["-E", "--non-interactive", "ls"])
636+
.run();
637+
println!("test output: {output:?}");
638+
639+
let mount = scene
640+
.cmd("sudo")
641+
.env("PATH", env!("PATH"))
642+
.args(&[
643+
"-E",
644+
"--non-interactive",
645+
"mount",
646+
"none",
647+
"-t",
648+
"tmpfs",
649+
"foo",
650+
])
651+
.run();
652+
653+
if !mount.succeeded() {
654+
print!("Test skipped; requires root user");
655+
return;
656+
}
657+
658+
scene.fixtures.mkdir("bar");
659+
scene.fixtures.symlink_file("nonexistent", "bar/baz");
660+
661+
scene
662+
.ucmd()
663+
.arg("bar")
664+
.arg("foo")
665+
.succeeds()
666+
.no_stderr()
667+
.no_stdout();
668+
669+
scene
670+
.cmd("sudo")
671+
.env("PATH", env!("PATH"))
672+
.args(&["-E", "--non-interactive", "umount", "foo"])
673+
.succeeds();
674+
}
675+
624676
#[test]
625677
#[cfg(all(unix, not(target_os = "android")))]
626678
fn test_mv_hardlink_to_symlink() {

0 commit comments

Comments
 (0)