Skip to content

Commit 1891bfa

Browse files
committedMay 3, 2019
Auto merge of #59883 - ebarnard:clonefile, r=sfackler
Make `std::fs::copy` attempt to create copy-on-write clones of files on MacOS The behaviour of MacOS now matches Linux which uses `copy_file_range` to perform CoW file copies where available and supported by the underlying filesystem.
2 parents 08bfe16 + 0fd446e commit 1891bfa

File tree

2 files changed

+71
-21
lines changed

2 files changed

+71
-21
lines changed
 

‎src/libstd/fs.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1615,8 +1615,8 @@ pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<()>
16151615
/// `O_CLOEXEC` is set for returned file descriptors.
16161616
/// On Windows, this function currently corresponds to `CopyFileEx`. Alternate
16171617
/// NTFS streams are copied but only the size of the main stream is returned by
1618-
/// this function. On MacOS, this function corresponds to `copyfile` with
1619-
/// `COPYFILE_ALL`.
1618+
/// this function. On MacOS, this function corresponds to `fclonefileat` and
1619+
/// `fcopyfile`.
16201620
/// Note that, this [may change in the future][changes].
16211621
///
16221622
/// [changes]: ../io/index.html#platform-specific-behavior

‎src/libstd/sys/unix/fs.rs

+69-19
Original file line numberDiff line numberDiff line change
@@ -823,24 +823,28 @@ pub fn canonicalize(p: &Path) -> io::Result<PathBuf> {
823823
Ok(PathBuf::from(OsString::from_vec(buf)))
824824
}
825825

826-
fn open_and_set_permissions(
827-
from: &Path,
826+
fn open_from(from: &Path) -> io::Result<(crate::fs::File, crate::fs::Metadata)> {
827+
use crate::fs::File;
828+
829+
let reader = File::open(from)?;
830+
let metadata = reader.metadata()?;
831+
if !metadata.is_file() {
832+
return Err(Error::new(
833+
ErrorKind::InvalidInput,
834+
"the source path is not an existing regular file",
835+
));
836+
}
837+
Ok((reader, metadata))
838+
}
839+
840+
fn open_to_and_set_permissions(
828841
to: &Path,
829-
) -> io::Result<(crate::fs::File, crate::fs::File, u64, crate::fs::Metadata)> {
830-
use crate::fs::{File, OpenOptions};
842+
reader_metadata: crate::fs::Metadata,
843+
) -> io::Result<(crate::fs::File, crate::fs::Metadata)> {
844+
use crate::fs::OpenOptions;
831845
use crate::os::unix::fs::{OpenOptionsExt, PermissionsExt};
832846

833-
let reader = File::open(from)?;
834-
let (perm, len) = {
835-
let metadata = reader.metadata()?;
836-
if !metadata.is_file() {
837-
return Err(Error::new(
838-
ErrorKind::InvalidInput,
839-
"the source path is not an existing regular file",
840-
));
841-
}
842-
(metadata.permissions(), metadata.len())
843-
};
847+
let perm = reader_metadata.permissions();
844848
let writer = OpenOptions::new()
845849
// create the file with the correct mode right away
846850
.mode(perm.mode())
@@ -855,15 +859,16 @@ fn open_and_set_permissions(
855859
// pipes/FIFOs or device nodes.
856860
writer.set_permissions(perm)?;
857861
}
858-
Ok((reader, writer, len, writer_metadata))
862+
Ok((writer, writer_metadata))
859863
}
860864

861865
#[cfg(not(any(target_os = "linux",
862866
target_os = "android",
863867
target_os = "macos",
864868
target_os = "ios")))]
865869
pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
866-
let (mut reader, mut writer, _, _) = open_and_set_permissions(from, to)?;
870+
let (mut reader, reader_metadata) = open_from(from)?;
871+
let (mut writer, _) = open_to_and_set_permissions(to, reader_metadata)?;
867872

868873
io::copy(&mut reader, &mut writer)
869874
}
@@ -896,7 +901,9 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
896901
)
897902
}
898903

899-
let (mut reader, mut writer, len, _) = open_and_set_permissions(from, to)?;
904+
let (mut reader, reader_metadata) = open_from(from)?;
905+
let len = reader_metadata.len();
906+
let (mut writer, _) = open_to_and_set_permissions(to, reader_metadata)?;
900907

901908
let has_copy_file_range = HAS_COPY_FILE_RANGE.load(Ordering::Relaxed);
902909
let mut written = 0u64;
@@ -955,6 +962,8 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
955962

956963
#[cfg(any(target_os = "macos", target_os = "ios"))]
957964
pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
965+
use crate::sync::atomic::{AtomicBool, Ordering};
966+
958967
const COPYFILE_ACL: u32 = 1 << 0;
959968
const COPYFILE_STAT: u32 = 1 << 1;
960969
const COPYFILE_XATTR: u32 = 1 << 2;
@@ -1000,7 +1009,48 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
10001009
}
10011010
}
10021011

1003-
let (reader, writer, _, writer_metadata) = open_and_set_permissions(from, to)?;
1012+
// MacOS prior to 10.12 don't support `fclonefileat`
1013+
// We store the availability in a global to avoid unnecessary syscalls
1014+
static HAS_FCLONEFILEAT: AtomicBool = AtomicBool::new(true);
1015+
syscall! {
1016+
fn fclonefileat(
1017+
srcfd: libc::c_int,
1018+
dst_dirfd: libc::c_int,
1019+
dst: *const libc::c_char,
1020+
flags: libc::c_int
1021+
) -> libc::c_int
1022+
}
1023+
1024+
let (reader, reader_metadata) = open_from(from)?;
1025+
1026+
// Opportunistically attempt to create a copy-on-write clone of `from`
1027+
// using `fclonefileat`.
1028+
if HAS_FCLONEFILEAT.load(Ordering::Relaxed) {
1029+
let to = cstr(to)?;
1030+
let clonefile_result = cvt(unsafe {
1031+
fclonefileat(
1032+
reader.as_raw_fd(),
1033+
libc::AT_FDCWD,
1034+
to.as_ptr(),
1035+
0,
1036+
)
1037+
});
1038+
match clonefile_result {
1039+
Ok(_) => return Ok(reader_metadata.len()),
1040+
Err(err) => match err.raw_os_error() {
1041+
// `fclonefileat` will fail on non-APFS volumes, if the
1042+
// destination already exists, or if the source and destination
1043+
// are on different devices. In all these cases `fcopyfile`
1044+
// should succeed.
1045+
Some(libc::ENOTSUP) | Some(libc::EEXIST) | Some(libc::EXDEV) => (),
1046+
Some(libc::ENOSYS) => HAS_FCLONEFILEAT.store(false, Ordering::Relaxed),
1047+
_ => return Err(err),
1048+
}
1049+
}
1050+
}
1051+
1052+
// Fall back to using `fcopyfile` if `fclonefileat` does not succeed.
1053+
let (writer, writer_metadata) = open_to_and_set_permissions(to, reader_metadata)?;
10041054

10051055
// We ensure that `FreeOnDrop` never contains a null pointer so it is
10061056
// always safe to call `copyfile_state_free`

0 commit comments

Comments
 (0)
Please sign in to comment.