v0.9.3 — mod-tempdir
Pre-releasemod-tempdir v0.9.3 — Release Notes
Date: 2026-05-13
Compare: v0.9.2...v0.9.3
Headline
Crash-safe file finalization via NamedTempFile::persist_atomic,
with a structured error type that preserves the source temp file
on failure so a retry path never loses data.
What's new
Public API additions
-
NamedTempFile::persist_atomic(target) -> Result<PathBuf, PersistAtomicError>
Atomically move the temp file totargetand disable cleanup on
drop. Performs the canonical "atomic durable write" sequence:fsyncthe temp file
(std::fs::File::sync_all)- atomic
std::fs::rename(POSIXrename(2)on Unix,
MoveFileExWwithMOVEFILE_REPLACE_EXISTINGon Windows) - best-effort
fsyncof the target's parent directory so the
rename itself survives a crash.
Atomic within a single filesystem. Cross-filesystem
target
returnsEXDEV(Unix) or the equivalent (Windows) inside the
error. -
PersistAtomicError { error: io::Error, file: NamedTempFile }
Structured error type. On any failure ofpersist_atomic, the
temp file is preserved on disk and the originalNamedTempFile
is returned to the caller inside the error so a retry or
fallback path does not lose the source. ImplementsDebug,
Display,std::error::Error, andFrom<PersistAtomicError> for io::Errorfor callers that only need the underlying IO
error.
Test additions
tests/persist_atomic.rs — four integration tests:
- Move + content preservation on the same filesystem.
- Replacement of an existing target.
- Data-integrity error path: target's parent directory missing
→ source survives, recoveredNamedTempFilepoints at the
original temp path. - Post-success invariant: nothing remains at the original temp
path after a successful persist.
Documentation
- README gets a new "Atomic persistence" section with the retry
pattern. - REPS.md §3 lists
persist_atomicandPersistAtomicErrorin
the public API surface. - Rustdoc on
persist_atomicincludes both the happy-path example
and the retry pattern on recoverable error.
Why no fsys
The roadmap reserved a possible v0.9.3+ fsys integration for
this milestone. After auditing the fsys public API, the
integration was not taken:
fsys::platform::atomic_renameispub(crate)— not callable
from outside the crate.fsys::Handle::renamerequires both paths to live under a
single handle root, which does not fit a generic
temp_dir → arbitrary_targetmove.std::fs::renameinvokes the same OS primitivesfsysuses
internally (POSIXrename(2)on Unix,MoveFileExWon
Windows), so thestd-only path is functionally equivalent for
this use case and keeps the default zero-dep build intact.
Same architectural call as the retired v0.9.1 fsys-for-directory-ops
milestone: when fsys's value lives in its internals rather than
its public surface, adding the dep does not pay off.
Migration
Purely additive. No code edits needed for existing callers. To
use the new method:
use mod_tempdir::NamedTempFile;
use std::io::Write;
let f = NamedTempFile::new()?;
{
let mut h = std::fs::OpenOptions::new().write(true).open(f.path())?;
h.write_all(b"finalized payload")?;
}
match f.persist_atomic("config.toml") {
Ok(landed) => { /* `landed` is the target path */ }
Err(e) => {
// `e.file` is the original NamedTempFile, intact for retry.
// `e.error` is the underlying io::Error.
}
}
# Ok::<(), std::io::Error>(())Verification
cargo build/--features mod-rand/--all-featurescargo +1.75 build --all-features(MSRV check)cargo fmt --all -- --checkcargo clippy --all-targets --all-features -- -D warningscargo test --all-features: 63 tests pass (Windows)cargo test(default): 59 tests passcargo doc --no-depsand--all-features: 0 warnings- Banned-word scan and em-dash scan: 0 hits across shipping files.
CI matrix: ubuntu-latest, macos-latest, windows-latest.
Limitations
persist_atomicis atomic only within a single filesystem.
Cross-mounttargetreturnsEXDEV/ equivalent. Callers
needing cross-filesystem finalization should copy through the
target filesystem first usingTempDir::with_prefixrooted at
the target's parent.- Parent-directory
fsyncis best-effort; on Windows it requires
FILE_FLAG_BACKUP_SEMANTICSto acquire the directory handle.
Failures here are silent and do not affect the return value
(the rename itself has already succeeded by that point). - The data-integrity contract (source preserved on failure) means
a successfulpersist_atomicconsumesself; an unsuccessful
one returns it insidePersistAtomicError.file. Callers that
useio::Result<PathBuf>via theFromconversion lose the
recovered file. Use the structuredResult<PathBuf, PersistAtomicError>
return type directly when retry-on-failure matters.