Guides
Embedding

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:

CapabilityMethods
ReadAll read-only methods (stat, exists, read, read_dir, tree, glob, find, search_content, list_trash, read_link, changes, workspace_usage)
WriteMutating methods that don't destroy: mkdir, write, write_at, append, truncate, touch, symlink, set_attribute, remove_attribute, copy, move_path
Deleteremove, purge
TrashRestoretrash, restore
WorkspaceAdmincreate_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; for InvalidRange it carries the requested range bytes; for RevisionConflict it 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:

  1. Process startup — open one SqliteFileSystem, create or look up the default workspace, hand the result back as Arc<dyn FileSystem> + Arc<Workspace>.
  2. Per-request handling — pull a WorkspaceId (from a header, a URL segment, or a token), build a RequestContext, run the operation, surface the Result.
  3. Background work (optional) — changes returns 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).