1#[cfg(not(windows))]
7use std::fs::File;
8use std::io;
9#[cfg(not(windows))]
10use std::io::{BufReader, BufWriter};
11use std::path::Path;
12
13use crate::util::AlignedBuf;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum ReflinkMode {
18 #[default]
20 Auto,
21 Always,
23 Never,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum FsyncMode {
30 #[default]
32 Auto,
33 Always,
35 Never,
37}
38
39const DEFAULT_BUF: usize = 1 << 20;
41
42pub fn copy_file_into(
55 src: &Path,
56 tmp: &Path,
57 reflink: ReflinkMode,
58 sparse: bool,
59) -> io::Result<u64> {
60 copy_file_into_sized(src, tmp, reflink, sparse, DEFAULT_BUF)
61}
62
63pub fn copy_file_into_sized(
70 src: &Path,
71 tmp: &Path,
72 reflink: ReflinkMode,
73 sparse: bool,
74 buffer_size: usize,
75) -> io::Result<u64> {
76 #[cfg(not(windows))]
79 {
80 copy_file_into_portable(src, tmp, reflink, sparse, buffer_size)
81 }
82 #[cfg(windows)]
83 {
84 let _ = sparse;
85 let _ = buffer_size;
86 crate::io::windows::copy_file_into(src, tmp, reflink)
87 }
88}
89
90#[cfg(not(windows))]
93fn copy_file_into_portable(
94 src: &Path,
95 tmp: &Path,
96 reflink: ReflinkMode,
97 sparse: bool,
98 buffer_size: usize,
99) -> io::Result<u64> {
100 if reflink != ReflinkMode::Never {
102 match reflink_copy::reflink(src, tmp) {
103 Ok(()) => return std::fs::metadata(tmp).map(|m| m.len()),
104 Err(e) => {
105 let _ = std::fs::remove_file(tmp);
106 if reflink == ReflinkMode::Always {
107 return Err(e);
108 }
109 }
110 }
111 }
112
113 #[cfg(any(
115 target_os = "linux",
116 target_os = "macos",
117 target_os = "ios",
118 target_os = "freebsd",
119 target_os = "dragonfly",
120 target_os = "solaris"
121 ))]
122 if sparse {
123 match sparse_copy_sized(src, tmp, buffer_size) {
124 Ok(n) => return Ok(n),
125 Err(_) => {
126 let _ = std::fs::remove_file(tmp);
127 }
128 }
129 }
130
131 #[cfg(target_os = "macos")]
133 {
134 match macos_fcopyfile_copy(src, tmp) {
135 Ok(n) => return Ok(n),
136 Err(_) => {
137 let _ = std::fs::remove_file(tmp);
138 }
139 }
140 }
141
142 #[cfg(all(target_os = "linux", feature = "io-uring"))]
144 {
145 let file_len = std::fs::metadata(src).map(|m| m.len()).unwrap_or(0);
146 if file_len > 1_048_576 && file_len <= 67_108_864 {
147 match crate::io::uring::copy_single_large(src, tmp, file_len) {
148 Ok(n) => return Ok(n),
149 Err(_) => {
150 let _ = std::fs::remove_file(tmp);
151 }
152 }
153 }
154 }
155
156 #[cfg(target_os = "linux")]
158 {
159 match copy_file_range_all(src, tmp) {
160 Ok(n) => return Ok(n),
161 Err(_) => {
162 let _ = std::fs::remove_file(tmp);
163 }
164 }
165 }
166
167 buffered_copy_sized(src, tmp, buffer_size)
169}
170
171#[cfg(target_os = "macos")]
177fn macos_fcopyfile_copy(src: &Path, tmp: &Path) -> io::Result<u64> {
178 use std::os::unix::io::AsRawFd;
179
180 let infile = File::open(src)?;
181 let len = infile.metadata()?.len();
182
183 if len >= 8 * 1024 * 1024 {
185 let _ = crate::io::macos::set_nocache(infile.as_raw_fd());
186 }
187
188 let outfile = std::fs::OpenOptions::new()
189 .write(true)
190 .create_new(true)
191 .open(tmp)?;
192
193 if len >= 8 * 1024 * 1024 {
194 let _ = crate::io::macos::set_nocache(outfile.as_raw_fd());
195 }
196
197 crate::io::macos::copy_file_with_fcopyfile(infile.as_raw_fd(), outfile.as_raw_fd())
198}
199
200#[cfg(any(
206 target_os = "linux",
207 target_os = "macos",
208 target_os = "ios",
209 target_os = "freebsd",
210 target_os = "dragonfly",
211 target_os = "solaris"
212))]
213fn sparse_copy_sized(src: &Path, tmp: &Path, buffer_size: usize) -> io::Result<u64> {
214 use std::os::unix::fs::FileExt;
215
216 use rustix::fs::SeekFrom;
217
218 let infile = File::open(src)?;
219 let outfile = File::create(tmp)?;
220 let len = infile.metadata()?.len();
221 outfile.set_len(len)?;
222
223 let mut offset = 0_u64;
224 let mut buf = AlignedBuf::new(buffer_size);
225 let mut bytes_written = 0_u64;
226 while offset < len {
227 let data = match rustix::fs::seek(&infile, SeekFrom::Data(offset)) {
228 Ok(data) => data,
229 Err(error) if error == rustix::io::Errno::NXIO => break,
230 Err(error) => return Err(io::Error::from(error)),
231 };
232 let hole = rustix::fs::seek(&infile, SeekFrom::Hole(data))?.min(len);
233 let mut position = data;
234 while position < hole {
235 let wanted = usize::try_from((hole - position).min(buf.len() as u64))
236 .unwrap_or(buf.len());
237 let read = infile.read_at(&mut buf[..wanted], position)?;
238 if read == 0 {
239 return Err(io::Error::new(
240 io::ErrorKind::UnexpectedEof,
241 "short sparse extent read",
242 ));
243 }
244 let mut written = 0;
245 while written < read {
246 let count =
247 outfile.write_at(&buf[written..read], position + written as u64)?;
248 if count == 0 {
249 return Err(io::Error::new(
250 io::ErrorKind::WriteZero,
251 "short sparse extent write",
252 ));
253 }
254 written += count;
255 }
256 position += read as u64;
257 }
258 bytes_written = hole;
259 offset = hole;
260 }
261 outfile.set_len(bytes_written)?;
262 Ok(bytes_written)
263}
264
265#[cfg(not(windows))]
268fn advise_sequential(file: &File, len: u64) {
269 #[cfg(target_os = "linux")]
270 {
271 use rustix::fs::{Advice, fadvise};
272 if len >= 8 * 1024 * 1024 {
273 let span = std::num::NonZeroU64::new(len);
274 let _ = fadvise(file, 0, span, Advice::Sequential);
275 let _ = fadvise(file, 0, span, Advice::WillNeed);
276 }
277 }
278 #[cfg(not(target_os = "linux"))]
279 let _ = (file, len);
280}
281
282#[cfg(not(windows))]
284fn buffered_copy_sized(src: &Path, tmp: &Path, buffer_size: usize) -> io::Result<u64> {
285 let reader = File::open(src)?;
286 let writer = File::create(tmp)?;
287 let file_len = reader.metadata()?.len();
288 advise_sequential(&reader, file_len);
289
290 #[cfg(target_os = "macos")]
292 if file_len >= 8 * 1024 * 1024 {
293 use std::os::unix::io::AsRawFd;
294 let _ = crate::io::macos::set_nocache(reader.as_raw_fd());
295 let _ = crate::io::macos::set_nocache(writer.as_raw_fd());
296 }
297
298 let mut r = BufReader::with_capacity(buffer_size, reader);
299 let mut w = BufWriter::with_capacity(buffer_size, writer);
300 let n = io::copy(&mut r, &mut w)?;
301 w.into_inner()?;
309 Ok(n)
310}
311
312#[cfg(target_os = "linux")]
314fn copy_file_range_all(src: &Path, tmp: &Path) -> io::Result<u64> {
315 let infile = File::open(src)?;
316 let outfile = File::create(tmp)?;
317 let len = infile.metadata()?.len();
318 advise_sequential(&infile, len);
319 let mut remaining = len;
320 while remaining > 0 {
321 let chunk = usize::try_from(remaining.min(1 << 30)).unwrap_or(usize::MAX);
322 let copied = rustix::fs::copy_file_range(&infile, None, &outfile, None, chunk)?;
323 if copied == 0 {
324 return Err(io::Error::new(
325 io::ErrorKind::UnexpectedEof,
326 "source file truncated during copy",
327 ));
328 }
329 remaining -= copied as u64;
330 }
331 Ok(len - remaining)
332}