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
maxBytesread 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
- Reading —
Rootreads with boundary checks. - Atomic writes — for atomic write semantics, prefer
replaceFileAtomic. fs.appendFile— Node's stock append, without regular-file gating.