@unikvs/fs.bun
Learn how to use the storage plugin that saves byte sequences to the local file system in Bun.
Overview
@unikvs/fs.bun is a storage plugin that saves data to the local file system in Bun. Keys are stored as file names under the specified root directory.Bun only
It handles byte sequences only. write accepts a Uint8Array<ArrayBuffer> and read returns a Uint8Array<ArrayBuffer>. File reads and writes use bytes(), write(), and writer() on Bun.file. Directory operations and renames dynamically import node:fs/promises, and paths use node:path. Temporary file names use Bun.randomUUIDv7().
Install it as follows.
npm install @unikvs/fs.bunpnpm add @unikvs/fs.bunyarn add @unikvs/fs.bunbun add @unikvs/fs.bunnub add @unikvs/fs.bunaube add @unikvs/fs.bunIt depends on @unikvs/core and @unikvs/utils.
Usage
The central class is BunFs.
import { BunFs } from "@unikvs/fs.bun";
public constructor(root: string = ".unikvs", options: BunFsOptions = {})
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"BunFs".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 { BunFs } from "@unikvs/fs.bun";
const storage = new BunFs("./.tmp/unikvs-fs-bun");
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}.${Bun.randomUUIDv7()}.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 writes to the FileSink returned by Bun.file(tmp).writer() and calls flush() for every chunk to commit it to disk. It uses swap-on-close: data is renamed to the final path only on successful close. If the AbortSignal aborts, the partially written temp file is removed. It 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>>
Instead of returning Bun.file(file).stream() directly, it wraps the stream in a new ReadableStream. Bun opens the file when a reader is acquired, so wrapping defers errors for missing files until read() and keeps behavior consistent with other storages. Aborting signal cancels the reader, and subsequent reads fail with the abort reason.
Notes
- Bun only.
open()throwsUnsupportedRuntimeErroron runtimes without theBunglobal. - 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. existsreturns the result ofBun.file(file).exists(), which isfalsefor non-files.readreads withBun.file(file).bytes()and returns aUint8Array. Missing files fail withENOENT.deleteremoves files withBun.file(file).delete(). Missing files fail withENOENT.- Temp files are removed with
Bun.file(tmp).delete(). - Permissions follow OS and Bun defaults.
close()is not implemented. SincecloseinIStorageis optional, manage cleanup yourself when needed.
Errors
| Error | Condition |
|---|---|
UnsupportedRuntimeError |
Thrown by open() on runtimes without the Bun global. A shared error from @unikvs/core. |
import { BunFs, UnsupportedRuntimeError } from "@unikvs/fs.bun";
const storage = new BunFs();
try {
await storage.open();
} catch (error) {
if (error instanceof UnsupportedRuntimeError) {
console.error("Run this on the Bun runtime");
} else {
throw error;
}
}
Examples
A minimal example storing under .tmp/.
import { BunFs } from "@unikvs/fs.bun";
const storage = new BunFs("./.tmp/unikvs-fs-bun-example");
await storage.open();
const key = "greeting.bin";
await storage.write({
key,
data: new TextEncoder().encode("hello, BunFs"),
});
if (await storage.exists({ key })) {
const data = await storage.read({ key });
console.log(new TextDecoder().decode(data));
}
await storage.delete({ key });
await storage.clear();