Skip to main content

Crate fs_transaction

Crate fs_transaction 

Source
Expand description

Crash-atomic filesystem transactions.

ChangeSet stages root-relative writes, renames, removals, copies, execute-bit flips, and symbolic links as one ordered unit. ChangeSet::apply lands the whole set or none of it: an error unwinds every op already applied, and a write-ahead journal makes a committed set recoverable after a process crash or power loss, via recover. A set can also expect — stage what the caller read alongside what it wants written, and have the apply refuse (Error::Drifted) before touching anything if something else wrote in between: optimistic concurrency, for the many-readers case locking can’t reach.

The journal’s file name is configurable and defaults to .fstx-journal. Apply and recovery must agree about it, which is why both operations hang off Journal.

Files stay ordinary files. This is not a virtual filesystem and not a database — nothing here changes how the tree is read, only how it is written.

All-or-nothing is not the only crash discipline here. Where every partially-written batch is a legal state — an append-only, content-addressed store — OrderedBatch provides durability and ordering without the journal: tiers of writes separated by barriers, a crash leaving some prefix of them, and no recovery step anywhere. See ordered for when each protocol is the right one.

use fs_transaction::{ChangeSet, StdFs, exec::block_on, recover};
use std::path::Path;

let root = Path::new("/tmp/example");
// Finish anything a previous crash left journaled, before reading the tree.
block_on(recover(&StdFs, root))?;

let mut change = ChangeSet::new();
change.write("notes/a.md", "hello");
change.rename("old.md", "notes/b.md");
change.remove("stale.md");
block_on(change.apply(&StdFs, root))?;

§Backends

Everything is generic over the fs port, so the same transaction runs over StdFs, the bundled InMemoryFs, or an adapter you write for OPFS, IndexedDB, or a network store. A backend declares the durability it can keep through Capabilities, and the apply path picks the strongest protocol that backend actually supports rather than assuming one and lying on the backends that cannot keep it.

§Scope

  • Single writer. There is no locking; concurrent appliers against one root will race. See change for the details.
  • A set is bounded by memory. Staged bytes and the undo buffer are both held in memory for the length of the apply; FileOp::CopyFrom is the escape hatch for a large payload already on disk.
  • A root lives on one filesystem. The staging renames assume it, and so do the batched flushes: a barrier and the drain that caps it prove nothing across a device boundary, so a root spanning a mount point is outside the crash promises. The one cross-device case the crate itself creates — a journal homed on another volume — is handled with its own drain.
  • Futures are not required to be Send. The port uses native async fn, so a backend keeps its own future types — which means an apply over a non-Send backend cannot be tokio::spawned.

Re-exports§

pub use change::ChangeSet;
pub use change::Expected;
pub use change::FileOp;
pub use error::Error;
pub use error::Result;
pub use fs::InMemoryFs;
pub use fs::ReadStorage;
pub use fs::StdFs;
pub use fs::Storage;
pub use journal::Journal;
pub use journal::Recovered;
pub use journal::recover;
pub use ordered::BatchOp;
pub use ordered::OrderedBatch;

Modules§

change
Transactional writes — the unit every change lands through.
error
What a transaction can fail with.
exec
A dependency-free executor for backends whose futures are already ready.
fs
The filesystem port — the seam every transaction lands through.
journal
The write-ahead journal — what makes a whole ChangeSet crash-atomic, not just each file in it.
ordered
Ordered writes — durability and ordering without atomicity, for trees where every prefix is a legal state.
path
Lexical path handling — normalization, and the guard that keeps a staged op inside the root it was applied against.