mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-30 12:37:49 +00:00
Merge pull request #2110 from nthery/cp_reflink_macos
cp: add --reflink support to macos, fixes #1773
This commit is contained in:
commit
d3775ea0e8
2 changed files with 111 additions and 42 deletions
|
@ -155,7 +155,8 @@ pub enum OverwriteMode {
|
||||||
NoClobber,
|
NoClobber,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Eq, PartialEq)]
|
/// Possible arguments for `--reflink`.
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
pub enum ReflinkMode {
|
pub enum ReflinkMode {
|
||||||
Always,
|
Always,
|
||||||
Auto,
|
Auto,
|
||||||
|
@ -1192,47 +1193,20 @@ fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
///Copy the file from `source` to `dest` either using the normal `fs::copy` or the
|
/// Copy the file from `source` to `dest` either using the normal `fs::copy` or a
|
||||||
///`FICLONE` ioctl if --reflink is specified and the filesystem supports it.
|
/// copy-on-write scheme if --reflink is specified and the filesystem supports it.
|
||||||
fn copy_helper(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> {
|
fn copy_helper(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> {
|
||||||
if options.reflink_mode != ReflinkMode::Never {
|
if options.reflink_mode != ReflinkMode::Never {
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||||
return Err("--reflink is only supported on linux".to_string().into());
|
return Err("--reflink is only supported on linux and macOS"
|
||||||
|
.to_string()
|
||||||
|
.into());
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
copy_on_write_macos(source, dest, options.reflink_mode)?;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
copy_on_write_linux(source, dest, options.reflink_mode)?;
|
||||||
let src_file = File::open(source).unwrap().into_raw_fd();
|
|
||||||
let dst_file = OpenOptions::new()
|
|
||||||
.write(true)
|
|
||||||
.truncate(false)
|
|
||||||
.create(true)
|
|
||||||
.open(dest)
|
|
||||||
.unwrap()
|
|
||||||
.into_raw_fd();
|
|
||||||
match options.reflink_mode {
|
|
||||||
ReflinkMode::Always => unsafe {
|
|
||||||
let result = ficlone(dst_file, src_file as *const i32);
|
|
||||||
if result != 0 {
|
|
||||||
return Err(format!(
|
|
||||||
"failed to clone {:?} from {:?}: {}",
|
|
||||||
source,
|
|
||||||
dest,
|
|
||||||
std::io::Error::last_os_error()
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
} else {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ReflinkMode::Auto => unsafe {
|
|
||||||
let result = ficlone(dst_file, src_file as *const i32);
|
|
||||||
if result != 0 {
|
|
||||||
fs::copy(source, dest).context(&*context_for(source, dest))?;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ReflinkMode::Never => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if options.no_dereference && fs::symlink_metadata(&source)?.file_type().is_symlink() {
|
} else if options.no_dereference && fs::symlink_metadata(&source)?.file_type().is_symlink() {
|
||||||
// Here, we will copy the symlink itself (actually, just recreate it)
|
// Here, we will copy the symlink itself (actually, just recreate it)
|
||||||
let link = fs::read_link(&source)?;
|
let link = fs::read_link(&source)?;
|
||||||
|
@ -1265,6 +1239,101 @@ fn copy_helper(source: &Path, dest: &Path, options: &Options) -> CopyResult<()>
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Copies `source` to `dest` using copy-on-write if possible.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn copy_on_write_linux(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyResult<()> {
|
||||||
|
debug_assert!(mode != ReflinkMode::Never);
|
||||||
|
|
||||||
|
let src_file = File::open(source).unwrap().into_raw_fd();
|
||||||
|
let dst_file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.truncate(false)
|
||||||
|
.create(true)
|
||||||
|
.open(dest)
|
||||||
|
.unwrap()
|
||||||
|
.into_raw_fd();
|
||||||
|
match mode {
|
||||||
|
ReflinkMode::Always => unsafe {
|
||||||
|
let result = ficlone(dst_file, src_file as *const i32);
|
||||||
|
if result != 0 {
|
||||||
|
return Err(format!(
|
||||||
|
"failed to clone {:?} from {:?}: {}",
|
||||||
|
source,
|
||||||
|
dest,
|
||||||
|
std::io::Error::last_os_error()
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ReflinkMode::Auto => unsafe {
|
||||||
|
let result = ficlone(dst_file, src_file as *const i32);
|
||||||
|
if result != 0 {
|
||||||
|
fs::copy(source, dest).context(&*context_for(source, dest))?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ReflinkMode::Never => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies `source` to `dest` using copy-on-write if possible.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn copy_on_write_macos(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyResult<()> {
|
||||||
|
debug_assert!(mode != ReflinkMode::Never);
|
||||||
|
|
||||||
|
// Extract paths in a form suitable to be passed to a syscall.
|
||||||
|
// The unwrap() is safe because they come from the command-line and so contain non nul
|
||||||
|
// character.
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
let src = CString::new(source.as_os_str().as_bytes()).unwrap();
|
||||||
|
let dst = CString::new(dest.as_os_str().as_bytes()).unwrap();
|
||||||
|
|
||||||
|
// clonefile(2) was introduced in macOS 10.12 so we cannot statically link against it
|
||||||
|
// for backward compatibility.
|
||||||
|
let clonefile = CString::new("clonefile").unwrap();
|
||||||
|
let raw_pfn = unsafe { libc::dlsym(libc::RTLD_NEXT, clonefile.as_ptr()) };
|
||||||
|
|
||||||
|
let mut error = 0;
|
||||||
|
if !raw_pfn.is_null() {
|
||||||
|
// Call clonefile(2).
|
||||||
|
// Safety: Casting a C function pointer to a rust function value is one of the few
|
||||||
|
// blessed uses of `transmute()`.
|
||||||
|
unsafe {
|
||||||
|
let pfn: extern "C" fn(
|
||||||
|
src: *const libc::c_char,
|
||||||
|
dst: *const libc::c_char,
|
||||||
|
flags: u32,
|
||||||
|
) -> libc::c_int = std::mem::transmute(raw_pfn);
|
||||||
|
error = pfn(src.as_ptr(), dst.as_ptr(), 0);
|
||||||
|
if std::io::Error::last_os_error().kind() == std::io::ErrorKind::AlreadyExists {
|
||||||
|
// clonefile(2) fails if the destination exists. Remove it and try again. Do not
|
||||||
|
// bother to check if removal worked because we're going to try to clone again.
|
||||||
|
let _ = fs::remove_file(dest);
|
||||||
|
error = pfn(src.as_ptr(), dst.as_ptr(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw_pfn.is_null() || error != 0 {
|
||||||
|
// clonefile(2) is not supported or it error'ed out (possibly because the FS does not
|
||||||
|
// support COW).
|
||||||
|
match mode {
|
||||||
|
ReflinkMode::Always => {
|
||||||
|
return Err(
|
||||||
|
format!("failed to clone {:?} from {:?}: {}", source, dest, error).into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ReflinkMode::Auto => fs::copy(source, dest).context(&*context_for(source, dest))?,
|
||||||
|
ReflinkMode::Never => unreachable!(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate an error message if `target` is not the correct `target_type`
|
/// Generate an error message if `target` is not the correct `target_type`
|
||||||
pub fn verify_target_type(target: &Path, target_type: &TargetType) -> CopyResult<()> {
|
pub fn verify_target_type(target: &Path, target_type: &TargetType) -> CopyResult<()> {
|
||||||
match (target_type, target.is_dir()) {
|
match (target_type, target.is_dir()) {
|
||||||
|
|
|
@ -967,7 +967,7 @@ fn test_cp_one_file_system() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
fn test_cp_reflink_always() {
|
fn test_cp_reflink_always() {
|
||||||
let (at, mut ucmd) = at_and_ucmd!();
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
let result = ucmd
|
let result = ucmd
|
||||||
|
@ -985,7 +985,7 @@ fn test_cp_reflink_always() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
fn test_cp_reflink_auto() {
|
fn test_cp_reflink_auto() {
|
||||||
let (at, mut ucmd) = at_and_ucmd!();
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
ucmd.arg("--reflink=auto")
|
ucmd.arg("--reflink=auto")
|
||||||
|
@ -998,7 +998,7 @@ fn test_cp_reflink_auto() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
fn test_cp_reflink_never() {
|
fn test_cp_reflink_never() {
|
||||||
let (at, mut ucmd) = at_and_ucmd!();
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
ucmd.arg("--reflink=never")
|
ucmd.arg("--reflink=never")
|
||||||
|
@ -1011,7 +1011,7 @@ fn test_cp_reflink_never() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
fn test_cp_reflink_bad() {
|
fn test_cp_reflink_bad() {
|
||||||
let (_, mut ucmd) = at_and_ucmd!();
|
let (_, mut ucmd) = at_and_ucmd!();
|
||||||
let _result = ucmd
|
let _result = ucmd
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue