Built-in Drivers
Three. Two are FullFsDrivers — every optional method implemented — and the third adapts a key–value store, which cannot be.
They exist to be useful on their own, and to be the thing you wrap when you want caching, logging, filtering or transformation on top of something that already works.
#Memory
import { createMemoryDriver } from "mountx/drivers/memory";
const driver = createMemoryDriver();A complete in-memory filesystem: files, directories, symlinks, hardlinks, permissions, timestamps, statfs. Nothing is persisted; the tree lives as long as the process does.
function createMemoryDriver(options?: MemoryDriverOptions): FullFsDriver;
interface MemoryDriverOptions {
/** Owner of everything created. Defaults to the current process. */
uid?: number;
gid?: number;
/** Bits cleared from every mkdir/open mode. Default 0 — see below. */
umask?: number;
/** Mode of the root directory. */
rootMode?: number;
}#Why umask defaults to none
A umask belongs to a process, and a driver is not one. Under a mount it is worse than redundant: the kernel has already applied the calling process's umask before the mode reaches FUSE_MKDIR/FUSE_CREATE, so a second one here masks with the wrong process's value and produces a file the caller did not ask for — create f 04777 arriving as 04755, which is how pjdfstest found it.
Set it explicitly for a loopback filesystem that wants node:fs-like behaviour.
#Node-fs
import { createNodeFsDriver } from "mountx/drivers/node-fs";
const driver = createNodeFsDriver("/home/me/data");A passthrough to a real directory on the host.
function createNodeFsDriver(root: string, options?: NodeFsDriverOptions): FullFsDriver;
interface NodeFsDriverOptions {
/** Report the driver as read-only (does not itself enforce it). */
readOnly?: boolean;
}It resolves every path component itself rather than handing a joined string to node:fs, so nothing outside its root can be reached — not through .., and not through a symlink pointing out. Symlink resolution is bounded at 40 levels, as the kernel's is.
#Unstorage
import { createStorage } from "unstorage";
import s3Driver from "unstorage/drivers/s3";
import { createUnstorageDriver } from "mountx/drivers/unstorage";
const storage = createStorage({ driver: s3Driver({/* … */}) });
const driver = createUnstorageDriver(storage);One adapter, and every unstorage driver becomes mountable: S3, Redis, Cloudflare KV, GitHub, an HTTP endpoint, a filesystem, or a Storage with several of those mounted into one key space.
function createUnstorageDriver(storage: Storage, options?: UnstorageDriverOptions): FsDriver;
interface UnstorageDriverOptions {
/** Owner of everything in the tree. Defaults to the current process. */
uid?: number;
gid?: number;
/** Permission bits reported for a file with no chmod applied. Default 0o644. */
fileMode?: number;
/** Permission bits reported for a directory with no chmod applied. Default 0o755. */
dirMode?: number;
/** Answer EROFS to everything that would write. */
readOnly?: boolean;
}unstorage is an optional peer dependency, imported for its types only — install it yourself, and nothing in mountx's runtime graph changes if you do not.
#How the tree maps onto keys
| filesystem | store |
|---|---|
/a/b/c.txt | the key a:b:c.txt |
| a directory | a key prefix — there is nothing else to be |
| a file's contents | getItemRaw / setItemRaw |
mtime, size | the driver's own getMeta, where it has one |
The / → : mapping is unstorage's own convention, not one invented here: its normalizeKey already treats the two as the same separator. To serve a subtree, hand over prefixStorage(storage, "base") rather than the whole store.
Three characters cannot survive the round trip and are refused with EINVAL rather than mangled: : is the separator, ? is truncated away by normalizeKey (a?b becomes a), and a key ending in $ is unstorage's metadata convention and is filtered out of getKeys — a file created with one would exist and never be listed.
#What a key–value store cannot do
- No symlinks, no hardlinks, no
statfs, andrenameis a copy followed by a delete rather than an atomic operation. All four are declared, so the mount answersENOSYS/ENOTSUPrather than pretending — see capabilities. - Empty directories live in the process, not the store. There is no such thing as an empty directory in a key–value store, so
mkdirof one is remembered until it holds a file — at which point it is a real prefix and outlives the process like anything else. Nothing is written to mark it, because a marker key would show up as a file to every other consumer of that store. - Permissions and timestamps are an overlay, held in memory for the life of the driver and seeded from
getMeta.chmod,chownandutimestherefore work —cp -pandtar -xneed them — but a store that cannot hold that metadata does not gain the ability to. - A key that is also a prefix (both
aanda:bexist) has no tree that represents it. The key wins:ais a file, anda/bisENOTDIR.
Worth knowing before mounting something remote: readdir lists every key under the directory's prefix, because the Storage interface has no shallow listing, and stat falls back to fetching a value to measure it when the underlying driver's getMeta reports no size.
#Which to use
- Memory — tests, demos, scratch space, and the base for a driver whose data comes from somewhere else entirely.
- Node-fs — the wrapping target. Add caching, an access-control layer, a virtual overlay, request logging, or on-the-fly transformation to an ordinary directory, and everything you did not override still works.
- Unstorage — anything that is already a key–value store, and the shortest route to mounting a remote one.
#Wrapping one
They are plain objects, so a Proxy or a spread is all a wrapper needs:
import { createNodeFsDriver } from "mountx/drivers/node-fs";
import type { FsDriver } from "mountx";
const base = createNodeFsDriver("/home/me/data");
const logged: FsDriver = {
...base,
async stat(path) {
console.log("stat", path);
return base.stat(path);
},
};Keep capabilities in mind when you wrap: spreading carries the base driver's declarations along with its methods, which is usually what you want — but a wrapper that removes a method should say so, since capabilities are inferred from shape.
#node:fs/promises itself
Worth remembering that this is a valid driver with no adaptation at all:
import * as fs from "node:fs/promises";
import { createLoopback } from "mountx";
const loopback = createLoopback(fs); // compiles, no castIt is unrooted — paths are the host's, not confined to any directory — so it is a conformance oracle rather than something to mount. createNodeFsDriver is the rooted version.
#Next
- Writing a driver — when none of the three is what you have.
- Mounting — turning any of them into a folder.