Skip to content

Commit 8576023

Browse files
committed
Add copy_file_range support to FileSystem trait
- Add copy_file_range method to FileSystem trait with default ENOSYS - Implement copy_file_range in PassthroughFs using libc syscall - Add test for copy_file_range with partial copy and offset support This enables efficient server-side copy operations on filesystems that support it (e.g., btrfs reflinks). When the underlying filesystem supports copy_file_range, copies can be instant O(1) operations instead of O(n) read+write.
1 parent da79781 commit 8576023

File tree

2 files changed

+138
-0
lines changed

2 files changed

+138
-0
lines changed

src/api/filesystem/sync_io.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,29 @@ pub trait FileSystem {
868868
Err(io::Error::from_raw_os_error(libc::ENOSYS))
869869
}
870870

871+
/// Copy a range of data from one file to another.
872+
///
873+
/// Performs an optimized copy between two file descriptors. On filesystems
874+
/// that support it (like btrfs), this creates a reflink (copy-on-write clone)
875+
/// which is nearly instantaneous regardless of file size.
876+
///
877+
/// Returns the number of bytes copied.
878+
#[allow(clippy::too_many_arguments)]
879+
fn copy_file_range(
880+
&self,
881+
ctx: &Context,
882+
inode_in: Self::Inode,
883+
handle_in: Self::Handle,
884+
offset_in: u64,
885+
inode_out: Self::Inode,
886+
handle_out: Self::Handle,
887+
offset_out: u64,
888+
len: u64,
889+
flags: u64,
890+
) -> io::Result<usize> {
891+
Err(io::Error::from_raw_os_error(libc::ENOSYS))
892+
}
893+
871894
/// send ioctl to the file
872895
#[allow(clippy::too_many_arguments)]
873896
fn ioctl(

src/passthrough/sync_io.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,6 +1425,47 @@ impl<S: BitmapSlice + Send + Sync> FileSystem for PassthroughFs<S> {
14251425
Ok(res as u64)
14261426
}
14271427
}
1428+
1429+
fn copy_file_range(
1430+
&self,
1431+
_ctx: &Context,
1432+
inode_in: Inode,
1433+
handle_in: Handle,
1434+
offset_in: u64,
1435+
inode_out: Inode,
1436+
handle_out: Handle,
1437+
offset_out: u64,
1438+
len: u64,
1439+
flags: u64,
1440+
) -> io::Result<usize> {
1441+
// Get file descriptors from handles
1442+
let data_in = self.handle_map.get(handle_in, inode_in)?;
1443+
let data_out = self.handle_map.get(handle_out, inode_out)?;
1444+
1445+
let (_guard_in, file_in) = data_in.get_file_mut();
1446+
let (_guard_out, file_out) = data_out.get_file_mut();
1447+
1448+
let mut off_in = offset_in as libc::off64_t;
1449+
let mut off_out = offset_out as libc::off64_t;
1450+
1451+
// Safe because we check the return value and the fds are valid
1452+
let result = unsafe {
1453+
libc::copy_file_range(
1454+
file_in.as_raw_fd(),
1455+
&mut off_in,
1456+
file_out.as_raw_fd(),
1457+
&mut off_out,
1458+
len as libc::size_t,
1459+
flags as libc::c_uint,
1460+
)
1461+
};
1462+
1463+
if result < 0 {
1464+
Err(io::Error::last_os_error())
1465+
} else {
1466+
Ok(result as usize)
1467+
}
1468+
}
14281469
}
14291470

14301471
#[cfg(test)]
@@ -1743,4 +1784,78 @@ mod tests {
17431784

17441785
assert!(fs.fsyncdir(&ctx, ROOT_ID, false, 0).is_ok());
17451786
}
1787+
1788+
#[test]
1789+
fn test_copy_file_range() {
1790+
let (fs, source) = prepare_fs_tmpdir();
1791+
let ctx = prepare_context();
1792+
1793+
// Create source file with data (using std::fs for simplicity)
1794+
let test_data = b"Hello, copy_file_range!";
1795+
let src_path = source.as_path().join("source.txt");
1796+
let dst_path = source.as_path().join("dest.txt");
1797+
std::fs::write(&src_path, test_data).unwrap();
1798+
std::fs::write(&dst_path, b"").unwrap(); // Create empty destination
1799+
1800+
// Look up and open both files through the passthrough fs
1801+
let src_name = CString::new("source.txt").unwrap();
1802+
let src_entry = fs.lookup(&ctx, ROOT_ID, &src_name).unwrap();
1803+
let (src_handle, _, _) = fs
1804+
.open(&ctx, src_entry.inode, libc::O_RDWR as u32, 0)
1805+
.unwrap();
1806+
let src_handle = src_handle.expect("expected src handle");
1807+
1808+
let dst_name = CString::new("dest.txt").unwrap();
1809+
let dst_entry = fs.lookup(&ctx, ROOT_ID, &dst_name).unwrap();
1810+
let (dst_handle, _, _) = fs
1811+
.open(&ctx, dst_entry.inode, libc::O_RDWR as u32, 0)
1812+
.unwrap();
1813+
let dst_handle = dst_handle.expect("expected dst handle");
1814+
1815+
// Copy data from source to destination using copy_file_range
1816+
let copied = fs
1817+
.copy_file_range(
1818+
&ctx,
1819+
src_entry.inode,
1820+
src_handle,
1821+
0, // offset_in
1822+
dst_entry.inode,
1823+
dst_handle,
1824+
0, // offset_out
1825+
test_data.len() as u64,
1826+
0, // flags
1827+
)
1828+
.unwrap();
1829+
assert_eq!(copied, test_data.len());
1830+
1831+
// Sync and verify by reading directly from disk
1832+
fs.fsync(&ctx, dst_entry.inode, false, dst_handle).unwrap();
1833+
let result = std::fs::read(&dst_path).unwrap();
1834+
assert_eq!(&result, test_data);
1835+
1836+
// Test partial copy with offset
1837+
let offset = 7; // "Hello, " is 7 bytes
1838+
let partial_len = test_data.len() - offset;
1839+
let copied = fs
1840+
.copy_file_range(
1841+
&ctx,
1842+
src_entry.inode,
1843+
src_handle,
1844+
offset as u64,
1845+
dst_entry.inode,
1846+
dst_handle,
1847+
test_data.len() as u64, // append after existing data
1848+
partial_len as u64,
1849+
0,
1850+
)
1851+
.unwrap();
1852+
assert_eq!(copied, partial_len);
1853+
1854+
// Verify the appended data
1855+
fs.fsync(&ctx, dst_entry.inode, false, dst_handle).unwrap();
1856+
let result = std::fs::read(&dst_path).unwrap();
1857+
assert_eq!(result.len(), test_data.len() + partial_len);
1858+
assert_eq!(&result[..test_data.len()], test_data);
1859+
assert_eq!(&result[test_data.len()..], &test_data[offset..]);
1860+
}
17461861
}

0 commit comments

Comments
 (0)