@openclaw/fs-safe 0.4.7 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +82 -43
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +5 -4
- package/dist/archive-errors.d.ts +11 -0
- package/dist/archive-errors.d.ts.map +1 -0
- package/dist/archive-errors.js +16 -0
- package/dist/archive-input.d.ts +12 -0
- package/dist/archive-input.d.ts.map +1 -0
- package/dist/archive-input.js +72 -0
- package/dist/archive-kind.d.ts +1 -1
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +18 -0
- package/dist/archive-limits.d.ts +10 -0
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +18 -0
- package/dist/archive-native.d.ts +18 -0
- package/dist/archive-native.d.ts.map +1 -0
- package/dist/archive-native.js +115 -0
- package/dist/archive-options.d.ts +21 -0
- package/dist/archive-options.d.ts.map +1 -0
- package/dist/archive-options.js +1 -0
- package/dist/archive-policy.d.ts +20 -0
- package/dist/archive-policy.d.ts.map +1 -0
- package/dist/archive-policy.js +29 -0
- package/dist/archive-read.d.ts +6 -0
- package/dist/archive-read.d.ts.map +1 -0
- package/dist/archive-read.js +200 -0
- package/dist/archive-staging.d.ts +2 -5
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +19 -10
- package/dist/archive-tar-meta.d.ts +6 -0
- package/dist/archive-tar-meta.d.ts.map +1 -0
- package/dist/archive-tar-meta.js +143 -0
- package/dist/archive-tar-runtime.d.ts +43 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -0
- package/dist/archive-tar-runtime.js +8 -0
- package/dist/archive-tar.d.ts +5 -1
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +24 -6
- package/dist/archive-zip-entry.d.ts +15 -0
- package/dist/archive-zip-entry.d.ts.map +1 -0
- package/dist/archive-zip-entry.js +17 -0
- package/dist/archive.d.ts +7 -17
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +111 -108
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/containment.d.ts +2 -0
- package/dist/containment.d.ts.map +1 -0
- package/dist/containment.js +1 -0
- package/dist/durability.d.ts +2 -0
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +2 -0
- package/dist/errors.d.ts +4 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/file-hash.d.ts +10 -0
- package/dist/file-hash.d.ts.map +1 -0
- package/dist/file-hash.js +73 -0
- package/dist/file-lock-sync.d.ts +35 -0
- package/dist/file-lock-sync.d.ts.map +1 -0
- package/dist/file-lock-sync.js +233 -0
- package/dist/file-lock.d.ts +2 -0
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +4 -1
- package/dist/guarded-mkdir.d.ts +7 -1
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +45 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +30 -15
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +101 -17
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
- package/dist/native-binding.d.ts +79 -0
- package/dist/native-binding.d.ts.map +1 -0
- package/dist/native-binding.js +1 -0
- package/dist/native-config.d.ts +17 -0
- package/dist/native-config.d.ts.map +1 -0
- package/dist/native-config.js +71 -0
- package/dist/native-operations.d.ts +21 -0
- package/dist/native-operations.d.ts.map +1 -0
- package/dist/native-operations.js +115 -0
- package/dist/native-pinned-write.d.ts +5 -0
- package/dist/native-pinned-write.d.ts.map +1 -0
- package/dist/native-pinned-write.js +115 -0
- package/dist/native.d.ts +14 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +204 -0
- package/dist/output-sibling.d.ts +8 -0
- package/dist/output-sibling.d.ts.map +1 -0
- package/dist/output-sibling.js +122 -0
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +32 -5
- package/dist/owner-dacl.d.ts +31 -0
- package/dist/owner-dacl.d.ts.map +1 -0
- package/dist/owner-dacl.js +27 -0
- package/dist/path-policy.js +1 -1
- package/dist/permissions-public.d.ts +2 -0
- package/dist/permissions-public.d.ts.map +1 -1
- package/dist/permissions-public.js +2 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +6 -0
- package/dist/pinned-operation.d.ts +2 -0
- package/dist/pinned-operation.d.ts.map +1 -0
- package/dist/pinned-operation.js +35 -0
- package/dist/pinned-write.d.ts +3 -16
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +20 -85
- package/dist/private-directory.d.ts +5 -0
- package/dist/private-directory.d.ts.map +1 -0
- package/dist/private-directory.js +13 -0
- package/dist/private-temp-workspace.d.ts +6 -2
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +40 -13
- package/dist/publish-file-failure.d.ts +27 -0
- package/dist/publish-file-failure.d.ts.map +1 -0
- package/dist/publish-file-failure.js +21 -0
- package/dist/publish-file.d.ts +21 -0
- package/dist/publish-file.d.ts.map +1 -0
- package/dist/publish-file.js +351 -0
- package/dist/read-opened-file.d.ts +3 -0
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +1 -0
- package/dist/replace-file-copy-fallback.d.ts +34 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -0
- package/dist/replace-file-copy-fallback.js +339 -0
- package/dist/replace-file.d.ts +6 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +39 -90
- package/dist/root-context.d.ts +4 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +20 -1
- package/dist/root-impl.d.ts +5 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +128 -225
- package/dist/root-path-existing.d.ts +3 -0
- package/dist/root-path-existing.d.ts.map +1 -0
- package/dist/root-path-existing.js +69 -0
- package/dist/root-path.d.ts +1 -2
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +67 -81
- package/dist/root-walk.d.ts +41 -0
- package/dist/root-walk.d.ts.map +1 -0
- package/dist/root-walk.js +114 -0
- package/dist/root.d.ts +2 -0
- package/dist/root.d.ts.map +1 -1
- package/dist/secret-file.d.ts +5 -2
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +20 -2
- package/dist/secret-read-async.d.ts +4 -0
- package/dist/secret-read-async.d.ts.map +1 -0
- package/dist/secret-read-async.js +95 -0
- package/dist/secret.d.ts +2 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +2 -1
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +1 -0
- package/dist/sidecar-lock-handle.d.ts +20 -0
- package/dist/sidecar-lock-handle.d.ts.map +1 -0
- package/dist/sidecar-lock-handle.js +28 -0
- package/dist/sidecar-lock-policy.d.ts +10 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -0
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts +22 -6
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +106 -20
- package/dist/sidecar-lock-types.d.ts +56 -0
- package/dist/sidecar-lock-types.d.ts.map +1 -0
- package/dist/sidecar-lock-types.js +1 -0
- package/dist/sidecar-lock.d.ts +2 -45
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +94 -55
- package/dist/temp-cleanup.d.ts +7 -1
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +35 -5
- package/dist/temp.d.ts +2 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +3 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/windows-permissions-native.d.ts +9 -0
- package/dist/windows-permissions-native.d.ts.map +1 -0
- package/dist/windows-permissions-native.js +31 -0
- package/docs/archive.md +131 -13
- package/docs/atomic.md +30 -1
- package/docs/config.md +51 -21
- package/docs/durability.md +211 -1
- package/docs/errors.md +36 -6
- package/docs/filename.md +0 -0
- package/docs/index.md +11 -8
- package/docs/install.md +27 -26
- package/docs/json-store.md +18 -3
- package/docs/migrating-to-0.5.md +196 -0
- package/docs/native-helper.md +85 -0
- package/docs/native.md +134 -0
- package/docs/output.md +37 -11
- package/docs/path.md +1 -1
- package/docs/permissions.md +84 -1
- package/docs/quickstart.md +5 -2
- package/docs/reading.md +4 -4
- package/docs/root.md +31 -15
- package/docs/secret-file.md +50 -1
- package/docs/security-model.md +29 -8
- package/docs/sidecar-lock.md +92 -7
- package/docs/temp.md +24 -1
- package/docs/test-hooks.md +22 -2
- package/docs/testing.md +18 -7
- package/docs/types.md +3 -1
- package/docs/walk.md +55 -0
- package/docs/writing.md +3 -3
- package/package.json +8 -2
- package/dist/pinned-helper.d.ts +0 -9
- package/dist/pinned-helper.d.ts.map +0 -1
- package/dist/pinned-helper.js +0 -19
- package/dist/pinned-path.d.ts +0 -7
- package/dist/pinned-path.d.ts.map +0 -1
- package/dist/pinned-path.js +0 -21
- package/dist/pinned-python-config.d.ts +0 -9
- package/dist/pinned-python-config.d.ts.map +0 -1
- package/dist/pinned-python-config.js +0 -38
- package/dist/pinned-python.d.ts +0 -12
- package/dist/pinned-python.d.ts.map +0 -1
- package/dist/pinned-python.js +0 -688
- package/docs/python-helper.md +0 -103
package/dist/pinned-write.js
CHANGED
|
@@ -8,8 +8,9 @@ import { syncDirectoryBestEffort } from "./fsync.js";
|
|
|
8
8
|
import { sameFileIdentity, sha256Hex } from "./file-identity.js";
|
|
9
9
|
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
10
10
|
import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { runPinnedWriteNative } from "./native-pinned-write.js";
|
|
12
|
+
import { getNativeBinding } from "./native.js";
|
|
13
|
+
import { validatePinnedOperationPayload } from "./pinned-operation.js";
|
|
13
14
|
import { withSidecarLock } from "./sidecar-lock.js";
|
|
14
15
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
15
16
|
function byteLength(input, encoding) {
|
|
@@ -57,74 +58,21 @@ async function writeStreamToHandle(stream, handle, maxBytes) {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
async function inputToBase64(input, maxBytes) {
|
|
61
|
-
if (input.kind === "buffer") {
|
|
62
|
-
assertWithinMaxBytes(byteLength(input.data, input.encoding), maxBytes);
|
|
63
|
-
return (typeof input.data === "string"
|
|
64
|
-
? Buffer.from(input.data, input.encoding ?? "utf8")
|
|
65
|
-
: input.data).toString("base64");
|
|
66
|
-
}
|
|
67
|
-
const chunks = [];
|
|
68
|
-
let bytes = 0;
|
|
69
|
-
for await (const chunk of input.stream) {
|
|
70
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
71
|
-
bytes += buffer.byteLength;
|
|
72
|
-
assertWithinMaxBytes(bytes, maxBytes);
|
|
73
|
-
chunks.push(buffer);
|
|
74
|
-
}
|
|
75
|
-
return Buffer.concat(chunks, bytes).toString("base64");
|
|
76
|
-
}
|
|
77
61
|
export async function runPinnedWriteHelper(params) {
|
|
78
62
|
assertSafeBasename(params.basename);
|
|
79
63
|
validatePinnedOperationPayload({
|
|
80
64
|
relativeParentPath: params.relativeParentPath,
|
|
81
65
|
});
|
|
82
|
-
// The
|
|
83
|
-
//
|
|
84
|
-
// Node fallback, where content verification can replace that one check.
|
|
66
|
+
// The explicit compatibility policy uses the guarded Node fallback, where
|
|
67
|
+
// content verification can replace the strict post-rename inode check.
|
|
85
68
|
if (params.onRenameIdentityMismatch === "verify-content") {
|
|
86
69
|
return await runPinnedWriteFallback(params);
|
|
87
70
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (params.input.kind === "stream") {
|
|
92
|
-
try {
|
|
93
|
-
assertPinnedPythonOperationAvailable();
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
if (canFallbackFromPythonError(error)) {
|
|
97
|
-
return await runPinnedWriteFallback(params);
|
|
98
|
-
}
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
const input = params.input.kind === "stream"
|
|
103
|
-
? { kind: "buffer", data: Buffer.from(await inputToBase64(params.input, params.maxBytes), "base64") }
|
|
104
|
-
: params.input;
|
|
105
|
-
const payload = {
|
|
106
|
-
base64: await inputToBase64(input, params.maxBytes),
|
|
107
|
-
basename: params.basename,
|
|
108
|
-
maxBytes: params.maxBytes ?? -1,
|
|
109
|
-
mkdir: params.mkdir,
|
|
110
|
-
mode: params.mode || 0o600,
|
|
111
|
-
overwrite: params.overwrite !== false,
|
|
112
|
-
relativeParentPath: params.relativeParentPath,
|
|
113
|
-
...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
|
|
114
|
-
};
|
|
115
|
-
try {
|
|
116
|
-
return await runPinnedPythonOperation({
|
|
117
|
-
operation: "write",
|
|
118
|
-
rootPath: params.rootPath,
|
|
119
|
-
payload,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
catch (error) {
|
|
123
|
-
if (canFallbackFromPythonError(error)) {
|
|
124
|
-
return await runPinnedWriteFallback({ ...params, input });
|
|
125
|
-
}
|
|
126
|
-
throw error;
|
|
71
|
+
const native = getNativeBinding();
|
|
72
|
+
if (native && params.overwrite === false) {
|
|
73
|
+
return await runPinnedWriteNative(native, params);
|
|
127
74
|
}
|
|
75
|
+
return await runPinnedWriteFallback(params);
|
|
128
76
|
}
|
|
129
77
|
export async function runPinnedWriteWithRenamePolicy(params) {
|
|
130
78
|
const { targetPath, renameIdentity, ...writeParams } = params;
|
|
@@ -147,34 +95,19 @@ export async function runPinnedWriteWithRenamePolicy(params) {
|
|
|
147
95
|
onRenameIdentityMismatch: "verify-content",
|
|
148
96
|
}));
|
|
149
97
|
}
|
|
150
|
-
export async function runPinnedCopyHelper(params) {
|
|
151
|
-
assertSafeBasename(params.basename);
|
|
152
|
-
validatePinnedOperationPayload({
|
|
153
|
-
relativeParentPath: params.relativeParentPath,
|
|
154
|
-
});
|
|
155
|
-
return await runPinnedPythonOperation({
|
|
156
|
-
operation: "copy",
|
|
157
|
-
rootPath: params.rootPath,
|
|
158
|
-
payload: {
|
|
159
|
-
basename: params.basename,
|
|
160
|
-
maxBytes: params.maxBytes ?? -1,
|
|
161
|
-
mkdir: params.mkdir,
|
|
162
|
-
mode: params.mode || 0o600,
|
|
163
|
-
overwrite: params.overwrite !== false,
|
|
164
|
-
relativeParentPath: params.relativeParentPath,
|
|
165
|
-
...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
|
|
166
|
-
sourceDev: params.sourceIdentity.dev,
|
|
167
|
-
sourceIno: params.sourceIdentity.ino,
|
|
168
|
-
sourcePath: params.sourcePath,
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
98
|
async function runPinnedWriteFallback(params) {
|
|
173
|
-
|
|
99
|
+
let parentPath = params.relativeParentPath
|
|
174
100
|
? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
|
|
175
101
|
: params.rootPath;
|
|
176
102
|
if (params.mkdir) {
|
|
177
|
-
|
|
103
|
+
// mkdirPathComponentsWithGuards may resolve the final component through
|
|
104
|
+
// an in-root symlink (e.g. a skill-bank layout). Use its returned real
|
|
105
|
+
// path for the subsequent guard and target path so we don't re-check the
|
|
106
|
+
// original, possibly-symlinked, lexical path and reject it outright.
|
|
107
|
+
parentPath = await mkdirPathComponentsWithGuards({
|
|
108
|
+
rootReal: params.rootPath,
|
|
109
|
+
targetPath: parentPath,
|
|
110
|
+
});
|
|
178
111
|
}
|
|
179
112
|
const parentGuard = params.mkdir
|
|
180
113
|
? await createAsyncDirectoryGuard(parentPath)
|
|
@@ -190,6 +123,7 @@ async function runPinnedWriteFallback(params) {
|
|
|
190
123
|
});
|
|
191
124
|
let created = true;
|
|
192
125
|
try {
|
|
126
|
+
await handle.chmod(params.mode);
|
|
193
127
|
if (params.input.kind === "buffer") {
|
|
194
128
|
assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
|
|
195
129
|
if (typeof params.input.data === "string") {
|
|
@@ -229,6 +163,7 @@ async function runPinnedWriteFallback(params) {
|
|
|
229
163
|
let renamed = false;
|
|
230
164
|
try {
|
|
231
165
|
handle = await fs.open(tempPath, tempFlags, params.mode);
|
|
166
|
+
await handle.chmod(params.mode);
|
|
232
167
|
if (params.input.kind === "buffer") {
|
|
233
168
|
assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
|
|
234
169
|
if (typeof params.input.data === "string") {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"private-directory.d.ts","sourceRoot":"","sources":["../src/private-directory.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,6BAA6B,GACtC,OAAO,CAAC,IAAI,CAAC,CAiBf"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import { getNativeBinding } from "./native.js";
|
|
3
|
+
export async function createPrivateDirectory(targetPath, options) {
|
|
4
|
+
const platform = options?.platform ?? process.platform;
|
|
5
|
+
if (platform !== "win32") {
|
|
6
|
+
throw new FsSafeError("helper-unavailable", "private-directory creation is available only on Windows through the bundled native binding");
|
|
7
|
+
}
|
|
8
|
+
const native = getNativeBinding();
|
|
9
|
+
if (!native) {
|
|
10
|
+
throw new FsSafeError("helper-unavailable", "private Windows directory creation requires the bundled native binding");
|
|
11
|
+
}
|
|
12
|
+
native.createPrivateDirectory(targetPath);
|
|
13
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type FileStore, type FileStoreSync } from "./file-store.js";
|
|
2
|
+
import { type TempPathIdentityReceipt } from "./temp-cleanup.js";
|
|
3
|
+
export type TempWorkspaceCleanupResult = "removed" | "missing" | "identity-mismatch";
|
|
2
4
|
export type TempWorkspaceOptions = {
|
|
3
5
|
rootDir: string;
|
|
4
6
|
prefix: string;
|
|
@@ -7,6 +9,7 @@ export type TempWorkspaceOptions = {
|
|
|
7
9
|
};
|
|
8
10
|
export type TempWorkspace = {
|
|
9
11
|
dir: string;
|
|
12
|
+
identity: TempPathIdentityReceipt;
|
|
10
13
|
store: FileStore;
|
|
11
14
|
path(fileName: string): string;
|
|
12
15
|
write(fileName: string, data: string | Uint8Array): Promise<string>;
|
|
@@ -16,11 +19,12 @@ export type TempWorkspace = {
|
|
|
16
19
|
}): Promise<string>;
|
|
17
20
|
copyIn(fileName: string, sourcePath: string): Promise<string>;
|
|
18
21
|
read(fileName: string): Promise<Buffer>;
|
|
19
|
-
cleanup(): Promise<
|
|
22
|
+
cleanup(): Promise<TempWorkspaceCleanupResult>;
|
|
20
23
|
[Symbol.asyncDispose](): Promise<void>;
|
|
21
24
|
};
|
|
22
25
|
export type TempWorkspaceSync = {
|
|
23
26
|
dir: string;
|
|
27
|
+
identity: TempPathIdentityReceipt;
|
|
24
28
|
store: FileStoreSync;
|
|
25
29
|
path(fileName: string): string;
|
|
26
30
|
write(fileName: string, data: string | Uint8Array): string;
|
|
@@ -29,7 +33,7 @@ export type TempWorkspaceSync = {
|
|
|
29
33
|
trailingNewline?: boolean;
|
|
30
34
|
}): string;
|
|
31
35
|
read(fileName: string): Buffer;
|
|
32
|
-
cleanup():
|
|
36
|
+
cleanup(): TempWorkspaceCleanupResult;
|
|
33
37
|
[Symbol.dispose](): void;
|
|
34
38
|
};
|
|
35
39
|
export declare function tempWorkspace(options: TempWorkspaceOptions): Promise<TempWorkspace>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,SAAS,GAAG,mBAAmB,CAAC;AAErF,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,uBAAuB,CAAC;IAClC,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3D,SAAS,CACP,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GACtC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9D,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,OAAO,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAC/C,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,uBAAuB,CAAC;IAClC,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IAC3D,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAClD,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM,CAAC;IAC5F,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,OAAO,IAAI,0BAA0B,CAAC;IACtC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B,CAAC;AA0IF,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,aAAa,CAAC,CAExB;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5C,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,oBAAoB,GAC5B,iBAAiB,CAsEnB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,CAAC,GACvC,CAAC,CAUH"}
|
|
@@ -4,7 +4,8 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileStore, fileStoreSync, } from "./file-store.js";
|
|
6
6
|
import { openRootFileSync } from "./root-file.js";
|
|
7
|
-
import {
|
|
7
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
8
|
+
import { registerTempPathForExit, } from "./temp-cleanup.js";
|
|
8
9
|
function sanitizeTempPrefix(prefix) {
|
|
9
10
|
const sanitized = prefix.trim().replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
10
11
|
if (!sanitized || sanitized === "." || sanitized === "..") {
|
|
@@ -49,6 +50,34 @@ function ensurePrivateDirectorySync(dir, mode) {
|
|
|
49
50
|
// Best-effort on platforms that do not enforce POSIX modes.
|
|
50
51
|
}
|
|
51
52
|
}
|
|
53
|
+
async function cleanupWorkspace(dir, identity) {
|
|
54
|
+
let current;
|
|
55
|
+
try {
|
|
56
|
+
current = await fs.lstat(dir);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return error.code === "ENOENT" ? "missing" : "identity-mismatch";
|
|
60
|
+
}
|
|
61
|
+
if (!sameFileIdentity(current, identity)) {
|
|
62
|
+
return "identity-mismatch";
|
|
63
|
+
}
|
|
64
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
65
|
+
return "removed";
|
|
66
|
+
}
|
|
67
|
+
function cleanupWorkspaceSync(dir, identity) {
|
|
68
|
+
let current;
|
|
69
|
+
try {
|
|
70
|
+
current = fsSync.lstatSync(dir);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return error.code === "ENOENT" ? "missing" : "identity-mismatch";
|
|
74
|
+
}
|
|
75
|
+
if (!sameFileIdentity(current, identity)) {
|
|
76
|
+
return "identity-mismatch";
|
|
77
|
+
}
|
|
78
|
+
fsSync.rmSync(dir, { recursive: true, force: true });
|
|
79
|
+
return "removed";
|
|
80
|
+
}
|
|
52
81
|
async function createTempWorkspace(options) {
|
|
53
82
|
const dirMode = options.dirMode ?? 0o700;
|
|
54
83
|
const mode = options.mode ?? 0o600;
|
|
@@ -56,15 +85,17 @@ async function createTempWorkspace(options) {
|
|
|
56
85
|
const root = await fs.realpath(requestedRoot).catch(() => requestedRoot);
|
|
57
86
|
await ensurePrivateDirectory(root, dirMode);
|
|
58
87
|
const dir = await fs.mkdtemp(path.join(root, sanitizeTempPrefix(options.prefix)));
|
|
59
|
-
const unregisterTempDir = registerTempPathForExit(dir, { recursive: true });
|
|
60
88
|
await fs.chmod(dir, dirMode).catch(() => undefined);
|
|
61
89
|
const stat = await fs.lstat(dir);
|
|
62
90
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
63
91
|
throw new Error(`Temp workspace must be a directory: ${dir}`);
|
|
64
92
|
}
|
|
93
|
+
const identity = { dev: stat.dev, ino: stat.ino };
|
|
94
|
+
const unregisterTempDir = registerTempPathForExit(dir, { recursive: true, identity });
|
|
65
95
|
const store = fileStore({ rootDir: dir, private: true, dirMode, mode });
|
|
66
96
|
return {
|
|
67
97
|
dir,
|
|
98
|
+
identity,
|
|
68
99
|
store,
|
|
69
100
|
path: (fileName) => resolveWorkspaceLeaf(dir, fileName),
|
|
70
101
|
write: async (fileName, data) => await store.write(assertWorkspaceFileName(fileName), data, { mode }),
|
|
@@ -77,7 +108,7 @@ async function createTempWorkspace(options) {
|
|
|
77
108
|
read: async (fileName) => await store.readBytes(assertWorkspaceFileName(fileName)),
|
|
78
109
|
cleanup: async () => {
|
|
79
110
|
try {
|
|
80
|
-
await
|
|
111
|
+
return await cleanupWorkspace(dir, identity);
|
|
81
112
|
}
|
|
82
113
|
finally {
|
|
83
114
|
unregisterTempDir();
|
|
@@ -85,7 +116,7 @@ async function createTempWorkspace(options) {
|
|
|
85
116
|
},
|
|
86
117
|
[Symbol.asyncDispose]: async () => {
|
|
87
118
|
try {
|
|
88
|
-
await
|
|
119
|
+
await cleanupWorkspace(dir, identity);
|
|
89
120
|
}
|
|
90
121
|
finally {
|
|
91
122
|
unregisterTempDir();
|
|
@@ -121,7 +152,6 @@ export function tempWorkspaceSync(options) {
|
|
|
121
152
|
}
|
|
122
153
|
ensurePrivateDirectorySync(root, dirMode);
|
|
123
154
|
const dir = fsSync.mkdtempSync(path.join(root, sanitizeTempPrefix(options.prefix)));
|
|
124
|
-
const unregisterTempDir = registerTempPathForExit(dir, { recursive: true });
|
|
125
155
|
try {
|
|
126
156
|
fsSync.chmodSync(dir, dirMode);
|
|
127
157
|
}
|
|
@@ -132,9 +162,12 @@ export function tempWorkspaceSync(options) {
|
|
|
132
162
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
133
163
|
throw new Error(`Temp workspace must be a directory: ${dir}`);
|
|
134
164
|
}
|
|
165
|
+
const identity = { dev: stat.dev, ino: stat.ino };
|
|
166
|
+
const unregisterTempDir = registerTempPathForExit(dir, { recursive: true, identity });
|
|
135
167
|
const store = fileStoreSync({ rootDir: dir, private: true, dirMode, mode });
|
|
136
168
|
return {
|
|
137
169
|
dir,
|
|
170
|
+
identity,
|
|
138
171
|
store,
|
|
139
172
|
path: (fileName) => resolveWorkspaceLeaf(dir, fileName),
|
|
140
173
|
write: (fileName, data) => store.write(assertWorkspaceFileName(fileName), data, { mode }),
|
|
@@ -162,10 +195,7 @@ export function tempWorkspaceSync(options) {
|
|
|
162
195
|
},
|
|
163
196
|
cleanup: () => {
|
|
164
197
|
try {
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
// Best-effort cleanup.
|
|
198
|
+
return cleanupWorkspaceSync(dir, identity);
|
|
169
199
|
}
|
|
170
200
|
finally {
|
|
171
201
|
unregisterTempDir();
|
|
@@ -173,10 +203,7 @@ export function tempWorkspaceSync(options) {
|
|
|
173
203
|
},
|
|
174
204
|
[Symbol.dispose]: () => {
|
|
175
205
|
try {
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
// Best-effort cleanup.
|
|
206
|
+
cleanupWorkspaceSync(dir, identity);
|
|
180
207
|
}
|
|
181
208
|
finally {
|
|
182
209
|
unregisterTempDir();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import type { FileIdentityStat } from "./file-identity.js";
|
|
3
|
+
export type PublishFileExclusiveSyncFailurePolicy = "rollback" | "preserve";
|
|
4
|
+
export type PublishFileExclusiveDirectorySyncFailure = {
|
|
5
|
+
status: "failed";
|
|
6
|
+
code?: string;
|
|
7
|
+
};
|
|
8
|
+
export type PublishFileExclusiveFailurePhase = "copy-create" | "copy-verify" | "directory-sync" | "hardlink-create" | "hardlink-verify" | "rename-create" | "rename-verify";
|
|
9
|
+
export type PublishFileExclusiveCleanup = "removed" | "preserved" | "unknown";
|
|
10
|
+
export type PublishFileExclusiveFailureDetails = {
|
|
11
|
+
phase: PublishFileExclusiveFailurePhase;
|
|
12
|
+
targetCreated: boolean;
|
|
13
|
+
targetIdentity?: FileIdentityStat;
|
|
14
|
+
cleanup: PublishFileExclusiveCleanup;
|
|
15
|
+
directorySync?: PublishFileExclusiveDirectorySyncFailure;
|
|
16
|
+
};
|
|
17
|
+
export type PublishFailureState = {
|
|
18
|
+
phase: PublishFileExclusiveFailurePhase;
|
|
19
|
+
targetCreated: boolean;
|
|
20
|
+
targetIdentity?: FileIdentityStat;
|
|
21
|
+
preserveTarget: boolean;
|
|
22
|
+
directorySync?: PublishFileExclusiveDirectorySyncFailure;
|
|
23
|
+
};
|
|
24
|
+
export declare function rememberCreatedTarget(state: PublishFailureState, identity: FileIdentityStat, phase: PublishFileExclusiveFailurePhase): void;
|
|
25
|
+
export declare function publicationFailure(error: unknown, state: PublishFailureState, cleanup: PublishFileExclusiveCleanup): FsSafeError;
|
|
26
|
+
export declare function directorySyncFailure(error: unknown): PublishFileExclusiveDirectorySyncFailure;
|
|
27
|
+
//# sourceMappingURL=publish-file-failure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-file-failure.d.ts","sourceRoot":"","sources":["../src/publish-file-failure.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,MAAM,qCAAqC,GAAG,UAAU,GAAG,UAAU,CAAC;AAC5E,MAAM,MAAM,wCAAwC,GAAG;IACrD,MAAM,EAAE,QAAQ,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AACF,MAAM,MAAM,gCAAgC,GACxC,aAAa,GACb,aAAa,GACb,gBAAgB,GAChB,iBAAiB,GACjB,iBAAiB,GACjB,eAAe,GACf,eAAe,CAAC;AACpB,MAAM,MAAM,2BAA2B,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;AAC9E,MAAM,MAAM,kCAAkC,GAAG;IAC/C,KAAK,EAAE,gCAAgC,CAAC;IACxC,aAAa,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,OAAO,EAAE,2BAA2B,CAAC;IACrC,aAAa,CAAC,EAAE,wCAAwC,CAAC;CAC1D,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,gCAAgC,CAAC;IACxC,aAAa,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,cAAc,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,wCAAwC,CAAC;CAC1D,CAAC;AAEF,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,mBAAmB,EAC1B,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,gCAAgC,GACtC,IAAI,CAIN;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,2BAA2B,GACnC,WAAW,CAcb;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,OAAO,GACb,wCAAwC,CAG1C"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
export function rememberCreatedTarget(state, identity, phase) {
|
|
3
|
+
state.targetCreated = true;
|
|
4
|
+
state.targetIdentity = { dev: identity.dev, ino: identity.ino };
|
|
5
|
+
state.phase = phase;
|
|
6
|
+
}
|
|
7
|
+
export function publicationFailure(error, state, cleanup) {
|
|
8
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
9
|
+
const details = {
|
|
10
|
+
phase: state.phase,
|
|
11
|
+
targetCreated: state.targetCreated,
|
|
12
|
+
...(state.targetIdentity ? { targetIdentity: state.targetIdentity } : {}),
|
|
13
|
+
...(state.directorySync ? { directorySync: state.directorySync } : {}),
|
|
14
|
+
cleanup,
|
|
15
|
+
};
|
|
16
|
+
return new FsSafeError(error instanceof FsSafeError ? error.code : "helper-failed", `exclusive file publication failed during ${state.phase}: ${cause.message}`, { cause, details });
|
|
17
|
+
}
|
|
18
|
+
export function directorySyncFailure(error) {
|
|
19
|
+
const code = error?.code;
|
|
20
|
+
return typeof code === "string" ? { status: "failed", code } : { status: "failed" };
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Stats } from "node:fs";
|
|
2
|
+
import { type DirectoryReceipt, type DirectorySyncOutcome } from "./directory-durability.js";
|
|
3
|
+
import { type FileIdentityStat } from "./file-identity.js";
|
|
4
|
+
import { type PublishFileExclusiveSyncFailurePolicy } from "./publish-file-failure.js";
|
|
5
|
+
export type { PublishFileExclusiveCleanup, PublishFileExclusiveDirectorySyncFailure, PublishFileExclusiveFailureDetails, PublishFileExclusiveFailurePhase, PublishFileExclusiveSyncFailurePolicy, } from "./publish-file-failure.js";
|
|
6
|
+
export type PublishFileExclusiveStrategy = "link-or-copy" | "link-required" | "rename-noreplace";
|
|
7
|
+
export type PublishFileExclusiveResult = {
|
|
8
|
+
method: "hardlink" | "exclusive-copy" | "rename-noreplace";
|
|
9
|
+
identity: Stats;
|
|
10
|
+
directorySync: DirectorySyncOutcome;
|
|
11
|
+
};
|
|
12
|
+
export declare function isHardlinkFallbackError(error: unknown): boolean;
|
|
13
|
+
export declare function publishFileExclusive(params: {
|
|
14
|
+
sourcePath: string;
|
|
15
|
+
targetPath: string;
|
|
16
|
+
expectedSourceIdentity?: FileIdentityStat;
|
|
17
|
+
strategy: PublishFileExclusiveStrategy;
|
|
18
|
+
onSyncFailure?: PublishFileExclusiveSyncFailurePolicy;
|
|
19
|
+
parentReceipt?: DirectoryReceipt;
|
|
20
|
+
}): Promise<PublishFileExclusiveResult>;
|
|
21
|
+
//# sourceMappingURL=publish-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-file.d.ts","sourceRoot":"","sources":["../src/publish-file.ts"],"names":[],"mappings":"AACA,OAAe,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAI7C,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG7E,OAAO,EAML,KAAK,qCAAqC,EAC3C,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EACV,2BAA2B,EAC3B,wCAAwC,EACxC,kCAAkC,EAClC,gCAAgC,EAChC,qCAAqC,GACtC,MAAM,2BAA2B,CAAC;AAEnC,MAAM,MAAM,4BAA4B,GACpC,cAAc,GACd,eAAe,GACf,kBAAkB,CAAC;AAEvB,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;IAC3D,QAAQ,EAAE,KAAK,CAAC;IAChB,aAAa,EAAE,oBAAoB,CAAC;CACrC,CAAC;AAkBF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D;AAoND,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,gBAAgB,CAAC;IAC1C,QAAQ,EAAE,4BAA4B,CAAC;IACvC,aAAa,CAAC,EAAE,qCAAqC,CAAC;IACtD,aAAa,CAAC,EAAE,gBAAgB,CAAC;CAClC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CA8LtC"}
|