1use crate::error::Result;
9use parking_lot::Mutex;
10use std::collections::HashMap;
11use std::fs::{File, OpenOptions};
12use std::io;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15
16pub const PART_SUFFIX: &str = ".rst-part";
18
19pub struct WriteHandle {
21 file: File,
22 part_path: PathBuf,
23 final_path: PathBuf,
24 size: u64,
25 created_new: bool,
29}
30
31impl WriteHandle {
32 pub fn open_cloned(final_path: &Path, size: u64, preallocate: bool) -> Result<Self> {
43 let part_path = part_path_for(final_path);
44 if !part_path.exists() && final_path.exists() && clone_file(final_path, &part_path) {
45 if let Ok(file) = OpenOptions::new().read(true).write(true).open(&part_path) {
46 if file.metadata()?.len() != size {
47 file.set_len(size)?;
48 }
49 return Ok(Self {
50 file,
51 part_path,
52 final_path: final_path.to_path_buf(),
53 size,
54 created_new: false,
58 });
59 }
60 }
61 Self::open(final_path, size, preallocate)
62 }
63
64 pub fn open(final_path: &Path, size: u64, preallocate: bool) -> Result<Self> {
66 if let Some(parent) = final_path.parent() {
67 std::fs::create_dir_all(parent)
68 .map_err(|e| io::Error::new(e.kind(), format!("{}: {e}", parent.display())))?;
69 }
70 let part_path = part_path_for(final_path);
71 let (file, created_new) = match OpenOptions::new()
75 .create_new(true)
76 .read(true)
77 .write(true)
78 .open(&part_path)
79 {
80 Ok(f) => (f, true),
81 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
82 let f = OpenOptions::new()
83 .read(true)
84 .write(true)
85 .truncate(false)
86 .open(&part_path)
87 .map_err(|e| {
88 io::Error::new(e.kind(), format!("{}: {e}", part_path.display()))
89 })?;
90 (f, false)
91 }
92 Err(e) => {
93 return Err(
94 io::Error::new(e.kind(), format!("{}: {e}", part_path.display())).into(),
95 )
96 }
97 };
98
99 if preallocate && size > 0 && created_new {
108 reserve(&file, size)?;
109 }
110 if file.metadata()?.len() != size {
113 file.set_len(size)?;
114 }
115
116 Ok(Self {
117 file,
118 part_path,
119 final_path: final_path.to_path_buf(),
120 size,
121 created_new,
122 })
123 }
124
125 pub fn size(&self) -> u64 {
126 self.size
127 }
128
129 pub fn part_path(&self) -> &Path {
130 &self.part_path
131 }
132
133 pub fn created_new(&self) -> bool {
136 self.created_new
137 }
138
139 pub fn write_zeros_at(&self, offset: u64, len: usize) -> io::Result<()> {
141 const BLOCK: usize = 256 * 1024;
142 let zeros = [0u8; BLOCK];
143 let mut done = 0usize;
144 while done < len {
145 let n = BLOCK.min(len - done);
146 self.write_at(offset + done as u64, &zeros[..n])?;
147 done += n;
148 }
149 Ok(())
150 }
151
152 pub fn write_at(&self, offset: u64, buf: &[u8]) -> io::Result<()> {
154 let end = offset
157 .checked_add(buf.len() as u64)
158 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "write offset overflow"))?;
159 if end > self.size {
160 return Err(io::Error::new(
161 io::ErrorKind::InvalidInput,
162 format!(
163 "write of {} bytes at {offset} exceeds declared size {}",
164 buf.len(),
165 self.size
166 ),
167 ));
168 }
169 let mut written = 0usize;
170 while written < buf.len() {
171 let n = pwrite(&self.file, &buf[written..], offset + written as u64)?;
172 if n == 0 {
173 return Err(io::Error::new(
174 io::ErrorKind::WriteZero,
175 "pwrite made no progress",
176 ));
177 }
178 written += n;
179 }
180 Ok(())
181 }
182
183 pub fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
184 pread(&self.file, buf, offset)
185 }
186
187 pub fn matches_at(&self, offset: u64, want: &[u8]) -> io::Result<bool> {
193 let mut scratch = vec![0u8; want.len()];
194 let n = pread(&self.file, &mut scratch, offset)?;
195 Ok(n == want.len() && scratch == want)
196 }
197
198 pub fn sync(&self) -> io::Result<()> {
201 self.file.sync_all()
202 }
203
204 pub fn commit(&self, mode: u32, mtime: i64, preserve_metadata: bool) -> Result<()> {
212 self.file.sync_all()?;
213 if preserve_metadata {
214 #[cfg(unix)]
215 if mode != 0 {
216 use std::os::unix::fs::PermissionsExt;
217 let _ = self
218 .file
219 .set_permissions(std::fs::Permissions::from_mode(mode & 0o7777));
220 }
221 }
222
223 std::fs::rename(&self.part_path, &self.final_path).map_err(|e| {
224 io::Error::new(
225 e.kind(),
226 format!(
227 "rename {} -> {}: {e}",
228 self.part_path.display(),
229 self.final_path.display()
230 ),
231 )
232 })?;
233
234 if preserve_metadata && mtime > 0 {
235 set_mtime(&self.final_path, mtime);
236 }
237 Ok(())
238 }
239
240 pub fn discard(&self) {
242 let _ = std::fs::remove_file(&self.part_path);
243 }
244}
245
246#[allow(clippy::needless_return)]
251fn clone_file(src: &Path, dst: &Path) -> bool {
252 #[cfg(target_os = "macos")]
253 {
254 use std::ffi::CString;
255 let (Ok(s), Ok(d)) = (
256 CString::new(src.as_os_str().as_encoded_bytes()),
257 CString::new(dst.as_os_str().as_encoded_bytes()),
258 ) else {
259 return false;
260 };
261 return unsafe { libc::clonefile(s.as_ptr(), d.as_ptr(), 0) } == 0;
263 }
264 #[cfg(target_os = "linux")]
265 {
266 use std::os::unix::io::AsRawFd;
267 const FICLONE: libc::c_ulong = 0x4004_9409;
269 let (Ok(from), Ok(to)) = (
270 std::fs::File::open(src),
271 OpenOptions::new().create_new(true).write(true).open(dst),
272 ) else {
273 return false;
274 };
275 let ok = unsafe { libc::ioctl(to.as_raw_fd(), FICLONE, from.as_raw_fd()) } == 0;
276 if !ok {
277 drop(to);
278 let _ = std::fs::remove_file(dst);
279 }
280 ok
281 }
282 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
283 {
284 let _ = (src, dst);
285 false
286 }
287}
288
289pub fn part_path_for(final_path: &Path) -> PathBuf {
290 let mut s = final_path.as_os_str().to_os_string();
291 s.push(PART_SUFFIX);
292 PathBuf::from(s)
293}
294
295#[allow(clippy::needless_return)]
305fn reserve(file: &File, size: u64) -> Result<()> {
306 #[cfg(target_os = "linux")]
307 {
308 use std::os::unix::io::AsRawFd;
309 let rc = unsafe {
311 libc::fallocate(
312 file.as_raw_fd(),
313 libc::FALLOC_FL_KEEP_SIZE,
314 0,
315 size as libc::off_t,
316 )
317 };
318 if rc != 0 {
319 let e = io::Error::last_os_error();
320 match e.raw_os_error() {
321 Some(libc::EOPNOTSUPP) | Some(libc::ENOSYS) | Some(libc::EINVAL) => {}
324 _ => return Err(crate::error::Error::Io(e)),
326 }
327 }
328 return Ok(());
329 }
330 #[cfg(target_os = "macos")]
331 {
332 use std::os::unix::fs::MetadataExt;
333 use std::os::unix::io::AsRawFd;
334
335 let allocated = file.metadata()?.blocks().saturating_mul(512);
341 if allocated >= size {
342 return Ok(());
343 }
344 let want = (size - allocated) as libc::off_t;
345
346 let mut store = libc::fstore_t {
347 fst_flags: libc::F_ALLOCATECONTIG,
348 fst_posmode: libc::F_PEOFPOSMODE,
349 fst_offset: 0,
350 fst_length: want,
351 fst_bytesalloc: 0,
352 };
353 unsafe {
354 if libc::fcntl(file.as_raw_fd(), libc::F_PREALLOCATE, &mut store) == -1 {
356 store.fst_flags = libc::F_ALLOCATEALL;
357 let _ = libc::fcntl(file.as_raw_fd(), libc::F_PREALLOCATE, &mut store);
358 }
359 }
360 return Ok(());
361 }
362 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
363 {
364 let _ = (file, size);
365 Ok(())
366 }
367}
368
369fn set_mtime(path: &Path, mtime: i64) {
370 #[cfg(unix)]
371 {
372 use std::ffi::CString;
373 if let Ok(c) = CString::new(path.as_os_str().as_encoded_bytes()) {
374 let times = [
375 libc::timeval {
376 tv_sec: mtime as libc::time_t,
377 tv_usec: 0,
378 },
379 libc::timeval {
380 tv_sec: mtime as libc::time_t,
381 tv_usec: 0,
382 },
383 ];
384 unsafe {
385 libc::utimes(c.as_ptr(), times.as_ptr());
386 }
387 }
388 }
389 #[cfg(not(unix))]
390 let _ = (path, mtime);
391}
392
393#[cfg(unix)]
394fn pwrite(file: &File, buf: &[u8], offset: u64) -> io::Result<usize> {
395 use std::os::unix::fs::FileExt;
396 file.write_at(buf, offset)
397}
398
399#[cfg(windows)]
400fn pwrite(file: &File, buf: &[u8], offset: u64) -> io::Result<usize> {
401 use std::os::windows::fs::FileExt;
402 file.seek_write(buf, offset)
403}
404
405#[cfg(unix)]
406fn pread(file: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
407 use std::os::unix::fs::FileExt;
408 file.read_at(buf, offset)
409}
410
411#[cfg(windows)]
412fn pread(file: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
413 use std::os::windows::fs::FileExt;
414 file.seek_read(buf, offset)
415}
416
417#[derive(Default)]
423struct Slot(Mutex<Option<Arc<WriteHandle>>>);
424
425#[derive(Default)]
431pub struct FileWriters {
432 map: Mutex<HashMap<u32, Arc<Slot>>>,
433}
434
435impl FileWriters {
436 pub fn new() -> Self {
437 Self::default()
438 }
439
440 pub fn get_or_open<F>(&self, file_id: u32, make: F) -> Result<Arc<WriteHandle>>
451 where
452 F: FnOnce() -> Result<WriteHandle>,
453 {
454 let slot = {
455 let mut map = self.map.lock();
456 map.entry(file_id).or_default().clone()
457 };
458 let mut guard = slot.0.lock();
459 if let Some(h) = guard.as_ref() {
460 return Ok(h.clone());
461 }
462 let handle = Arc::new(make()?);
463 *guard = Some(handle.clone());
464 Ok(handle)
465 }
466
467 pub fn take(&self, file_id: u32) -> Option<Arc<WriteHandle>> {
468 let slot = self.map.lock().remove(&file_id)?;
469 let mut guard = slot.0.lock();
470 guard.take()
471 }
472
473 pub fn len(&self) -> usize {
474 self.map
475 .lock()
476 .values()
477 .filter(|s| s.0.lock().is_some())
478 .count()
479 }
480
481 pub fn is_empty(&self) -> bool {
482 self.len() == 0
483 }
484
485 pub fn discard_all(&self) {
487 let slots: Vec<_> = self.map.lock().drain().map(|(_, s)| s).collect();
488 for s in slots {
489 if let Some(h) = s.0.lock().take() {
490 h.discard();
491 }
492 }
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn writes_out_of_order_then_commits() {
502 let tmp = tempfile::tempdir().unwrap();
503 let dest = tmp.path().join("nested/dir/out.bin");
504 let h = WriteHandle::open(&dest, 3000, true).unwrap();
505
506 h.write_at(2000, &[3u8; 1000]).unwrap();
508 h.write_at(0, &[1u8; 1000]).unwrap();
509 h.write_at(1000, &[2u8; 1000]).unwrap();
510
511 assert!(h.part_path().exists());
512 assert!(!dest.exists(), "must not appear until committed");
513
514 h.commit(0o640, 1_600_000_000, true).unwrap();
515 assert!(dest.exists());
516 assert!(!part_path_for(&dest).exists());
517
518 let got = std::fs::read(&dest).unwrap();
519 assert_eq!(got.len(), 3000);
520 assert_eq!(&got[0..1000], &[1u8; 1000]);
521 assert_eq!(&got[1000..2000], &[2u8; 1000]);
522 assert_eq!(&got[2000..3000], &[3u8; 1000]);
523
524 #[cfg(unix)]
525 {
526 use std::os::unix::fs::PermissionsExt;
527 let m = std::fs::metadata(&dest).unwrap();
528 assert_eq!(m.permissions().mode() & 0o777, 0o640);
529 }
530 }
531
532 #[test]
533 fn refuses_writes_past_declared_size() {
534 let tmp = tempfile::tempdir().unwrap();
535 let h = WriteHandle::open(&tmp.path().join("a.bin"), 100, false).unwrap();
536 assert!(h.write_at(90, &[0u8; 20]).is_err());
537 assert!(h.write_at(u64::MAX, &[0u8; 1]).is_err());
538 h.write_at(90, &[0u8; 10]).unwrap();
539 }
540
541 #[test]
542 fn concurrent_writers_share_one_handle() {
543 let tmp = tempfile::tempdir().unwrap();
544 let dest = tmp.path().join("big.bin");
545 let h = Arc::new(WriteHandle::open(&dest, 64 * 4096, true).unwrap());
546
547 let threads: Vec<_> = (0..8u64)
548 .map(|t| {
549 let h = h.clone();
550 std::thread::spawn(move || {
551 for i in 0..8u64 {
552 let idx = t * 8 + i;
553 h.write_at(idx * 4096, &vec![idx as u8; 4096]).unwrap();
554 }
555 })
556 })
557 .collect();
558 for t in threads {
559 t.join().unwrap();
560 }
561
562 h.commit(0o644, 0, true).unwrap();
563 let got = std::fs::read(&dest).unwrap();
564 for idx in 0..64u64 {
565 let s = (idx * 4096) as usize;
566 assert!(
567 got[s..s + 4096].iter().all(|&b| b == idx as u8),
568 "chunk {idx}"
569 );
570 }
571 }
572
573 #[test]
574 fn reopening_a_partial_preserves_bytes() {
575 let tmp = tempfile::tempdir().unwrap();
576 let dest = tmp.path().join("resume.bin");
577 {
578 let h = WriteHandle::open(&dest, 8192, true).unwrap();
579 h.write_at(0, &[9u8; 4096]).unwrap();
580 h.sync().unwrap();
581 }
582 let h = WriteHandle::open(&dest, 8192, true).unwrap();
584 let mut buf = [0u8; 4096];
585 h.read_at(0, &mut buf).unwrap();
586 assert!(buf.iter().all(|&b| b == 9));
587 }
588
589 #[test]
590 fn registry_opens_once_under_contention() {
591 let tmp = tempfile::tempdir().unwrap();
592 let reg = Arc::new(FileWriters::new());
593 let opens = Arc::new(std::sync::atomic::AtomicUsize::new(0));
594
595 let threads: Vec<_> = (0..16)
596 .map(|_| {
597 let reg = reg.clone();
598 let opens = opens.clone();
599 let path = tmp.path().join("shared.bin");
600 std::thread::spawn(move || {
601 reg.get_or_open(1, || {
602 opens.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
603 WriteHandle::open(&path, 1024, false)
604 })
605 .unwrap()
606 })
607 })
608 .collect();
609 let handles: Vec<_> = threads.into_iter().map(|t| t.join().unwrap()).collect();
610
611 for h in &handles {
613 assert!(Arc::ptr_eq(h, &handles[0]));
614 }
615 assert_eq!(reg.len(), 1);
616 assert_eq!(
620 opens.load(std::sync::atomic::Ordering::SeqCst),
621 1,
622 "the destination was opened more than once"
623 );
624 }
625
626 #[cfg(unix)]
627 fn allocated_bytes(p: &Path) -> u64 {
628 use std::os::unix::fs::MetadataExt;
629 std::fs::metadata(p)
630 .map(|m| m.blocks().saturating_mul(512))
631 .unwrap_or(0)
632 }
633
634 #[cfg(unix)]
640 #[test]
641 fn preallocation_reserves_exactly_the_file_size() {
642 let tmp = tempfile::tempdir().unwrap();
643 let dest = tmp.path().join("prealloc.bin");
644 let size = 64 * 1024 * 1024;
645
646 {
647 let h = WriteHandle::open(&dest, size, true).unwrap();
648 h.write_at(0, &[1u8; 4096]).unwrap();
649 h.sync().unwrap();
650 }
651 let after_first = allocated_bytes(&part_path_for(&dest));
652 assert!(
653 after_first <= size + size / 8,
654 "a {size} byte file reserved {after_first} bytes on first open"
655 );
656
657 for _ in 0..3 {
659 let h = WriteHandle::open(&dest, size, true).unwrap();
660 h.write_at(0, &[1u8; 4096]).unwrap();
661 h.sync().unwrap();
662 }
663 let after_reopens = allocated_bytes(&part_path_for(&dest));
664 assert!(
665 after_reopens <= size + size / 8,
666 "reopening grew the reservation to {after_reopens} for a {size} byte file"
667 );
668 }
669
670 #[test]
671 fn zero_length_file_commits() {
672 let tmp = tempfile::tempdir().unwrap();
673 let dest = tmp.path().join("empty.bin");
674 let h = WriteHandle::open(&dest, 0, true).unwrap();
675 h.commit(0o644, 0, true).unwrap();
676 assert!(dest.exists());
677 assert_eq!(std::fs::metadata(&dest).unwrap().len(), 0);
678 }
679}