reflink_copy/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
//! Some file systems implement COW (copy on write) functionality in order to speed up file copies.
//! On a high level, the new file does not actually get copied, but shares the same on-disk data
//! with the source file. As soon as one of the files is modified, the actual copying is done by
//! the underlying OS.
//!
//! This library exposes a single function, `reflink`, which attempts to copy a file using the
//! underlying OSs' block cloning capabilities. The function signature is identical to `std::fs::copy`.
//!
//! At the moment Linux, Android, OSX, iOS, and Windows are supported.
//!
//! Note: On Windows, the integrity information features are only available on Windows Server editions
//! starting from Windows Server 2012. Client versions of Windows do not support these features.
//! [More Information](https://learn.microsoft.com/en-us/windows/win32/api/winioctl/ni-winioctl-fsctl_set_integrity_information)
//!
//! As soon as other OSes support the functionality, support will be added.
mod sys;
use std::fs;
use std::io;
use std::io::ErrorKind;
use std::path::Path;
/// Copies a file using COW semantics.
///
/// For compatibility reasons with macOS, the target file will be created using `OpenOptions::create_new`.
/// If you want to overwrite existing files, make sure you manually delete the target file first
/// if it exists.
///
/// ```rust
/// match reflink_copy::reflink("src.txt", "dest.txt") {
/// Ok(()) => println!("file has been reflinked"),
/// Err(e) => println!("error while reflinking: {:?}", e)
/// }
/// ```
///
/// # Implementation details per platform
///
/// ## Linux / Android
///
/// Uses `ioctl_ficlone`. Supported file systems include btrfs and XFS (and maybe more in the future).
/// NOTE that it generates a temporary file and is not atomic.
///
/// ## MacOS / OS X / iOS
///
/// Uses `clonefile` library function. This is supported on OS X Version >=10.12 and iOS version >= 10.0
/// This will work on APFS partitions (which means most desktop systems are capable).
/// If src names a directory, the directory hierarchy is cloned as if each item was cloned individually.
///
/// ## Windows
///
/// Uses ioctl `FSCTL_DUPLICATE_EXTENTS_TO_FILE`.
///
/// Supports ReFS on Windows Server and Windows Dev Drives. *Important note*: The windows implementation is currently
/// untested and probably buggy. Contributions/testers with access to a Windows Server or Dev Drives are welcome.
/// [More Information on Dev Drives](https://learn.microsoft.com/en-US/windows/dev-drive/#how-does-dev-drive-work)
///
/// NOTE that it generates a temporary file and is not atomic.
#[inline(always)]
pub fn reflink(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
#[cfg_attr(feature = "tracing", tracing_attributes::instrument(name = "reflink"))]
fn inner(from: &Path, to: &Path) -> io::Result<()> {
sys::reflink(from, to).map_err(|err| {
// Linux and Windows will return an inscrutable error when `from` is a directory or a
// symlink, so add the real problem to the error. We need to use `fs::symlink_metadata`
// here because `from.is_file()` traverses symlinks.
//
// According to https://www.manpagez.com/man/2/clonefile/, Macos otoh can reflink files,
// directories and symlinks, so the original error is fine.
if !cfg!(any(
target_os = "macos",
target_os = "ios",
target_os = "tvos",
target_os = "watchos"
)) && !fs::symlink_metadata(from).map_or(false, |m| m.is_file())
{
io::Error::new(
io::ErrorKind::InvalidInput,
format!("the source path is not an existing regular file: {}", err),
)
} else {
err
}
})
}
inner(from.as_ref(), to.as_ref())
}
/// Attempts to reflink a file. If the operation fails, a conventional copy operation is
/// attempted as a fallback.
///
/// If the function reflinked a file, the return value will be `Ok(None)`.
///
/// If the function copied a file, the return value will be `Ok(Some(written))`.
///
/// If target file already exists, operation fails with [`ErrorKind::AlreadyExists`].
///
/// ```rust
/// match reflink_copy::reflink_or_copy("src.txt", "dest.txt") {
/// Ok(None) => println!("file has been reflinked"),
/// Ok(Some(written)) => println!("file has been copied ({} bytes)", written),
/// Err(e) => println!("an error occured: {:?}", e)
/// }
/// ```
///
/// # Implementation details per platform
///
/// ## MacOS / OS X / iOS
///
/// If src names a directory, the directory hierarchy is cloned as if each item was cloned
/// individually. This method does not provide a fallback for directories, so the fallback will also
/// fail if reflinking failed. Macos supports reflinking symlinks, which is supported by the
/// fallback.
#[inline(always)]
pub fn reflink_or_copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<Option<u64>> {
#[cfg_attr(
feature = "tracing",
tracing_attributes::instrument(name = "reflink_or_copy")
)]
fn inner(from: &Path, to: &Path) -> io::Result<Option<u64>> {
if let Err(err) = sys::reflink(from, to) {
match err.kind() {
ErrorKind::NotFound | ErrorKind::PermissionDenied | ErrorKind::AlreadyExists => {
return Err(err);
}
_ => {}
}
#[cfg(feature = "tracing")]
tracing::warn!(?err, "Failed to reflink, fallback to fs::copy");
fs::copy(from, to).map(Some).map_err(|err| {
// Both regular files and symlinks to regular files can be copied, so unlike
// `reflink` we don't want to report invalid input on both files and symlinks
if from.is_file() {
err
} else {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("the source path is not an existing regular file: {}", err),
)
}
})
} else {
Ok(None)
}
}
inner(from.as_ref(), to.as_ref())
}
/// Checks whether reflink is supported on the filesystem for the specified source and target paths.
///
/// This function verifies that both paths are on the same volume and that the filesystem supports
/// reflink.
///
/// > Note: Currently the function works only for windows. It returns `Ok(ReflinkSupport::Unknown)`
/// > for any other platform.
///
/// # Example
/// ```
/// fn main() -> std::io::Result<()> {
/// let support = reflink_copy::check_reflink_support("C:\\path\\to\\file", "C:\\path\\to\\another_file")?;
/// println!("{support:?}");
/// let support = reflink_copy::check_reflink_support("path\\to\\folder", "path\\to\\another_folder")?;
/// println!("{support:?}");
/// Ok(())
/// }
/// ```
pub fn check_reflink_support(
from: impl AsRef<Path>,
to: impl AsRef<Path>,
) -> io::Result<ReflinkSupport> {
#[cfg(windows)]
return sys::check_reflink_support(from, to);
#[cfg(not(windows))]
Ok(ReflinkSupport::Unknown)
}
/// Enum indicating the reflink support status.
#[derive(Debug, PartialEq, Eq)]
pub enum ReflinkSupport {
/// Reflink is supported.
Supported,
/// Reflink is not supported.
NotSupported,
/// Reflink support is unconfirmed.
Unknown,
}