Embedding
fslite-sqlite is the recommended way to embed an fslite workspace
into a larger Rust binary — it's the only backend crate that exposes
real persistence, and it implements every method on FileSystem.
Share one backend across handlers
Every method on FileSystem takes &self, so a single backend can be
shared freely. The most ergonomic shape is:
use std::sync::Arc;
use fslite_core::FileSystem;
use fslite_sqlite::SqliteFileSystem;
#[derive(Clone)]
struct AppState {
fs: Arc<dyn FileSystem>,
}
async fn build_state(path: &Path) -> anyhow::Result<AppState> {
let fs = SqliteFileSystem::open(path, Default::default()).await?;
Ok(AppState { fs: Arc::new(fs) })
}From there, hand AppState to your HTTP handlers, your CLI dispatcher,
or both. Arc<dyn FileSystem> is Send + Sync (the trait is
#[async_trait]) and trivially cloneable.
Capability gating
fslite-server gates every request by the authenticated actor's
BTreeSet<Capability>. The mapping is enforced in
crates/fslite-sqlite/src/lib.rs via the require_capability helper:
| Capability | Methods |
|---|---|
Read | All read-only methods (stat, exists, read, read_dir, tree, glob, find, search_content, list_trash, read_link, changes, workspace_usage) |
Write | Mutating methods that don't destroy: mkdir, write, write_at, append, truncate, touch, symlink, set_attribute, remove_attribute, copy, move_path |
Delete | remove, purge |
TrashRestore | trash, restore |
WorkspaceAdmin | create_workspace, delete_workspace (inherent methods, not on the trait) |
If you're not using fslite-server and you build your own request
context with RequestContext::trusted, every capability check passes
unconditionally. That's intentional — it's how the test suite trusts its
own identities. Production callers should hand-build contexts only
from server-issued AuthenticatedActors.
Error handling
fs::FileSystem methods return FsResult<T> = Result<T, FsError>. An
FsError carries three things:
code: ErrorCode— a stable enum (NotFound,AlreadyExists,RevisionConflict,QuotaExceeded, ...). See the error codes reference for the full list and the HTTP status each one maps to.message: String— human-readable, safe to surface to end users. Suitable for logs and error envelopes.details: Value— structured, serde-serialised extra context. For batched operations this carries the per-operation index; forInvalidRangeit carries the requested range bytes; forRevisionConflictit carries the conflicting current revision.
fslite-server wraps each FsError in an ApiError, maps the code
to an HTTP status, and emits a uniform { "code": ..., "message": ..., "details": ... } JSON envelope. Embedders that ship their own
transport should do the equivalent.
Optimistic concurrency
Every mutating *Options struct (WriteOptions, CopyOptions,
MoveOptions, RemoveOptions, MutationOptions, etc.) carries an
optional expected_revision: Option<Revision>. Set it to the
Revision you read earlier; the mutation succeeds only if the node
hasn't changed since.
let node = fs.stat(&ctx, &path, Default::default()).await?;
let expected = node.revision;
fs.write(&ctx, &path, bytes, WriteOptions {
expected_revision: Some(expected),
..Default::default()
}).await?;If the revision has changed in between, the call returns
RevisionConflict with details["current"] = <new revision> so you can
decide whether to retry, recurse, or surface the conflict.
Streaming I/O
Writes use WriteSource — either WriteSource::from_bytes(Vec<u8>)
for in-memory data, or WriteSource::new(stream) for any
Stream<Item = Bytes>. Reads return FileRead, which you can call
into_stream() on to get a bounded-memory Stream<Item = Result<Bytes, ...>>.
SqliteFileSystem stores content as immutable 1 MiB chunks and reads/
writes one chunk at a time (see
content.rs (opens in a new tab))
so memory pressure stays flat regardless of file size — a 10 GiB upload
peaks at one chunk of RAM.
Real-world shape
A typical embedder has three pieces:
- Process startup — open one
SqliteFileSystem, create or look up the default workspace, hand the result back asArc<dyn FileSystem> + Arc<Workspace>. - Per-request handling — pull a
WorkspaceId(from a header, a URL segment, or a token), build aRequestContext, run the operation, surface theResult. - Background work (optional) —
changesreturns the change feed; you can subscribe with your preferred polling cadence and project outbound events.
A complete, runnable version of (1)+(2) lives at
examples/embedded.rs (opens in a new tab).