Skip to content

Commit c377a54

Browse files
committed
Add a FileExt with copy_to
Currently the rust stdlib wraps `copy_file_range` but just for path copies.
1 parent 24c7b26 commit c377a54

File tree

4 files changed

+106
-2
lines changed

4 files changed

+106
-2
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ documentation = "http://docs.rs/openat-ext"
1616
[dependencies]
1717
openat = "0.1.15"
1818
libc = "0.2.34"
19+
nix = "0.17"
1920

2021
[dev-dependencies]
2122
tempfile = "3.0.3"

src/copyfile.rs

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use nix;
2+
use std::fs::File;
3+
use std::io;
4+
5+
/// Helper functions for std::fs::File
6+
pub trait FileExt {
7+
/// Copy the entire contents of `self` to `to`. This uses operating system
8+
/// specific fast paths if available.
9+
fn copy_to(&mut self, to: &mut File) -> io::Result<u64>;
10+
}
11+
12+
impl FileExt for File {
13+
#[cfg(not(any(target_os = "linux", target_os = "android")))]
14+
fn copy_to(&mut self, to: &mut File) -> io::Result<u64> {
15+
return io::copy(self, &mut to);
16+
}
17+
18+
// Derived from src/libstd/sys/unix/fs.rs in Rust
19+
#[cfg(any(target_os = "linux", target_os = "android"))]
20+
fn copy_to(&mut self, to: &mut File) -> io::Result<u64> {
21+
use nix::fcntl::copy_file_range;
22+
use nix::errno::Errno;
23+
use std::os::unix::io::AsRawFd;
24+
use std::sync::atomic::{AtomicBool, Ordering};
25+
26+
// Kernel prior to 4.5 don't have copy_file_range
27+
// We store the availability in a global to avoid unnecessary syscalls
28+
static HAS_COPY_FILE_RANGE: AtomicBool = AtomicBool::new(true);
29+
30+
let len = self.metadata()?.len();
31+
32+
let has_copy_file_range = HAS_COPY_FILE_RANGE.load(Ordering::Relaxed);
33+
let mut written = 0u64;
34+
while written < len {
35+
let copy_result = if has_copy_file_range {
36+
let bytes_to_copy = std::cmp::min(len - written, usize::MAX as u64) as usize;
37+
// We actually don't have to adjust the offsets,
38+
// because copy_file_range adjusts the file offset automatically
39+
let copy_result =
40+
copy_file_range(self.as_raw_fd(), None, to.as_raw_fd(), None, bytes_to_copy);
41+
if let Err(ref copy_err) = copy_result {
42+
match copy_err.as_errno() {
43+
Some(Errno::ENOSYS) | Some(Errno::EPERM) => {
44+
HAS_COPY_FILE_RANGE.store(false, Ordering::Relaxed);
45+
}
46+
_ => {}
47+
}
48+
}
49+
copy_result
50+
} else {
51+
Err(nix::Error::from_errno(Errno::ENOSYS))
52+
};
53+
match copy_result {
54+
Ok(ret) => written += ret as u64,
55+
Err(err) => {
56+
match err.as_errno() {
57+
Some(os_err)
58+
if os_err == Errno::ENOSYS
59+
|| os_err == Errno::EXDEV
60+
|| os_err == Errno::EINVAL
61+
|| os_err == Errno::EPERM =>
62+
{
63+
// Try fallback io::copy if either:
64+
// - Kernel version is < 4.5 (ENOSYS)
65+
// - Files are mounted on different fs (EXDEV)
66+
// - copy_file_range is disallowed, for example by seccomp (EPERM)
67+
// - copy_file_range cannot be used with pipes or device nodes (EINVAL)
68+
assert_eq!(written, 0);
69+
return io::copy(self, to);
70+
}
71+
Some(os_err) => return Err(io::Error::from_raw_os_error(os_err as i32)),
72+
_ => return Err(io::Error::new(io::ErrorKind::Other, err))
73+
}
74+
}
75+
}
76+
}
77+
Ok(written)
78+
}
79+
}

src/lib.rs

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
1-
//! # Extension methods for openat::Dir
1+
//! # Extension methods for openat::Dir and std::fs::File
22
//!
33
//! ```
4-
//! use openat_ext::*;
4+
//! use openat_ext::OpenatDirExt;
55
//! ```
66
//!
77
//! The `openat` crate is a low-level API, generally just exposing
88
//! thin wrappers for the underlying system call. This crate offers
99
//! a number of common higher level convenience functions.
10+
//!
11+
//! More recently, there is also an `FileExt` available; it currently
12+
//! just contains an optimized file copy method that will hopefully
13+
//! go into the standard library.
1014
1115
use libc;
1216
use openat;
1317
use std::{fs, io};
1418

19+
mod copyfile;
20+
pub use copyfile::FileExt;
21+
1522
/// Helper functions for openat::Dir
1623
pub trait OpenatDirExt {
1724
/// Checking for nonexistent files (`ENOENT`) is by far the most common case of inspecting error

tests/basic.rs

+17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use openat;
22
use openat_ext::*;
33
use std::{error, result};
4+
use std::fs::File;
45
use tempfile;
56

67
type Result<T> = result::Result<T, Box<dyn error::Error>>;
@@ -43,3 +44,19 @@ fn exists() -> Result<()> {
4344

4445
Ok(())
4546
}
47+
48+
#[test]
49+
fn copy() -> Result<()> {
50+
let td = tempfile::tempdir()?;
51+
let src_p = td.path().join("testfile");
52+
let dest_p = td.path().join("testfiledest");
53+
let contents = "somefilecontents";
54+
std::fs::write(&src_p, contents)?;
55+
let mut src = File::open(&src_p)?;
56+
{ let mut dest = File::create(&dest_p)?;
57+
src.copy_to(&mut dest)?;
58+
}
59+
let testf_contents = std::fs::read_to_string(&dest_p)?;
60+
assert_eq!(contents, testf_contents.as_str());
61+
Ok(())
62+
}

0 commit comments

Comments
 (0)