blockworx/storage/mod.rs
1//! The seam between a document container and whatever holds its bytes.
2//!
3//! A container is a directory of small files — `root.kdl`, and as they come to
4//! exist `assets/` and `history/`. Natively that is a real directory; in the
5//! browser it is the origin-private filesystem. [`Storage`] is what both look
6//! like from the container's side, so [`container`] is written once.
7//!
8//! The trait is deliberately **synchronous**, which is why it is not the whole
9//! story on the web: OPFS is async on the main thread and only synchronous
10//! inside a worker. The web implementation therefore lives *in* that worker, and
11//! the UI thread reaches it through a channel — the shape `crate::import`
12//! already uses for file dialogs. That channel arrives with the writer thread;
13//! until then the app calls straight through on native, as it always has.
14
15// The container compiles for wasm but has no caller there until the OPFS
16// implementation and the document picker land: building it for the web anyway is
17// what proves it stayed portable, rather than quietly growing a `std::fs`
18// dependency that only shows up at P5.
19#![cfg_attr(target_arch = "wasm32", allow(dead_code))]
20
21pub mod archive;
22#[cfg(not(target_arch = "wasm32"))]
23pub mod atomic;
24pub mod compact;
25pub mod container;
26#[cfg(not(target_arch = "wasm32"))]
27pub mod fs;
28pub mod history;
29#[cfg(not(target_arch = "wasm32"))]
30pub mod lock;
31#[cfg(not(target_arch = "wasm32"))]
32pub mod writer;
33
34/// A flat namespace of files, addressed by `/`-separated paths relative to a
35/// container root (`root.kdl`, `assets/9f3a.png`). Paths never escape the root;
36/// implementations reject anything that tries.
37///
38/// Missing intermediate directories are created by [`write`](Self::write), so a
39/// container that has never held an asset has no `assets/` until it does.
40///
41/// The trait carries only what a caller uses today.
42pub trait Storage {
43 fn read(&self, path: &str) -> std::io::Result<Vec<u8>>;
44
45 /// Write `contents`, creating intermediate directories as needed. Never
46 /// leaves a partially written file at `path`: the bytes land beside it and
47 /// are moved into place, so a crash yields the old contents or the new ones.
48 /// `durability` decides only whether to wait for the storage device.
49 fn write(&self, path: &str, contents: &[u8], durability: Durability) -> std::io::Result<()>;
50
51 /// The file names directly inside `dir`, in no particular order. An empty
52 /// `dir` is the container root.
53 ///
54 /// A directory that does not exist lists as empty rather than failing: a
55 /// container legitimately has no `history/` until its first snapshot, and
56 /// callers would otherwise all have to special-case that.
57 fn list(&self, dir: &str) -> std::io::Result<Vec<String>>;
58
59 /// Delete `path`. Removing something that is not there is not an error:
60 /// compaction removes a pair of files and must not care which of them a
61 /// previous interrupted run already took.
62 fn remove(&self, path: &str) -> std::io::Result<()>;
63
64 fn exists(&self, path: &str) -> bool;
65}
66
67/// How hard a write should work to survive a power cut, as opposed to a crash —
68/// the move into place protects against a crash either way.
69///
70/// This is the difference between roughly 11 ms and roughly nothing (see
71/// `todo.md` P0), so it is worth saying which one a call site needs.
72#[derive(Clone, Copy, PartialEq, Eq, Debug)]
73pub enum Durability {
74 /// Flush to the storage device before returning. For a file that is
75 /// overwritten in place, where losing the write loses the previous contents
76 /// too.
77 Sync,
78 /// Return once the bytes are handed to the OS. For write-once files, where
79 /// a lost write is a missing file rather than a damaged one — and where a
80 /// torn snapshot fails its gzip checksum and is skipped.
81 Relaxed,
82}