---
title: "@unikvs/fs.node"
description: "Learn how to use the storage plugin that saves byte sequences to the local file system in Node.js."
---

## Overview [#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.

```package-install
npm install @unikvs/fs.node
```

It depends on `@unikvs/core` and `@unikvs/utils`.

## Usage [#usage]

The central class is `NodeFs`.

```ts
import { NodeFs } from "@unikvs/fs.node";
```

```ts
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.

- `name` is `"NodeFs"`.
- `allowRepair` indicates whether repair writes are allowed.
- `isOpen` indicates whether the storage is open.
- `open()` recursively creates the root directory and makes it ready for reads and writes.
- `write`, `read`, `exists`, `delete`, and `clear` handle data.

```ts
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 [#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 so `rename` stays atomic.
- `open()` absolutizes with `path.resolve(this.root)` and creates the root with `mkdir(root, { recursive: true })`.
- `clear()` removes the root with `rm(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 [#streams]

Both readable and writable streams are supported.

Obtain a writable stream with the following method.

```ts
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.

```ts
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 [#notes]

- Don't call `write` or `read` before `open()`. The internal connection is `null` until then. Check with `isOpen`.
- `open()` re-initializes even when already open. `root` is kept as an absolute path.
- `write` writes 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 `rename` wins.
- `exists` returns `false` on `access(file)` errors.
- `delete` fails for missing files.
- Permissions follow OS and Node.js defaults.
- `close()` is not implemented. Since `close` in `IStorage` is optional, manage cleanup yourself when needed.

:::warning
Permissions follow OS and Node.js defaults. Concurrent writes to the same key resolve to the last `rename`, so add app-level locking when needed.
:::

## Examples [#examples]

A minimal example storing under `.tmp/`.

```ts
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();
```
