fslite
A transport-independent, async filesystem interface; a persistent, multi-workspace SQLite implementation suitable for direct embedding; and an HTTP server, typed command codec, and CLI built on top of it.
Docs: guides and concepts · API reference on docs.rs · project homepage
Workspace layout
| Crate | Purpose |
|---|---|
fslite-core |
The canonical FileSystem trait, domain types (VirtualPath, Node, Revision, ...), and stable typed errors. Transport-independent: no SQL, no HTTP, no host filesystem paths. |
fslite-sqlite |
SqliteFileSystem: a FileSystem implementation backed by one SQLite database that can hold many isolated workspaces. |
fslite-conformance |
A backend-agnostic contract test suite. Any FileSystem implementation can prove basic compliance by implementing ConformanceFactory and calling run_conformance. |
fslite-server |
An axum-based HTTP adapter exposing FileSystem as a resource-oriented API: nodes, directories, trash, content (including ranged reads), search, batch, and workspace-admin routes, gated behind a pluggable AuthProvider. |
fslite-command |
A typed Command codec (one variant per FileSystem operation), a constrained shell-like lexer/parser, and local/remote executors that drive either an in-process FileSystem or a running fslite-server over HTTP. |
fslite-cli |
fslite-cli: a command-line client built on fslite-command, usable against a local SQLite database, an in-memory database, or a remote fslite-server, in one-shot or REPL mode. |
Quick start
use ;
use SqliteFileSystem;
use StreamExt;
# async
See examples/embedded.rs for a complete, runnable
version (cargo run --example embedded), and the full list below for more.
Examples
Every example is self-contained (opens its own in-memory database, needs no
setup) and runnable with cargo run --example <name>:
| Example | Demonstrates |
|---|---|
embedded |
Open a database, write a file from a byte stream, read it back, list a directory, print workspace usage. |
batch |
Atomic multi-operation batches via batch — an aborted batch commits nothing, a valid one commits every operation together. |
trash_lifecycle |
trash hides a subtree without touching its data, restore brings it back (optionally under a new name), and purge is the only way its content is actually reclaimed. |
workspace_isolation |
Two workspaces in one database hold the same path independently, with no cross-workspace visibility — including a rejected cross-workspace pagination cursor. |
search_and_glob |
glob (path-shape matching), find (bounded metadata predicates), and search_content (literal byte matches inside files). |
server_and_remote_cli |
Runs fslite-server's HTTP API in-process and drives it with fslite-command's RemoteExecutor — the same client fslite-cli --server uses — over a real TCP connection. |
CLI and server
fslite-cli can talk to a local database file, a private in-memory one, or a
remote fslite-server over HTTP — the same verb syntax works in all three
modes. Flag values use --name=value (the lexer does not treat a following
bare word as a flag's value). --memory opens a fresh, unshared database for
that single process only, so it's suited to one-off experiments or scripts
that create a workspace and use it within the same invocation; use
--db <path> whenever a workspace needs to be visible across separate
fslite-cli invocations:
For everyday interactive use, fslite also supports named filesystems and
workspaces, persisted in a small local registry
($XDG_CONFIG_HOME/fslite or ~/.config/fslite, overridable via
FSLITE_CONFIG_DIR) so you don't need to repeat --db/--workspace on
every invocation:
create/use/delete manage this registry only — the names it stores
have no meaning to fslite-core/fslite-sqlite/fslite-server, which
only ever see raw workspace ids. --filesystem <name> and --workspace <name-or-id> override the persisted context for a single invocation
without changing it (fslite --filesystem other-fs mkdir /tmp); the raw
--db/--memory/--server + --workspace <uuid> flags shown below
remain fully supported and bypass the registry/context entirely — useful
for scripting against a database you don't want registered under a name.
# Local, one-shot, persisted to a file. --create-workspace prints the new
# workspace's id as its only line of output (after cargo's own build/run
# noise) — capture it with `-q` to suppress that noise and keep the
# variable clean:
WORKSPACE=
# Local, interactive REPL (the workspace must already exist — create it
# first, as above, or against the same --db file)
# Remote, against a running fslite-server
fslite-server is a standalone binary; it reads bearer tokens from
FSLITE_TOKENS (token=workspace_uuid pairs) and serves an in-memory
SqliteFileSystem on 0.0.0.0:8080:
FSLITE_TOKENS="devtoken=<workspace-uuid>"
This shipped main.rs is reference wiring, not a deployable server as-is: the
backing store is a fresh in-memory database on every start (nothing survives
a restart), and FSLITE_TOKENS is read once at startup, so a workspace
created afterward via POST /v1/workspaces has no token that can reach it
until the process is restarted with that id added — which then loses the
workspace again. A real deployment needs its own main wiring a persistent
SqliteFileSystem::open(path, ..) and its own AuthProvider that can mint or
look up tokens for workspaces created at runtime.
Every verb fslite-command understands corresponds one-to-one to a
FileSystem method: usage, stat, exists, ls, tree, mkdir, cat,
write, write-at, append, truncate, touch, cp, mv, rm, ln,
readlink, trash, trash-ls, restore, purge, setattr, rmattr,
glob, find, grep, changes, and batch — plus --json on the CLI for
machine-readable output.
Run fslite help to list every verb with a one-line summary, or
fslite help <verb> for a verb's full flag table (the same metadata is
also exposed as reference/cli-verbs in the docs site).
Limits
These are the SQLite backend's defaults; per-workspace byte/node/file-size
quotas are configurable via WorkspaceOptions at create_workspace time.
- Regular file size: up to the configured
max_file_bytes(workspace default: 1 GiB). Enforced incrementally as bytes stream in, before they're flushed to storage — an oversized write fails partway through, not after fully buffering the input. - Workspace byte/node quotas:
max_bytes(workspace default: 10 GiB) andmax_nodes(workspace default: 1,000,000), checked against active- trashed logical bytes and node counts.
- Custom attributes: a 256-byte key, a 4096-byte value, and 64
attributes per node. Attribute values are arbitrary bytes; since
Node::attributesis a JSON map, each value is base64url-encoded into a JSON string. - Symbolic link resolution: at most 40 hops per path lookup, after which
resolution fails with
LinkLooprather than looping forever. - Content chunking: file content is stored as immutable 1 MiB chunks. Reads and writes are streamed chunk-by-chunk in bounded memory — a read never buffers more than the chunk currently being produced, regardless of file size.
Transaction guarantees
- Every mutation (
write,mkdir,copy,move,remove,trash,restore,purge, attribute changes, ...) commits in one short SQLite transaction: the node/content change, its change-feed row, and any cascading cleanup (e.g. reclaiming a replaced file's old content generation) either all land together or none do. - A replacement upload is invisible until it fully commits.
writestages incoming bytes into a brand-new, independent content generation as they stream in. If the source stream errors partway through — or a configured quota is exceeded — the partially staged generation is discarded and the target's previous content and revision are left exactly as they were. Nothing about the existing file is touched until the new content is completely staged. batchis all-or-nothing. Every operation in a batch runs against one shared transaction, using each operation's internal transaction-level logic directly (never by recursively opening another connection call, which would deadlock against the single dedicated connection thread). The first failing operation aborts the whole batch — nothing commits — and the returned error's safe details include{"index": N}identifying which operation failed.- Mutations accept an optional expected revision (
expected_revision) and are rejected withRevisionConflictif the target has changed since the caller last observed it — a standard optimistic-concurrency precondition.
Workspace isolation
A single SQLite database holds many independent workspaces. Every stored
query is scoped by workspace_id, so two workspaces can contain identically
named paths (even at the root) without collision, and no operation in one
workspace can observe or mutate another's nodes, trash, attributes, or
change feed. Pagination cursors are versioned and bound to the workspace
(and, where relevant, the specific parent/root they were issued for) that
produced them; presenting a cursor to a different workspace fails with
InvalidCursor rather than silently returning the wrong data.
Permanent remove vs. recoverable trash
removedeletes a node (and, recursively, its entire subtree) permanently in one step. There is no way to recover a removed node short of restoring a database backup.trashmarks a node's own row as trashed and records its original location; the node and its descendants become invisible to every directory listing and lookup immediately (descendants are never individually marked — they simply become unreachable once their parent is), but the underlying data is untouched.list_trashenumerates trashed subtrees,restoremoves one back to its original location (or an alternate destination) if nothing already occupies that name, andpurgepermanently deletes a trashed subtree — this is the only way a trashed node's content is actually reclaimed.- The workspace root can never be moved, trashed, or removed.
Development
The backend's own test suite (crates/fslite-sqlite/tests/) exercises each
operation group in depth; crates/fslite-conformance captures the smaller,
backend-agnostic contract every FileSystem implementation must satisfy and
is run against SqliteFileSystem in crates/fslite-sqlite/tests/conformance.rs.
Status
The canonical FileSystem trait and the SQLite backend are complete: all 28
trait methods are implemented and covered by the conformance suite plus the
SQLite backend's own extensive test suite. fslite-server, fslite-command,
and fslite-cli build on this crate's exact public API and are workspace
members with their own test suites (HTTP contract tests for the server;
lexer/parser/sanitizer/executor tests for the command codec; end-to-end
local, remote, and REPL tests for the CLI). They've had several rounds of
security hardening (bidi-override stripping, output sanitization, token
handling) but are newer than fslite-core/fslite-sqlite and should be
treated as less battle-tested.