Skip to main content

fs_transaction/
lib.rs

1//! Crash-atomic filesystem transactions.
2//!
3//! [`ChangeSet`] stages root-relative writes, renames, removals, copies,
4//! execute-bit flips, and symbolic links as one ordered unit. [`ChangeSet::apply`] lands the whole set or none of it: an
5//! error unwinds every op already applied, and a write-ahead journal makes a
6//! *committed* set recoverable after a process crash or power loss, via
7//! [`recover`]. A set can also [*expect*](ChangeSet::expect) — stage what the
8//! caller read alongside what it wants written, and have the apply refuse
9//! ([`Error::Drifted`]) before touching anything if something else wrote in
10//! between: optimistic concurrency, for the many-readers case locking can't
11//! reach.
12//!
13//! The journal's file name is [configurable](Journal::named) and defaults to
14//! [`.fstx-journal`](Journal::DEFAULT_NAME). Apply and recovery must agree
15//! about it, which is why both operations hang off [`Journal`].
16//!
17//! Files stay ordinary files. This is not a virtual filesystem and not a
18//! database — nothing here changes how the tree is read, only how it is
19//! written.
20//!
21//! All-or-nothing is not the only crash discipline here. Where every
22//! partially-written batch is a *legal* state — an append-only,
23//! content-addressed store — [`OrderedBatch`] provides durability and
24//! ordering without the journal: tiers of writes separated by barriers, a
25//! crash leaving some prefix of them, and no recovery step anywhere. See
26//! [`ordered`] for when each protocol is the right one.
27//!
28//! ```no_run
29//! use fs_transaction::{ChangeSet, StdFs, exec::block_on, recover};
30//! use std::path::Path;
31//!
32//! let root = Path::new("/tmp/example");
33//! # std::fs::create_dir_all(root).unwrap();
34//! // Finish anything a previous crash left journaled, before reading the tree.
35//! block_on(recover(&StdFs, root))?;
36//!
37//! let mut change = ChangeSet::new();
38//! change.write("notes/a.md", "hello");
39//! change.rename("old.md", "notes/b.md");
40//! change.remove("stale.md");
41//! block_on(change.apply(&StdFs, root))?;
42//! # Ok::<(), fs_transaction::Error>(())
43//! ```
44//!
45//! ## Backends
46//!
47//! Everything is generic over the [`fs`] port, so the same transaction runs
48//! over [`StdFs`], the bundled [`InMemoryFs`], or an adapter you write for
49//! OPFS, IndexedDB, or a network store. A backend *declares* the durability it
50//! can keep through [`Capabilities`](fs::Capabilities), and the apply path
51//! picks the strongest protocol that backend actually supports rather than
52//! assuming one and lying on the backends that cannot keep it.
53//!
54//! ## Scope
55//!
56//! - **Single writer.** There is no locking; concurrent appliers against one
57//!   root will race. See [`change`] for the details.
58//! - **A set is bounded by memory.** Staged bytes and the undo buffer are both
59//!   held in memory for the length of the apply; [`FileOp::CopyFrom`] is the
60//!   escape hatch for a large payload already on disk.
61//! - **A root lives on one filesystem.** The staging renames assume it, and
62//!   so do the batched flushes: a barrier and the drain that caps it prove
63//!   nothing across a device boundary, so a root spanning a mount point is
64//!   outside the crash promises. The one cross-device case the crate itself
65//!   creates — a [journal homed](Journal::kept_in) on another volume — is
66//!   handled with its own drain.
67//! - **Futures are not required to be `Send`.** The port uses native
68//!   `async fn`, so a backend keeps its own future types — which means an
69//!   apply over a non-`Send` backend cannot be `tokio::spawn`ed.
70
71pub mod change;
72pub mod error;
73pub mod exec;
74pub mod fs;
75pub mod journal;
76pub mod ordered;
77pub mod path;
78
79#[cfg(test)]
80mod fs_faults;
81
82pub use change::{ChangeSet, Expected, FileOp};
83pub use error::{Error, Result};
84pub use fs::{InMemoryFs, ReadStorage, StdFs, Storage};
85pub use journal::{Journal, Recovered, recover};
86pub use ordered::{BatchOp, OrderedBatch};