External outputs
@openclaw/fs-safe/output covers the case where another library insists on writing to an absolute path you give it. Browser downloads, renderers, media tools, and native libraries often have this shape:
import { writeExternalFileWithinRoot } from "@openclaw/fs-safe/output";
await writeExternalFileWithinRoot({
rootDir: "/srv/workspace/downloads",
path: "reports/today.pdf",
write: async (filePath) => {
await download.saveAs(filePath);
},
});
The external writer never receives the final destination path. It receives a private temp file path instead. After the callback returns, fs-safe copies that staged file into the requested target through the same root boundary used by Root.copyIn().
#Signature
function writeExternalFileWithinRoot<T = void>(
options: ExternalFileWriteOptions<T>,
): Promise<ExternalFileWriteResult<T>>;
type ExternalFileWriteOptions<T = void> = {
rootDir: string;
path: string; // relative or absolute, but must stay under rootDir
write: (filePath: string) => Promise<T>;
maxBytes?: number;
mode?: number;
};
type ExternalFileWriteResult<T = void> = {
path: string; // final absolute path under the canonical root
result: T; // value returned by write()
};
The requested path must name a file. Missing destination parents are created by the helper because the operation is "produce this output file under the root"; callers should choose the filename before calling this API.
Use maxBytes when the external producer can create arbitrarily large files. Use mode when the finalized file needs a specific POSIX mode. Both are enforced during the Root.copyIn() finalization step, after the external writer has produced the staged file and before the final target is committed.
#Why not pass the final path to the library?
If a target parent can be swapped after validation, handing an external library the final path can make the library write outside the intended root before fs-safe has a chance to finalize or reject the operation. This helper stages in a private temp workspace first, then finalizes with Root.copyIn(). That keeps the trust-boundary write inside fs-safe's root-aware copy/atomic-write path.
#Browser download example
const outputPath = requestedOutputPath || sanitizeBrowserSuggestedName(suggestedFilename);
await writeExternalFileWithinRoot({
rootDir: downloadsRoot,
path: outputPath,
maxBytes: 512 * 1024 * 1024,
write: async (filePath) => {
await download.saveAs(filePath);
},
});
The chosen path may be absolute if it is already inside downloadsRoot, or relative to downloadsRoot. Traversal, symlink parent escapes, hardlinked final targets, over-large staged files, and missing temp files surface as FsSafeErrors.
This helper is not the right fit when the final filename depends on inspecting the produced bytes. In that case, write to a private temp workspace, sniff or validate the file, choose the final name, then copy or write into the root with the normal root APIs.
#See also
- Root writes —
write,copyIn,move, andmkdir. - Temp workspaces — private scratch directories for longer workflows.
pathScope()— validation-only helper when you must pass an
absolute path directly to another library.