mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-27 19:17:43 +00:00
Merge pull request #6040 from hamflx/main
Make mv command fallback to copy only if the src and dst are on different device
This commit is contained in:
commit
58c336d5c3
5 changed files with 142 additions and 1 deletions
|
@ -136,6 +136,7 @@ vmsplice
|
||||||
|
|
||||||
# * vars/libc
|
# * vars/libc
|
||||||
COMFOLLOW
|
COMFOLLOW
|
||||||
|
EXDEV
|
||||||
FILENO
|
FILENO
|
||||||
FTSENT
|
FTSENT
|
||||||
HOSTSIZE
|
HOSTSIZE
|
||||||
|
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -3009,8 +3009,10 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"fs_extra",
|
"fs_extra",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
|
"libc",
|
||||||
"thiserror 2.0.11",
|
"thiserror 2.0.11",
|
||||||
"uucore",
|
"uucore",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -28,6 +28,16 @@ uucore = { workspace = true, features = [
|
||||||
] }
|
] }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
windows-sys = { workspace = true, features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_Security",
|
||||||
|
"Win32_Storage_FileSystem",
|
||||||
|
] }
|
||||||
|
|
||||||
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
libc = { workspace = true }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "mv"
|
name = "mv"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
|
@ -657,7 +657,22 @@ fn rename_with_fallback(
|
||||||
to: &Path,
|
to: &Path,
|
||||||
multi_progress: Option<&MultiProgress>,
|
multi_progress: Option<&MultiProgress>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
if fs::rename(from, to).is_err() {
|
if let Err(err) = fs::rename(from, to) {
|
||||||
|
#[cfg(windows)]
|
||||||
|
const EXDEV: i32 = windows_sys::Win32::Foundation::ERROR_NOT_SAME_DEVICE as _;
|
||||||
|
#[cfg(unix)]
|
||||||
|
const EXDEV: i32 = libc::EXDEV as _;
|
||||||
|
|
||||||
|
// We will only copy if:
|
||||||
|
// 1. Files are on different devices (EXDEV error)
|
||||||
|
// 2. On Windows, if the target file exists and source file is opened by another process
|
||||||
|
// (MoveFileExW fails with "Access Denied" even if the source file has FILE_SHARE_DELETE permission)
|
||||||
|
let should_fallback = matches!(err.raw_os_error(), Some(EXDEV))
|
||||||
|
|| (from.is_file() && can_delete_file(from).unwrap_or(false));
|
||||||
|
if !should_fallback {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
// Get metadata without following symlinks
|
// Get metadata without following symlinks
|
||||||
let metadata = from.symlink_metadata()?;
|
let metadata = from.symlink_metadata()?;
|
||||||
let file_type = metadata.file_type();
|
let file_type = metadata.file_type();
|
||||||
|
@ -792,3 +807,55 @@ fn is_empty_dir(path: &Path) -> bool {
|
||||||
Err(_e) => false,
|
Err(_e) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if a file can be deleted by attempting to open it with delete permissions.
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn can_delete_file(path: &Path) -> Result<bool, io::Error> {
|
||||||
|
use std::{
|
||||||
|
os::windows::ffi::OsStrExt as _,
|
||||||
|
ptr::{null, null_mut},
|
||||||
|
};
|
||||||
|
|
||||||
|
use windows_sys::Win32::{
|
||||||
|
Foundation::{CloseHandle, INVALID_HANDLE_VALUE},
|
||||||
|
Storage::FileSystem::{
|
||||||
|
CreateFileW, DELETE, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_DELETE, FILE_SHARE_READ,
|
||||||
|
FILE_SHARE_WRITE, OPEN_EXISTING,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let wide_path = path
|
||||||
|
.as_os_str()
|
||||||
|
.encode_wide()
|
||||||
|
.chain([0])
|
||||||
|
.collect::<Vec<u16>>();
|
||||||
|
|
||||||
|
let handle = unsafe {
|
||||||
|
CreateFileW(
|
||||||
|
wide_path.as_ptr(),
|
||||||
|
DELETE,
|
||||||
|
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||||
|
null(),
|
||||||
|
OPEN_EXISTING,
|
||||||
|
FILE_ATTRIBUTE_NORMAL,
|
||||||
|
null_mut(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if handle == INVALID_HANDLE_VALUE {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe { CloseHandle(handle) };
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn can_delete_file(_: &Path) -> Result<bool, io::Error> {
|
||||||
|
// On non-Windows platforms, always return false to indicate that we don't need
|
||||||
|
// to try the copy+delete fallback. This is because on Unix-like systems,
|
||||||
|
// rename() failing with errors other than EXDEV means the operation cannot
|
||||||
|
// succeed even with a copy+delete approach (e.g. permission errors).
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
|
@ -49,6 +49,22 @@ fn test_mv_rename_file() {
|
||||||
assert!(at.file_exists(file2));
|
assert!(at.file_exists(file2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mv_with_source_file_opened_and_target_file_exists() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
let src = "source_file_opened";
|
||||||
|
let dst = "target_file_exists";
|
||||||
|
|
||||||
|
let f = at.make_file(src);
|
||||||
|
|
||||||
|
at.touch(dst);
|
||||||
|
|
||||||
|
ucmd.arg(src).arg(dst).succeeds().no_stderr().no_stdout();
|
||||||
|
|
||||||
|
drop(f);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mv_move_file_into_dir() {
|
fn test_mv_move_file_into_dir() {
|
||||||
let (at, mut ucmd) = at_and_ucmd!();
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
@ -1670,6 +1686,51 @@ fn test_acl() {
|
||||||
assert!(compare_xattrs(&file, &file_target));
|
assert!(compare_xattrs(&file, &file_target));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn test_move_should_not_fallback_to_copy() {
|
||||||
|
use std::os::windows::fs::OpenOptionsExt;
|
||||||
|
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
let locked_file = "a_file_is_locked";
|
||||||
|
let locked_file_path = at.plus(locked_file);
|
||||||
|
let file = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.share_mode(
|
||||||
|
uucore::windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ
|
||||||
|
| uucore::windows_sys::Win32::Storage::FileSystem::FILE_SHARE_WRITE,
|
||||||
|
)
|
||||||
|
.open(locked_file_path);
|
||||||
|
|
||||||
|
let target_file = "target_file";
|
||||||
|
ucmd.arg(locked_file).arg(target_file).fails();
|
||||||
|
|
||||||
|
assert!(at.file_exists(locked_file));
|
||||||
|
assert!(!at.file_exists(target_file));
|
||||||
|
|
||||||
|
drop(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn test_move_should_not_fallback_to_copy() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
let readonly_dir = "readonly_dir";
|
||||||
|
let locked_file = "readonly_dir/a_file_is_locked";
|
||||||
|
at.mkdir(readonly_dir);
|
||||||
|
at.touch(locked_file);
|
||||||
|
at.set_mode(readonly_dir, 0o555);
|
||||||
|
|
||||||
|
let target_file = "target_file";
|
||||||
|
ucmd.arg(locked_file).arg(target_file).fails();
|
||||||
|
|
||||||
|
assert!(at.file_exists(locked_file));
|
||||||
|
assert!(!at.file_exists(target_file));
|
||||||
|
}
|
||||||
|
|
||||||
// Todo:
|
// Todo:
|
||||||
|
|
||||||
// $ at.touch a b
|
// $ at.touch a b
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue