Skip to content

Commit

Permalink
Add a FileExt with copy_to
Browse files Browse the repository at this point in the history
Currently the rust stdlib wraps `copy_file_range` but just for
path copies.
  • Loading branch information
cgwalters committed Jul 24, 2020
1 parent 24c7b26 commit c377a54
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 2 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ documentation = "http://docs.rs/openat-ext"
[dependencies]
openat = "0.1.15"
libc = "0.2.34"
nix = "0.17"

[dev-dependencies]
tempfile = "3.0.3"
79 changes: 79 additions & 0 deletions src/copyfile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use nix;
use std::fs::File;
use std::io;

/// Helper functions for std::fs::File
pub trait FileExt {
/// Copy the entire contents of `self` to `to`. This uses operating system
/// specific fast paths if available.
fn copy_to(&mut self, to: &mut File) -> io::Result<u64>;
}

impl FileExt for File {
#[cfg(not(any(target_os = "linux", target_os = "android")))]
fn copy_to(&mut self, to: &mut File) -> io::Result<u64> {
return io::copy(self, &mut to);
}

// Derived from src/libstd/sys/unix/fs.rs in Rust
#[cfg(any(target_os = "linux", target_os = "android"))]
fn copy_to(&mut self, to: &mut File) -> io::Result<u64> {
use nix::fcntl::copy_file_range;
use nix::errno::Errno;
use std::os::unix::io::AsRawFd;
use std::sync::atomic::{AtomicBool, Ordering};

// Kernel prior to 4.5 don't have copy_file_range
// We store the availability in a global to avoid unnecessary syscalls
static HAS_COPY_FILE_RANGE: AtomicBool = AtomicBool::new(true);

let len = self.metadata()?.len();

let has_copy_file_range = HAS_COPY_FILE_RANGE.load(Ordering::Relaxed);
let mut written = 0u64;
while written < len {
let copy_result = if has_copy_file_range {
let bytes_to_copy = std::cmp::min(len - written, usize::MAX as u64) as usize;
// We actually don't have to adjust the offsets,
// because copy_file_range adjusts the file offset automatically
let copy_result =
copy_file_range(self.as_raw_fd(), None, to.as_raw_fd(), None, bytes_to_copy);
if let Err(ref copy_err) = copy_result {
match copy_err.as_errno() {
Some(Errno::ENOSYS) | Some(Errno::EPERM) => {
HAS_COPY_FILE_RANGE.store(false, Ordering::Relaxed);
}
_ => {}
}
}
copy_result
} else {
Err(nix::Error::from_errno(Errno::ENOSYS))
};
match copy_result {
Ok(ret) => written += ret as u64,
Err(err) => {
match err.as_errno() {
Some(os_err)
if os_err == Errno::ENOSYS
|| os_err == Errno::EXDEV
|| os_err == Errno::EINVAL
|| os_err == Errno::EPERM =>
{
// Try fallback io::copy if either:
// - Kernel version is < 4.5 (ENOSYS)
// - Files are mounted on different fs (EXDEV)
// - copy_file_range is disallowed, for example by seccomp (EPERM)
// - copy_file_range cannot be used with pipes or device nodes (EINVAL)
assert_eq!(written, 0);
return io::copy(self, to);
}
Some(os_err) => return Err(io::Error::from_raw_os_error(os_err as i32)),
_ => return Err(io::Error::new(io::ErrorKind::Other, err))
}
}
}
}
Ok(written)
}
}
11 changes: 9 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
//! # Extension methods for openat::Dir
//! # Extension methods for openat::Dir and std::fs::File
//!
//! ```
//! use openat_ext::*;
//! use openat_ext::OpenatDirExt;
//! ```
//!
//! The `openat` crate is a low-level API, generally just exposing
//! thin wrappers for the underlying system call. This crate offers
//! a number of common higher level convenience functions.
//!
//! More recently, there is also an `FileExt` available; it currently
//! just contains an optimized file copy method that will hopefully
//! go into the standard library.
use libc;
use openat;
use std::{fs, io};

mod copyfile;
pub use copyfile::FileExt;

/// Helper functions for openat::Dir
pub trait OpenatDirExt {
/// Checking for nonexistent files (`ENOENT`) is by far the most common case of inspecting error
Expand Down
17 changes: 17 additions & 0 deletions tests/basic.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use openat;
use openat_ext::*;
use std::{error, result};
use std::fs::File;
use tempfile;

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

Ok(())
}

#[test]
fn copy() -> Result<()> {
let td = tempfile::tempdir()?;
let src_p = td.path().join("testfile");
let dest_p = td.path().join("testfiledest");
let contents = "somefilecontents";
std::fs::write(&src_p, contents)?;
let mut src = File::open(&src_p)?;
{ let mut dest = File::create(&dest_p)?;
src.copy_to(&mut dest)?;
}
let testf_contents = std::fs::read_to_string(&dest_p)?;
assert_eq!(contents, testf_contents.as_str());
Ok(())
}

0 comments on commit c377a54

Please sign in to comment.