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,
}