@unikvs/fs.node
Learn how to use the storage plugin that saves byte sequences to the local file system in Node.js.
Overview
@unikvs/fs.node is a storage plugin that saves data to the local file system in Node.js. Keys are stored as file names under the specified root directory.Node.js only
It handles byte sequences only. write accepts a Uint8Array<ArrayBuffer> and read returns a Uint8Array<ArrayBuffer>. The implementation dynamically imports node:fs, node:path, node:crypto, and node:stream.
Install it as follows.
npm install @unikvs/fs.nodepnpm add @unikvs/fs.nodeyarn add @unikvs/fs.nodebun add @unikvs/fs.nodenub add @unikvs/fs.nodeaube add @unikvs/fs.nodeIt depends on @unikvs/core and @unikvs/utils.
Usage
The central class is NodeFs.
import { NodeFs } from "@unikvs/fs.node";
public constructor(root: string = ".unikvs", options: NodeFsOptions = {})
root is the root directory for data. Relative paths are resolved against the current working directory when open() runs. The default is ".unikvs".
options.allowRepair controls whether repair writes are allowed. The default is false. When false, write and getWritable with a repair marker (vars["unikvs:repair"] is true) fail with RepairNotAllowedError.
The main members are as follows.
nameis"NodeFs".allowRepairindicates whether repair writes are allowed.isOpenindicates whether the storage is open.open()recursively creates the root directory and makes it ready for reads and writes.write,read,exists,delete, andclearhandle data.
import { NodeFs } from "@unikvs/fs.node";
const storage = new NodeFs("./.tmp/unikvs-fs");
await storage.open();
await storage.write({
key: "hello.bin",
data: new TextEncoder().encode("hello"),
});
const data = await storage.read({ key: "hello.bin" });
console.log(new TextDecoder().decode(data));
File Layout
Keys map to file paths as follows. Keys sit directly under the root with no subdirectories.
- .unikvs/
- hello.bin
- greeting.bin
- The final path is
path.join(this.root, key). - The temporary write path is
`${dest}.${crypto.randomUUID()}.tmp`. It is created in the same directory sorenamestays atomic. open()absolutizes withpath.resolve(this.root)and creates the root withmkdir(root, { recursive: true }).clear()removes the root withrm(this.root, { recursive: true, force: true })and recreates it.
File names are validated with assertValidFilename from @unikvs/utils. Each of write, read, exists, delete, getWritable, and getReadable validates first. Invalid names throw InvalidFilenameError. Empty strings, ., .., names over 255 bytes, non-NFC strings, and reserved/Windows-reserved names cannot be used.
Streams
Both readable and writable streams are supported.
Obtain a writable stream with the following method.
public async getWritable(
args: Pick<IStorage.GetWritableArgs, "key"> & { signal?: AbortSignal },
): Promise<WritableStream<Uint8Array<ArrayBuffer>>>
It uses swap-on-close: data is written to a temporary file and renamed to the final path only on successful close. If the AbortSignal aborts, the stream is discarded and the temp file is removed. It supports backpressure and never destroys existing data on failure or abort.
Obtain a readable stream with the following method.
public getReadable(
args: Pick<IStorage.GetReadableArgs, "key" | "signal">,
): ReadableStream<Uint8Array<ArrayBuffer>>
It converts fs.createReadStream(file, { signal }) to a web stream with stream.Readable.toWeb(readStream). Aborting signal destroys the read stream with the abort reason, so in-flight reads fail as well.
Notes
- Don’t call
writeorreadbeforeopen(). The internal connection isnulluntil then. Check withisOpen. open()re-initializes even when already open.rootis kept as an absolute path.writewrites to a temp file then renames. On failure or abort, the temp file is removed, preserving existing data.- Concurrent writes to the same key don’t collide thanks to unique suffixes. The last
renamewins. existsreturnsfalseonaccess(file)errors.deletefails for missing files.- Permissions follow OS and Node.js defaults.
close()is not implemented. SincecloseinIStorageis optional, manage cleanup yourself when needed.
Examples
A minimal example storing under .tmp/.
import { NodeFs } from "@unikvs/fs.node";
const storage = new NodeFs("./.tmp/unikvs-fs-node-example");
await storage.open();
const key = "greeting.bin";
await storage.write({
key,
data: new TextEncoder().encode("hello, NodeFs"),
});
if (await storage.exists({ key })) {
const data = await storage.read({ key });
console.log(new TextDecoder().decode(data));
}
await storage.delete({ key });
await storage.clear();