refactor: trim temp workspace surface
This commit is contained in:
parent
542657b9d2
commit
02b9a9d2ae
@ -159,26 +159,6 @@ type FileStorePruneOptions = {
|
||||
|
||||
Symlinks are skipped. The walk is best-effort — failures on individual entries don't abort the whole prune. Compares against `mtimeMs`.
|
||||
|
||||
## Standalone: `copyIntoRoot`
|
||||
|
||||
The same one-shot copy primitive used by `FileStore.copyIn`, available from the advanced surface for callers that don't want to instantiate a store:
|
||||
|
||||
```ts
|
||||
import { copyIntoRoot } from "@openclaw/fs-safe/advanced";
|
||||
|
||||
await copyIntoRoot({
|
||||
rootDir: "/var/cache/app",
|
||||
relativePath: "ingest/upload.bin",
|
||||
sourcePath: "/tmp/upload.bin",
|
||||
mode: 0o600,
|
||||
dirMode: 0o700,
|
||||
maxBytes: 32 * 1024 * 1024,
|
||||
tempPrefix: ".upload.bin", // optional
|
||||
});
|
||||
```
|
||||
|
||||
Returns the final absolute path. Throws `not-file` if the source is a symlink or non-regular file; throws `too-large` if it exceeds `maxBytes`.
|
||||
|
||||
## Difference from `Root`
|
||||
|
||||
| `FileStore` | `Root` |
|
||||
@ -230,4 +210,4 @@ await root.move(`pending/${id}`, `done/${id}`);
|
||||
- [`root()`](root.md) — the boundary `FileStore` is built on; reach for it when you need move/list/append.
|
||||
- [JSON store](json-store.md) — the JSON-state-file equivalent of this surface.
|
||||
- [Atomic writes](atomic.md) — `writeSiblingTempFile` is what every write goes through.
|
||||
- [Temp workspaces](temp.md) — `TempWorkspace.copyIn` uses `copyIntoRoot`.
|
||||
- [Temp workspaces](temp.md) — private scratch directories backed by `FileStore`.
|
||||
|
||||
11
docs/temp.md
11
docs/temp.md
@ -24,9 +24,8 @@ The compact factory. Returns:
|
||||
type TempWorkspace = {
|
||||
dir: string;
|
||||
store: FileStore;
|
||||
file(fileName: string): string;
|
||||
path(fileName: string): string;
|
||||
writePrivate(fileName: string, data: string | Uint8Array): Promise<string>;
|
||||
write(fileName: string, data: string | Uint8Array): Promise<string>;
|
||||
writeText(fileName: string, data: string): Promise<string>;
|
||||
writeJson(fileName: string, data: unknown, options?: { trailingNewline?: boolean }): Promise<string>;
|
||||
copyIn(fileName: string, sourcePath: string): Promise<string>;
|
||||
@ -40,11 +39,11 @@ type TempWorkspace = {
|
||||
import { tempWorkspace } from "@openclaw/fs-safe/temp";
|
||||
|
||||
await using workspace = await tempWorkspace({ rootDir: "/tmp/my-app", prefix: "build-" });
|
||||
const inputPath = await workspace.writePrivate("input.txt", "data");
|
||||
const inputPath = await workspace.write("input.txt", "data");
|
||||
await runBuild(workspace.dir, inputPath);
|
||||
```
|
||||
|
||||
`writePrivate` writes at `mode` (default `0o600`); `writeText` and `writeJson` are convenience wrappers for the common scratch-file shapes; `copyIn` ingests an absolute source path through the same atomic-rename machinery as `Root.copyIn`. `read` is a small accessor that reads back any file you wrote into the workspace.
|
||||
`write` writes at `mode` (default `0o600`); `writeText` and `writeJson` are convenience wrappers for the common scratch-file shapes; `copyIn` ingests an absolute source path through the same atomic-rename machinery as `Root.copyIn`. `read` is a small accessor that reads back any file you wrote into the workspace.
|
||||
|
||||
`store` is a `fileStore({ rootDir: workspace.dir, private: true })` handle. Use
|
||||
it when you want the richer store surface, including `writeStream`, `exists`,
|
||||
@ -70,7 +69,7 @@ The recommended shape. Auto-cleanup on every exit path:
|
||||
import { withTempWorkspace } from "@openclaw/fs-safe/temp";
|
||||
|
||||
const result = await withTempWorkspace({ rootDir: "/tmp/my-app", prefix: "build-" }, async (workspace) => {
|
||||
await workspace.writePrivate("input.txt", "data");
|
||||
await workspace.write("input.txt", "data");
|
||||
return await runBuild(workspace.dir);
|
||||
});
|
||||
```
|
||||
@ -101,7 +100,7 @@ type TempWorkspaceOptions = {
|
||||
rootDir: string; // parent directory for workspaces
|
||||
prefix: string; // dir prefix (sanitized)
|
||||
dirMode?: number; // dir mode; default 0o700
|
||||
mode?: number; // writePrivate file mode; default 0o600
|
||||
mode?: number; // file write mode; default 0o600
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
@ -15,7 +15,6 @@ export {
|
||||
export { sameFileIdentity, type FileIdentityStat } from "./file-identity.js";
|
||||
export { sanitizeUntrustedFileName } from "./filename.js";
|
||||
export { pathExists, pathExistsSync } from "./fs.js";
|
||||
export { copyIntoRoot, fileStoreSync, type FileStoreSync } from "./file-store.js";
|
||||
export {
|
||||
resolveLocalPathFromRootsSync,
|
||||
readLocalFileFromRoots,
|
||||
|
||||
@ -134,7 +134,7 @@ function isNotFound(error: unknown): boolean {
|
||||
(error as NodeJS.ErrnoException).code === "ENOTDIR";
|
||||
}
|
||||
|
||||
export async function copyIntoRoot(params: {
|
||||
async function copyIntoRoot(params: {
|
||||
rootDir: string;
|
||||
relativePath: string;
|
||||
sourcePath: string;
|
||||
|
||||
@ -20,9 +20,8 @@ export type TempWorkspaceOptions = {
|
||||
export type TempWorkspace = {
|
||||
dir: string;
|
||||
store: FileStore;
|
||||
file(fileName: string): string;
|
||||
path(fileName: string): string;
|
||||
writePrivate(fileName: string, data: string | Uint8Array): Promise<string>;
|
||||
write(fileName: string, data: string | Uint8Array): Promise<string>;
|
||||
writeText(fileName: string, data: string): Promise<string>;
|
||||
writeJson(
|
||||
fileName: string,
|
||||
@ -38,9 +37,8 @@ export type TempWorkspace = {
|
||||
export type TempWorkspaceSync = {
|
||||
dir: string;
|
||||
store: FileStoreSync;
|
||||
file(fileName: string): string;
|
||||
path(fileName: string): string;
|
||||
writePrivate(fileName: string, data: string | Uint8Array): string;
|
||||
write(fileName: string, data: string | Uint8Array): string;
|
||||
writeText(fileName: string, data: string): string;
|
||||
writeJson(fileName: string, data: unknown, options?: { trailingNewline?: boolean }): string;
|
||||
read(fileName: string): Buffer;
|
||||
@ -119,9 +117,8 @@ async function createTempWorkspace(
|
||||
return {
|
||||
dir,
|
||||
store,
|
||||
file: (fileName) => resolveWorkspaceLeaf(dir, fileName),
|
||||
path: (fileName) => resolveWorkspaceLeaf(dir, fileName),
|
||||
writePrivate: async (fileName, data) =>
|
||||
write: async (fileName, data) =>
|
||||
await store.write(assertWorkspaceFileName(fileName), data, { mode }),
|
||||
writeText: async (fileName, data) =>
|
||||
await store.writeText(assertWorkspaceFileName(fileName), data, { mode }),
|
||||
@ -200,9 +197,8 @@ export function tempWorkspaceSync(
|
||||
return {
|
||||
dir,
|
||||
store,
|
||||
file: (fileName) => resolveWorkspaceLeaf(dir, fileName),
|
||||
path: (fileName) => resolveWorkspaceLeaf(dir, fileName),
|
||||
writePrivate: (fileName, data) =>
|
||||
write: (fileName, data) =>
|
||||
store.write(assertWorkspaceFileName(fileName), data, { mode }),
|
||||
writeText: (fileName, data) =>
|
||||
store.writeText(assertWorkspaceFileName(fileName), data, { mode }),
|
||||
|
||||
@ -9,7 +9,7 @@ import { extractArchive } from "../src/archive.js";
|
||||
import { loadZipArchiveWithPreflight, readZipCentralDirectoryEntryCount } from "../src/archive-zip-preflight.js";
|
||||
import { createAsyncLock } from "../src/async-lock.js";
|
||||
import { writeTextAtomic } from "../src/atomic.js";
|
||||
import { copyIntoRoot, fileStore, fileStoreSync } from "../src/file-store.js";
|
||||
import { fileStore, fileStoreSync } from "../src/file-store.js";
|
||||
import {
|
||||
assertCanonicalPathWithinBase,
|
||||
resolveSafeInstallDir,
|
||||
@ -624,8 +624,8 @@ describe("temporary workspace and symlink parent helpers", () => {
|
||||
await fs.writeFile(source, "copy", "utf8");
|
||||
|
||||
const workspace = await tempWorkspace({ rootDir: root, prefix: "bad prefix!" });
|
||||
expect(() => workspace.file("../bad")).toThrow("Invalid temp workspace");
|
||||
const privateFile = await workspace.writePrivate("private.bin", Buffer.from("private"));
|
||||
expect(() => workspace.path("../bad")).toThrow("Invalid temp workspace");
|
||||
const privateFile = await workspace.write("private.bin", Buffer.from("private"));
|
||||
await workspace.store.writeText("store.txt", "stored");
|
||||
const textFile = await workspace.writeText("text.txt", "text");
|
||||
const jsonFile = await workspace.writeJson("data.json", { ok: true }, {
|
||||
@ -648,8 +648,8 @@ describe("temporary workspace and symlink parent helpers", () => {
|
||||
|
||||
const syncWorkspace = tempWorkspaceSync({ rootDir: root, prefix: ".." });
|
||||
try {
|
||||
expect(() => syncWorkspace.file("bad/name")).toThrow("Invalid temp workspace");
|
||||
expect(syncWorkspace.writePrivate("private.bin", Buffer.from("private"))).toContain(
|
||||
expect(() => syncWorkspace.path("bad/name")).toThrow("Invalid temp workspace");
|
||||
expect(syncWorkspace.write("private.bin", Buffer.from("private"))).toContain(
|
||||
"private.bin",
|
||||
);
|
||||
expect(syncWorkspace.store.writeText("store.txt", "stored")).toContain("store.txt");
|
||||
@ -756,7 +756,7 @@ describe("file stores and private stores", () => {
|
||||
maxBytes: 4,
|
||||
})).rejects.toMatchObject({ code: "too-large" });
|
||||
await expect(store.copyIn("copied.txt", source)).resolves.toBe(path.join(root, "copied.txt"));
|
||||
await expect(copyIntoRoot({ rootDir: root, relativePath: "bad.txt", sourcePath: sourceRoot }))
|
||||
await expect(store.copyIn("bad.txt", sourceRoot))
|
||||
.rejects
|
||||
.toMatchObject({ code: "not-file" });
|
||||
await expect(store.exists("copied.txt")).resolves.toBe(true);
|
||||
|
||||
@ -52,7 +52,7 @@ describe("private temp workspaces", () => {
|
||||
let workspaceDir = "";
|
||||
const content = await withTempWorkspace({ rootDir: root, prefix: "work-" }, async (tmp) => {
|
||||
workspaceDir = tmp.dir;
|
||||
const filePath = await tmp.writePrivate("input.txt", "hello");
|
||||
const filePath = await tmp.write("input.txt", "hello");
|
||||
expect(await fs.readFile(filePath, "utf8")).toBe("hello");
|
||||
return await tmp.read("input.txt");
|
||||
});
|
||||
@ -64,7 +64,7 @@ describe("private temp workspaces", () => {
|
||||
it("rejects path-like file names", async () => {
|
||||
const tmp = await tempWorkspace({ rootDir: root, prefix: "work-" });
|
||||
try {
|
||||
await expect(tmp.writePrivate("../escape.txt", "nope")).rejects.toThrow(/Invalid/);
|
||||
await expect(tmp.write("../escape.txt", "nope")).rejects.toThrow(/Invalid/);
|
||||
} finally {
|
||||
await tmp.cleanup();
|
||||
}
|
||||
@ -74,7 +74,7 @@ describe("private temp workspaces", () => {
|
||||
let workspaceDir = "";
|
||||
const result = withTempWorkspaceSync({ rootDir: root, prefix: "sync-" }, (tmp) => {
|
||||
workspaceDir = tmp.dir;
|
||||
const filePath = tmp.writePrivate("input.txt", "hello");
|
||||
const filePath = tmp.write("input.txt", "hello");
|
||||
expect(tmp.read("input.txt").toString("utf8")).toBe("hello");
|
||||
return filePath;
|
||||
});
|
||||
@ -83,7 +83,7 @@ describe("private temp workspaces", () => {
|
||||
|
||||
const tmp = tempWorkspaceSync({ rootDir: root, prefix: "sync-" });
|
||||
try {
|
||||
expect(tmp.writePrivate("again.txt", "ok")).toContain("again.txt");
|
||||
expect(tmp.write("again.txt", "ok")).toContain("again.txt");
|
||||
} finally {
|
||||
tmp.cleanup();
|
||||
}
|
||||
@ -94,8 +94,8 @@ describe("private temp workspaces", () => {
|
||||
{
|
||||
await using tmp = await tempWorkspace({ rootDir: root, prefix: "compact-" });
|
||||
workspaceDir = tmp.dir;
|
||||
const filePath = await tmp.writePrivate("input.txt", "hello");
|
||||
expect(filePath).toBe(tmp.file("input.txt"));
|
||||
const filePath = await tmp.write("input.txt", "hello");
|
||||
expect(filePath).toBe(tmp.path("input.txt"));
|
||||
expect(tmp.path("input.txt")).toBe(filePath);
|
||||
await tmp.store.json<{ ok: boolean }>("state.json").write({ ok: true });
|
||||
await expect(tmp.store.readJson("state.json")).resolves.toEqual({ ok: true });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user