Introduces the SequentialSink / RandomAccessSink trait pair under io::sink and an open_for_mkv dispatch helper that picks WritebackFile on Linux+NFS and LocalFileSink everywhere else. LocalFileSink wraps BufWriter<File> with a 4 MiB buffer and exposes a per-OS preallocate path (fallocate on Linux, F_PREALLOCATE on macOS, no-op fallback). Adds platform::fs_type::detect with a per-OS split (statfs on Linux / macOS, UNC heuristic on Windows, Unknown elsewhere) so construction- site dispatch has a single primitive to call. Blanket impls cover the common shapes: any Write+Send is a SequentialSink, and any SequentialSink+Seek is a RandomAccessSink. WritebackFile satisfies the random-access trait via the blanket impl without needing an explicit per-type impl. No callers wired yet — the mux::resolve construction sites stay on WritebackFile pending Phase 3. Tests: 5 new sink/preallocate tests + 3 fs_type tests (1 ignored, needs a real NFS mount). cargo +1.86 fmt + clippy + tests all green.
163 lines
5.5 KiB
Rust
163 lines
5.5 KiB
Rust
//! `LocalFileSink` — `BufWriter<File>` for the common local-disk case.
|
|
//!
|
|
//! Buffering: 4 MiB internal `BufWriter`. Sized to coalesce the small
|
|
//! per-PES writes that come out of the muxer into kernel-page-aligned
|
|
//! flushes without making the buffer big enough to matter for memory
|
|
//! pressure on a single concurrent rip.
|
|
//!
|
|
//! `Seek` flushes the underlying `BufWriter` first; otherwise a seek
|
|
//! could leapfrog buffered data and silently corrupt the file. This is
|
|
//! the same shape `BufWriter` itself uses when it impls `Seek` in
|
|
//! stdlib, and is necessary for MKV's seek-back operations (cluster
|
|
//! size patch, Cues index, segment header backpatch) to land on the
|
|
//! right offset.
|
|
//!
|
|
//! `RandomAccessSink` is satisfied via the blanket impl in
|
|
//! [`super::mod`]; no explicit impl needed here.
|
|
|
|
use std::fs::{File, OpenOptions};
|
|
use std::io::{self, BufWriter, Seek, SeekFrom, Write};
|
|
use std::path::Path;
|
|
|
|
use super::preallocate;
|
|
|
|
const BUFFER_BYTES: usize = 4 * 1024 * 1024;
|
|
|
|
/// Random-access write sink for local disks.
|
|
///
|
|
/// Wraps a `BufWriter<File>` with a 4 MiB internal buffer and forwards
|
|
/// `Write`/`Seek` so any call site that previously held a `File` or
|
|
/// `WritebackFile` can drop this in. `finish()` flushes the buffer and
|
|
/// runs `sync_all` on the underlying file so the caller can drop it
|
|
/// without losing data.
|
|
///
|
|
/// Construction always opens the file `create + truncate + read +
|
|
/// write`. `read` is enabled so the same handle can be reused for a
|
|
/// verification re-read after the mux (the existing
|
|
/// `FileSectorSink::create` pattern). On Linux, [`with_size_hint`]
|
|
/// additionally calls `fallocate(FALLOC_FL_KEEP_SIZE)` to pre-reserve
|
|
/// extents.
|
|
///
|
|
/// [`with_size_hint`]: Self::with_size_hint
|
|
pub struct LocalFileSink {
|
|
inner: BufWriter<File>,
|
|
}
|
|
|
|
impl LocalFileSink {
|
|
/// Open `path` for writing, truncating any existing contents.
|
|
pub fn create(path: &Path) -> io::Result<Self> {
|
|
let file = OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(true)
|
|
.open(path)?;
|
|
Ok(Self {
|
|
inner: BufWriter::with_capacity(BUFFER_BYTES, file),
|
|
})
|
|
}
|
|
|
|
/// Like [`Self::create`] but additionally calls the per-OS
|
|
/// preallocate path with `size_bytes`. On Linux this is
|
|
/// `fallocate(FALLOC_FL_KEEP_SIZE)` so the on-disk extents are
|
|
/// reserved up front (reducing fragmentation for big sequential
|
|
/// muxer output); on other OSes it is a no-op today. Failures
|
|
/// from the preallocate call are non-fatal — the file is still
|
|
/// returned, just without the size reservation.
|
|
pub fn with_size_hint(path: &Path, size_bytes: u64) -> io::Result<Self> {
|
|
let file = OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(true)
|
|
.open(path)?;
|
|
preallocate::preallocate(&file, size_bytes);
|
|
Ok(Self {
|
|
inner: BufWriter::with_capacity(BUFFER_BYTES, file),
|
|
})
|
|
}
|
|
|
|
/// Drain the internal buffer and `fsync` the underlying file.
|
|
/// Idempotent with `Drop` (the `BufWriter` also flushes on drop;
|
|
/// this call additionally surfaces fsync errors to the caller).
|
|
#[allow(dead_code)] // exposed for parity with WritebackFile::sync_all
|
|
pub fn sync_all(&mut self) -> io::Result<()> {
|
|
self.inner.flush()?;
|
|
self.inner.get_ref().sync_all()
|
|
}
|
|
}
|
|
|
|
impl Write for LocalFileSink {
|
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
self.inner.write(buf)
|
|
}
|
|
|
|
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
|
|
self.inner.write_all(buf)
|
|
}
|
|
|
|
fn flush(&mut self) -> io::Result<()> {
|
|
self.inner.flush()
|
|
}
|
|
}
|
|
|
|
impl Seek for LocalFileSink {
|
|
fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
|
|
// Flush before seeking so buffered bytes land at the offset
|
|
// they were written for, not the new one.
|
|
self.inner.flush()?;
|
|
self.inner.get_mut().seek(from)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::io::Read;
|
|
|
|
#[test]
|
|
fn write_seek_roundtrip() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let p = dir.path().join("rt.bin");
|
|
let mut s = LocalFileSink::create(&p).unwrap();
|
|
s.write_all(b"AAAA").unwrap();
|
|
s.write_all(b"BBBB").unwrap();
|
|
// Seek back over the second word and overwrite.
|
|
s.seek(SeekFrom::Start(4)).unwrap();
|
|
s.write_all(b"CCCC").unwrap();
|
|
s.sync_all().unwrap();
|
|
drop(s);
|
|
|
|
let mut f = File::open(&p).unwrap();
|
|
let mut got = Vec::new();
|
|
f.read_to_end(&mut got).unwrap();
|
|
assert_eq!(&got[..], b"AAAACCCC");
|
|
}
|
|
|
|
#[test]
|
|
fn drop_flushes() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let p = dir.path().join("drop.bin");
|
|
{
|
|
let mut s = LocalFileSink::create(&p).unwrap();
|
|
s.write_all(b"buffered").unwrap();
|
|
// No explicit flush / sync_all — BufWriter drop runs the
|
|
// flush and the file should land on disk.
|
|
}
|
|
let bytes = std::fs::read(&p).unwrap();
|
|
assert_eq!(&bytes[..], b"buffered");
|
|
}
|
|
|
|
#[test]
|
|
fn with_size_hint_creates_writable_file() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let p = dir.path().join("sz.bin");
|
|
let mut s = LocalFileSink::with_size_hint(&p, 64 * 1024).unwrap();
|
|
s.write_all(b"hint-ok").unwrap();
|
|
s.sync_all().unwrap();
|
|
drop(s);
|
|
let bytes = std::fs::read(&p).unwrap();
|
|
assert_eq!(&bytes[..], b"hint-ok");
|
|
}
|
|
}
|