dupe_krill/
reflink.rs

1use std::path::Path;
2use std::{fs, io};
3
4#[derive(Debug, Copy, Clone, Eq, PartialEq)]
5pub enum LinkType {
6    Hardlink,
7    Reflink,
8}
9
10/// Create a reflink (copy-on-write link) between two files
11/// Falls back to hardlinking if reflinking is not supported
12pub fn reflink_or_hardlink(src: &Path, dst: &Path) -> io::Result<LinkType> {
13    // Try reflink first
14    match reflink(src, dst) {
15        Ok(()) => Ok(LinkType::Reflink),
16        Err(_) => {
17            // Fall back to hardlink
18            fs::hard_link(src, dst)?;
19            Ok(LinkType::Hardlink)
20        }
21    }
22}
23
24/// Create a reflink (copy-on-write link) between two files
25pub fn reflink(src: &Path, dst: &Path) -> io::Result<()> {
26    #[cfg(target_os = "linux")]
27    {
28        reflink_linux(src, dst)
29    }
30    #[cfg(target_os = "macos")]
31    {
32        reflink_macos(src, dst)
33    }
34    #[cfg(target_os = "windows")]
35    {
36        reflink_windows(src, dst)
37    }
38    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
39    {
40        Err(io::Error::new(
41            io::ErrorKind::Unsupported,
42            "Reflinks are not supported on this platform",
43        ))
44    }
45}
46
47#[cfg(target_os = "linux")]
48fn reflink_linux(src: &Path, dst: &Path) -> io::Result<()> {
49    use std::ffi::CString;
50    use std::os::unix::ffi::OsStrExt;
51
52    let src_c = CString::new(src.as_os_str().as_bytes())?;
53    let dst_c = CString::new(dst.as_os_str().as_bytes())?;
54
55    unsafe {
56        // First try ioctl FICLONE (Btrfs, XFS)
57        let src_fd = libc::open(src_c.as_ptr(), libc::O_RDONLY);
58        if src_fd == -1 {
59            return Err(io::Error::last_os_error());
60        }
61
62        let dst_fd = libc::open(dst_c.as_ptr(), libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL, 0o644);
63        if dst_fd == -1 {
64            libc::close(src_fd);
65            return Err(io::Error::last_os_error());
66        }
67
68        // FICLONE ioctl constant - this creates a reflink
69        const FICLONE: libc::c_ulong = 0x40049409;
70        let result = libc::ioctl(dst_fd, FICLONE, src_fd);
71
72        libc::close(src_fd);
73        libc::close(dst_fd);
74
75        if result == 0 {
76            Ok(())
77        } else {
78            // Clean up the created file on failure
79            let _ = libc::unlink(dst_c.as_ptr());
80            Err(io::Error::last_os_error())
81        }
82    }
83}
84
85#[cfg(target_os = "macos")]
86fn reflink_macos(src: &Path, dst: &Path) -> io::Result<()> {
87    use std::ffi::CString;
88    use std::os::unix::ffi::OsStrExt;
89
90    let src_c = CString::new(src.as_os_str().as_bytes())?;
91    let dst_c = CString::new(dst.as_os_str().as_bytes())?;
92
93    // Use clonefile() on macOS
94    unsafe extern "C" {
95        fn clonefile(src: *const libc::c_char, dst: *const libc::c_char, flags: u32) -> libc::c_int;
96    }
97    unsafe {
98        let result = clonefile(src_c.as_ptr(), dst_c.as_ptr(), 0);
99        if result == 0 {
100            Ok(())
101        } else {
102            Err(io::Error::last_os_error())
103        }
104    }
105}
106
107#[cfg(target_os = "windows")]
108fn reflink_windows(src: &Path, dst: &Path) -> io::Result<()> {
109    use std::os::windows::ffi::OsStrExt;
110    use std::ptr;
111
112    // Convert paths to wide strings for Windows API
113    let src_wide: Vec<u16> = src.as_os_str().encode_wide().chain(Some(0)).collect();
114    let dst_wide: Vec<u16> = dst.as_os_str().encode_wide().chain(Some(0)).collect();
115
116    unsafe {
117        // Windows doesn't have a direct equivalent to FICLONE, but it's possible to
118        // use the CopyFile API with COPY_FILE_COPY_SYMLINK | COPY_FILE_CLONE_FORCE.
119        // This requires Windows 10 version 1903 or later with a ReFS filesystem
120
121        extern "system" {
122            fn CopyFileExW(
123                lpExistingFileName: *const u16,
124                lpNewFileName: *const u16,
125                lpProgressRoutine: *const u8,
126                lpData: *const u8,
127                pbCancel: *const i32,
128                dwCopyFlags: u32,
129            ) -> i32;
130        }
131
132        // COPY_FILE_CLONE_FORCE = 0x00800000 - Force a clone (reflink)
133        const COPY_FILE_CLONE_FORCE: u32 = 0x00800000;
134
135        let result = CopyFileExW(
136            src_wide.as_ptr(),
137            dst_wide.as_ptr(),
138            ptr::null(),
139            ptr::null(),
140            ptr::null(),
141            COPY_FILE_CLONE_FORCE,
142        );
143
144        if result != 0 {
145            Ok(())
146        } else {
147            Err(io::Error::last_os_error())
148        }
149    }
150}