fs-safe/docs/regular-file.md
2026-05-05 20:57:44 +01:00

5.3 KiB

Regular file helpers

The advanced regular-file helpers provide direct read/append/stat helpers for absolute file paths, with an explicit "regular file or nothing" contract. Useful when you have a trusted absolute path and want a thin layer on top of fs that:

  • refuses non-regular files (directories, FIFOs, sockets, symlinks)
  • enforces a maxBytes read cap
  • separates "missing" from "io-error" in the result type
import {
  readRegularFile,
  readRegularFileSync,
  appendRegularFile,
  appendRegularFileSync,
  resolveRegularFileAppendFlags,
  statRegularFile,
  statRegularFileSync,
  type AppendRegularFileOptions,
  type RegularFileStatResult,
} from "@openclaw/fs-safe/advanced";

Stat

statRegularFile(filePath)

Async. Returns:

type RegularFileStatResult =
  | { missing: true }
  | { missing: false; stat: Stats };

A non-regular file (directory, FIFO, …) returns { missing: false } with a stat whose isFile() is false — the helper does not throw, you decide what to do.

import { statRegularFile } from "@openclaw/fs-safe/advanced";

const r = await statRegularFile("/var/log/app.log");
if (r.missing) return;
if (!r.stat.isFile()) throw new Error("expected a regular file");
console.log(`size=${r.stat.size}`);

statRegularFileSync(filePath)

Synchronous variant. Same shape.

Read

readRegularFile(params)

Async. Reads the entire file into a Buffer if it is a regular file, with maxBytes enforcement.

import { readRegularFile } from "@openclaw/fs-safe/advanced";

const result = await readRegularFile({
  filePath: "/var/log/app.log",
  maxBytes: 4 * 1024 * 1024,
});
if (result.missing) return null;
if (!result.regular) throw new Error("not a regular file");
processLog(result.buffer);

Result shape:

type Result =
  | { missing: true }
  | { missing: false; regular: false; stat: Stats }
  | { missing: false; regular: true; stat: Stats; buffer: Buffer };

Throws FsSafeError with code too-large if the file exceeds maxBytes. Other I/O errors propagate as NodeJS.ErrnoException.

readRegularFileSync(params)

Synchronous variant. Same shape; the only required field is filePath. maxBytes is optional.

Append

appendRegularFile(options)

Async. Opens the file in append mode, writes data, closes. Refuses non-regular targets:

import { appendRegularFile } from "@openclaw/fs-safe/advanced";

await appendRegularFile({
  filePath: "/var/log/app.log",
  data: `[${new Date().toISOString()}] ${line}\n`,
  encoding: "utf8",
  prependNewlineIfNeeded: true,
});

Options

type AppendRegularFileOptions = {
  filePath: string;
  data: string | Buffer;
  encoding?: BufferEncoding;             // default utf8 when data is string
  prependNewlineIfNeeded?: boolean;      // insert "\n" if file does not end with one
  flags?: number;                         // raw open flags; default O_WRONLY | O_APPEND
  mode?: number;                          // default 0o644 if file is created
};

prependNewlineIfNeeded reads the trailing byte of the existing file and prepends a \n to your data if it isn't already present. Useful for log appenders that want to preserve line boundaries even when callers forget the newline.

appendRegularFileSync(options)

Synchronous. Same options.

resolveRegularFileAppendFlags(append, truncateExisting)

Helper that returns the right open-flag bitmask for combinations of "append" / "truncate". Use it when you're building your own open path and want to match the append helpers' behavior:

import { resolveRegularFileAppendFlags } from "@openclaw/fs-safe/advanced";

const flags = resolveRegularFileAppendFlags(true, false); // O_WRONLY | O_APPEND | O_CREAT

Difference from Root methods

regular-file Root
Absolute paths only. Relative to the root.
No identity check post-open. Identity check on every read/write.
Caller must be confident the path is trusted. Boundary check is automatic.
Returns explicit {missing, regular} shape. Throws FsSafeError with code.

If your call site already trusts the path (it came from your own config, not a caller), regular-file is a thinner, faster surface. If the path is caller-influenced, prefer root() or wrap in pathScope().

Common patterns

Read a config file if it's there, else seed

const r = await readRegularFile({ filePath: "/etc/app/config.json", maxBytes: 64 * 1024 });
if (r.missing) {
  await writeJson("/etc/app/config.json", defaultConfig);
} else if (r.regular) {
  applyConfig(JSON.parse(r.buffer.toString("utf8")));
} else {
  throw new Error("/etc/app/config.json is not a regular file");
}

Cheap "exists and is a file" check

const r = await statRegularFile(p);
if (r.missing || !r.stat.isFile()) return false;
return true;

Bounded log tail

const r = await readRegularFile({ filePath: logPath, maxBytes: 1 * 1024 * 1024 });
if (r.missing || !r.regular) return [];
return r.buffer.toString("utf8").split("\n").slice(-100);

See also

  • ReadingRoot reads with boundary checks.
  • Atomic writes — for atomic write semantics, prefer replaceFileAtomic.
  • fs.appendFile — Node's stock append, without regular-file gating.