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;