reflink_copy/
lib.rs

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