fstx: atomic, crash-safe file transactions for Rust
fstx is a Rust library that changes many files and directories as one atomic transaction. Either every change happens or none of them does, even if the process is killed or the machine loses power in the middle.
let mut tx = begin?;
tx.write?;
tx.rename?;
tx.remove_dir_all?;
tx.commit?; // all changes appear at once, or none do
Why
A filesystem makes exactly one kind of change atomic: renaming a single entry. Real programs change many files at once:
- installers and updaters replacing a set of files,
- config managers rewriting several related configs,
- package managers and build tools,
- code generators, and AI coding agents editing many source files.
When such a program crashes halfway, the directory is left half old, half new, which is
often worse than either version. Existing crates such as tempfile and atomicwrites
make one file atomic. fstx makes a whole set of changes atomic, with rollback and
crash recovery.
Features
- All-or-nothing commits across any number of files and directories.
- Crash recovery: after a crash, even one during recovery, the next start leaves the tree exactly as it was before or exactly as committed. Never a mix.
- Durable:
commit()returnsOkonly once the changes are safely on disk. - Read-your-writes:
tx.read()sees staged changes before commit. - O(1) directory moves that keep file identity (inodes) and hard links intact.
- Symlink-safe: never follows symlinks and never writes outside the root directory
(
openat2withRESOLVE_BENEATH | RESOLVE_NO_SYMLINKS). - Refuses rather than guesses: if recovery finds a state it can't interpret, it touches
nothing and returns
RecoveryRequired.fstx::inspect()shows why. - Non-UTF-8 file names are supported.
Install
or in Cargo.toml:
[]
= "0.1"
API documentation: docs.rs/fstx.
Requires Rust 1.89 or newer. Linux only for now (see Status).
Usage
Dropping a transaction without calling commit() discards it. Other entry points:
| Function | What it does |
|---|---|
fstx::recover(root) |
Finishes recovery of interrupted transactions (begin does this too) |
fstx::inspect(root) |
Read-only report of pending transactions and what recovery would do |
Transaction::begin_with(root, &Options) |
Begin with options, e.g. allowing network filesystems |
Try the examples:
Command-line tool
The companion crate fstx-cli provides an fstx command that applies a JSON
list of changes atomically. It suits scripts, dotfile syncs and AI coding agents:
See cli/README.md for the change-set format.
How it works
- Every change is staged in a private
.fstx/directory inside the root. Nothing visible changes yet. - On
commit, fstx writes a journal listing every entry it will move, identified by inode number, and makes it durable. - It then applies the changes as no-replace renames, in waves separated by
fsyncbarriers, and finally writes a durableCOMMITTEDmarker. - After a crash, recovery finds each entry by its identity, never by name alone, and undoes any unfinished transaction in reverse, using the same barrier discipline.
The full design, the exact crash-consistency assumptions and the correctness arguments are in DESIGN.md.
Testing
Crash safety is only as good as its tests. fstx is tested with:
- Exhaustive crash simulation (
tests/crash_sim.rs): an in-memory filesystem that models real crash behaviour, including reordered and lost unsynced writes. Every commit is crashed at every system call, under every allowed power-loss outcome, and then recovery is crashed at every system call too. About 1.3 million recovery runs, each checked for "exactly before or exactly after". A slower bounded check (--ignored) also covers a crash during the recovery of a recovery. - Real process kills (
tests/sigkill.rs): 200SIGKILLs mid-commit on a real disk. - Model-based testing (
tests/model.rs): random operation sequences compared against a reference model that tracks file identities. - Security tests: symlink escapes, and a thread racing to swap a directory for a symlink during commits.
- Parser robustness: property tests and cargo-fuzz targets for the on-disk formats.
The simulator found four real bugs during development, all fixed and documented in DESIGN.md.
Status
v0.1, Linux. Tested on btrfs and tmpfs. ext4 and xfs are expected to work, since fstx
checks the required filesystem features at startup and refuses to run without them. On
macOS and Windows the crate builds, but begin returns UnsupportedPlatform. Backends for
both are planned.
Not supported yet: operating on symlinks directly, extended attributes, ACLs and ownership, roots spanning several filesystems, async, a command-line tool, and isolation for concurrent readers. fstx coordinates fstx users with a lock; other programs writing to the same directory at the same time are detected where possible but not prevented.
FAQ
Is this a database? No. It works on ordinary files and directories that other programs
can read normally. The only extra is a small .fstx/ directory used while a transaction
is running.
What happens if my program crashes during commit()? The next Transaction::begin or
fstx::recover on that directory rolls the transaction back, or completes the cleanup if
it had already committed.
How is this different from tempfile::persist or atomicwrites? Those make a single
file replacement atomic. fstx makes an arbitrary set of writes, renames and deletions
atomic together, and recovers from crashes.
License
Licensed under either of Apache License 2.0 or MIT, at your option.