@k13engineering/fsapi-nodefs
v0.0.2
Published
An fsapi file system of a directory on the disk, through the fs promises API of Node.js
Readme
fsapi-nodefs
An fsapi file system of a directory on the disk, through the fs promises API of Node.js. Everything fsapi offers on top of a file system, such as its convenience API, chroot, virtual file systems, or an NFS server like nfs-server, then works on real files.
Note: this project is mostly AI coded. Its code, tests and documentation were largely written by an AI assistant, and reviewed and directed by a human maintainer.
npm install @k13engineering/fsapi-nodefs @k13engineering/fsapiExample
import { createConvenienceApi } from "@k13engineering/fsapi";
import { createNodeFileSystem } from "@k13engineering/fsapi-nodefs";
const fileSystem = createNodeFileSystem({ root: "/srv/data" });
const files = createConvenienceApi({ fileSystem });
await files.mkdirOrThrow({ filepath: "/docs" });
await files.writeFileOrThrow({ filepath: "/docs/hello.txt", data: new TextEncoder().encode("Hello!") });
// /srv/data/docs/hello.txt is on the disk now
const entries = await files.readdirOrThrow({ filepath: "/docs" });
const data = await files.readFileOrThrow({ filepath: "/docs/hello.txt" });
await fileSystem.close();examples/serve-a-directory.ts does more in a temporary directory: node examples/serve-a-directory.ts.
API
createNodeFileSystem({ root, nodeFs?, watchIntervalMs?, watchTimers? })
root: the directory of the host that is served as/. Symlinks in it are resolved once.nodeFs: the fs promises API to use,node:fsby default. The tests give an in-memory fake.watchIntervalMs: how often watches poll the stat of their inode, 1000 by default.watchTimers: the timers the polling uses, the timers of the platform by default.
Returns a TFilesystem of fsapi. Paths are checked like in every file system of fsapi: paths that are not normalised throw, as programming errors.
Behavior
The file system behaves like Linux, which is what Node.js reports, with what fsapi asks for on top:
- Errors are values. Errors of Node.js are returned as the errno of their code, and as
EIOif fsapi does not know the code. Nothing is thrown for errors of the disk. - Symlinks are never followed. Opening a path opens the entry itself, and a path through a symlinked directory fails with
ENOTDIR, also where Node.js would follow it. So a symlink in the served directory never leads out of it. - File handles find their inode by its path. Node.js cannot open directories and symlinks without following them, so a handle remembers the device, inode number and type it was opened for, and checks them on every use. Once its path is gone or holds another inode, the handle fails with
ENOENT, as fsapi has noESTALE. A renamed node is gone for handles opened before. - IO: regular files are opened only for the actions asked for, never through a symlink, and read and written at exact
bigintoffsets. Directories list their entries with type and inode number, leaving out entries removed while listing. Symlinks fail withELOOP, fifos withEOPNOTSUPP, sockets and devices withENXIO, as in ramfs: opening a fifo blocks, and opening a device can have effects of its own. - Creating nodes: fsapi creates nodes without a name and names them with
link, which Node.js cannot do. A created file handle keeps the mode, owner and times set on it, and creates the node at its firstlink, exclusively, removing it again if the attributes cannot be set. Until then it cannot be opened for IO (EOPNOTSUPP), and watches start once the node exists. Later links add hard links. - Changing nodes:
chmodchanges the permission bits and fails for symlinks withEOPNOTSUPP.chownandutimenschange the node itself, never what a symlink points to. The root cannot be removed or renamed (EBUSY). - Watching: Node.js cannot tell what changed, so each watch polls the stat of its inode and reports what differs from the stat before, starting with the stat the handle was opened with. A watch reports the removal once its inode is gone from its path or has no links.
- stat reports a generation of 0, which Node.js does not know.
Limitations
- Races: each call checks the directories along its path, and then works on the path. Something that changes the path in between is not noticed, as Node.js has no
openat(). Do not serve directories that others can change to attack the process. - Nodes Node.js cannot create: fifos, sockets and device nodes cannot be created (
ENOSYS), as Node.js has nomknod. - Times are set in seconds as a number, which keeps them to about a tenth of a microsecond.
- Permissions are those of the process. Changing owners needs the privileges for it.
- ioctl and fcntl are not supported (
ENOSYS).
Development
npm ci
npm run build
npm run type-check
npm run lint
npm testThe tests are deterministic. They run against an in-memory fake of the fs promises API (lib/test-helpers/fake-node-fs.ts), and use no real timers or disk. The behavior of the fs API they rely on is written down as a contract in lib/test-helpers/node-fs-contract.ts, which passes against the fake in the tests, and against the real API as well. npm test reports the coverage, which is 100%.
License
LGPL-2.1-only
