@openclaw/feishu 2026.9.8 → 2026.10.1-beta.2
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/dist/.setup/{channel-DV4hM2Uf.mjs → channel-CX62T71S.mjs} +146 -245
- package/dist/.setup/{channel.runtime-CXQcH5zZ.mjs → channel.runtime-COiaq-Rv.mjs} +6 -12
- package/dist/.setup/{chat-D8MlFAZi.mjs → chat-BdyvImsY.mjs} +6 -9
- package/dist/.setup/{client-BYG-_IZl.mjs → client-BsBVWJzN.mjs} +1 -1
- package/dist/.setup/{doctor-contract-3XC0vbIw.mjs → doctor-contract-B1B9ogdd.mjs} +212 -27
- package/dist/.setup/{monitor-D_rsHBZ6.mjs → monitor-7DvwUs4Y.mjs} +5 -6
- package/dist/.setup/{monitor.account-DvvY7i6k.mjs → monitor.account-7xVu7XWY.mjs} +190 -393
- package/dist/.setup/{probe-_ViqSi0j.mjs → probe-B9o7iey6.mjs} +13 -39
- package/dist/.setup/{reply-delivery-result-COAjaukc.mjs → reply-delivery-result-Bt-BWBEA.mjs} +151 -297
- package/dist/.setup/{setup-api-C4S5I3ac.mjs → setup-api-DmzX9S01.mjs} +1 -1
- package/dist/.setup/{subagent-hooks-BaFoMu8W.mjs → subagent-hooks-CUXg0pC-.mjs} +5 -11
- package/dist/.setup/{thread-bindings-EdLXPXzw.mjs → thread-bindings-jW2x5VuN.mjs} +23 -41
- package/dist/api.js +234 -297
- package/dist/channel-plugin-api.js +1 -1
- package/dist/doctor-contract-api.js +2 -2
- package/dist/session-binding-contract-api.js +1 -1
- package/dist/setup-api.js +1 -1
- package/dist/setup-entry.js +1 -1
- package/dist/subagent-hooks-api.js +1 -1
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +95 -0
- package/node_modules/@openclaw/fs-safe/README.md +70 -379
- package/node_modules/@openclaw/fs-safe/dist/absolute-path.js +35 -51
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-input.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +9 -10
- package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/atomic-io.d.ts +51 -0
- package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +242 -0
- package/node_modules/@openclaw/fs-safe/dist/config.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/config.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/create.js +12 -5
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +11 -0
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +4 -7
- package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.d.ts +55 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +262 -0
- package/node_modules/@openclaw/fs-safe/dist/exclusive-create.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/exclusive-create.js +22 -0
- package/node_modules/@openclaw/fs-safe/dist/file-cleanup.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/file-cleanup.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/file-identity.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-mutation.js +9 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.js +4 -6
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-stale-admission.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -0
- package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/file-store-sync-write.js +20 -18
- package/node_modules/@openclaw/fs-safe/dist/file-store.js +4 -10
- package/node_modules/@openclaw/fs-safe/dist/fs.d.ts +2 -10
- package/node_modules/@openclaw/fs-safe/dist/fs.js +2 -10
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +3 -4
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +6 -18
- package/node_modules/@openclaw/fs-safe/dist/guest-dispatch-python.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +0 -2
- package/node_modules/@openclaw/fs-safe/dist/guest.js +3 -9
- package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-directory.js +1 -5
- package/node_modules/@openclaw/fs-safe/dist/json.js +19 -37
- package/node_modules/@openclaw/fs-safe/dist/move-path.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/mutation-authority.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +9 -1
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +47 -2
- package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +1 -9
- package/node_modules/@openclaw/fs-safe/dist/native-config.js +6 -40
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +5 -2
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +11 -316
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.d.ts +7 -7
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.js +28 -175
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +15 -0
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +418 -0
- package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +9 -4
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +17 -13
- package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +6 -24
- package/node_modules/@openclaw/fs-safe/dist/native.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/path-case.js +10 -9
- package/node_modules/@openclaw/fs-safe/dist/path-scope-lexical.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/path-segment-route.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/path-segment-route.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/path.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +1 -3
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +9 -10
- package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +3 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +18 -32
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +5 -17
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +92 -230
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.d.ts +4 -15
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.js +22 -63
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +9 -34
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +78 -83
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +18 -20
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +64 -98
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +16 -33
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +61 -200
- package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/replace-file.js +76 -216
- package/node_modules/@openclaw/fs-safe/dist/root-boundary.js +14 -24
- package/node_modules/@openclaw/fs-safe/dist/root-context.d.ts +1 -4
- package/node_modules/@openclaw/fs-safe/dist/root-context.js +4 -19
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +52 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +350 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +35 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +2 -27
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +31 -28
- package/node_modules/@openclaw/fs-safe/dist/root-directory.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/root-errors.js +34 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file.js +9 -21
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +105 -37
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +7 -2
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +67 -38
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -2
- package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -3
- package/node_modules/@openclaw/fs-safe/dist/root-path-existing.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/root-path-stat.js +68 -85
- package/node_modules/@openclaw/fs-safe/dist/root-path.js +6 -1
- package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-paths.js +4 -9
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +311 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove.d.ts +11 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +1 -3
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.d.ts +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -10
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +0 -4
- package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -9
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.d.ts +0 -2
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/sibling-temp.js +7 -4
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +13 -6
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +0 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +5 -23
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +11 -6
- package/node_modules/@openclaw/fs-safe/dist/temp-cleanup.js +1 -8
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +2 -9
- package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/trash.js +6 -2
- package/node_modules/@openclaw/fs-safe/dist/walk.js +100 -46
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +7 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +235 -19
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +21 -2
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +21 -3
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +111 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +27 -4
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/watch.js +131 -40
- package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.js +24 -6
- package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.js +51 -0
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -0
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +4 -1
- package/node_modules/@openclaw/fs-safe/docs/config.md +6 -14
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +4 -14
- package/node_modules/@openclaw/fs-safe/docs/durability.md +6 -29
- package/node_modules/@openclaw/fs-safe/docs/entry-publication.md +146 -0
- package/node_modules/@openclaw/fs-safe/docs/errors.md +16 -0
- package/node_modules/@openclaw/fs-safe/docs/file-store.md +28 -5
- package/node_modules/@openclaw/fs-safe/docs/index.md +2 -31
- package/node_modules/@openclaw/fs-safe/docs/install.md +10 -31
- package/node_modules/@openclaw/fs-safe/docs/json.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/local-roots.md +0 -1
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +12 -12
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +35 -96
- package/node_modules/@openclaw/fs-safe/docs/native.md +22 -20
- package/node_modules/@openclaw/fs-safe/docs/path.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/permissions.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/reading.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/root.md +26 -184
- package/node_modules/@openclaw/fs-safe/docs/secret-file.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/security-model.md +115 -3
- package/node_modules/@openclaw/fs-safe/docs/store.md +2 -2
- package/node_modules/@openclaw/fs-safe/docs/temp.md +29 -65
- package/node_modules/@openclaw/fs-safe/docs/testing.md +90 -58
- package/node_modules/@openclaw/fs-safe/docs/types.md +3 -16
- package/node_modules/@openclaw/fs-safe/docs/walk.md +33 -5
- package/node_modules/@openclaw/fs-safe/docs/watch.md +96 -14
- package/node_modules/@openclaw/fs-safe/docs/writing.md +172 -16
- package/node_modules/@openclaw/fs-safe/package.json +10 -10
- package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- package/package.json +5 -5
- package/skills/feishu-wiki/SKILL.md +1 -1
- package/dist/.setup/accounts-wRqItHug.mjs +0 -206
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.d.ts +0 -6
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +0 -88
- package/node_modules/@openclaw/fs-safe/docs/mutation-policy-proof.md +0 -69
- package/node_modules/@openclaw/fs-safe/docs/private-file-store.md +0 -68
- package/node_modules/@openclaw/fs-safe/docs/test-hooks.md +0 -110
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
export const NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH = "FS_SAFE_INTERNAL_RENAME_SOURCE_IDENTITY_MISMATCH";
|
|
2
2
|
export function classifyNativeRenameFailure(error) {
|
|
3
|
-
|
|
3
|
+
try {
|
|
4
|
+
if (error?.code === NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH) {
|
|
5
|
+
return "uncommitted";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
// Unreadable diagnostics cannot prove that the rename was uncommitted.
|
|
10
|
+
}
|
|
4
11
|
// Ordinary errno can follow a committed remote rename whose reply was lost.
|
|
5
|
-
return
|
|
6
|
-
? "uncommitted"
|
|
7
|
-
: "indeterminate";
|
|
12
|
+
return "indeterminate";
|
|
8
13
|
}
|
|
@@ -57,14 +57,14 @@ import { randomUUID } from "node:crypto";
|
|
|
57
57
|
import fs, {} from "node:fs";
|
|
58
58
|
import path from "node:path";
|
|
59
59
|
import { FsSafeError } from "./errors.js";
|
|
60
|
-
import { assertSynchronousCallbackResult,
|
|
60
|
+
import { assertSynchronousCallbackResult, isMutationAuthorityError } from "./mutation-authority.js";
|
|
61
61
|
import { captureNativeFdClose } from "./native-binding.js";
|
|
62
62
|
import { writePinnedInput } from "./pinned-write-input.js";
|
|
63
63
|
import { assertNativeCopyCompleted, createNativeCopyFile } from "./copy-file-input.js";
|
|
64
64
|
import { assertStagedDirectoryCurrent, openStagedDirectory } from "./staged-directory.js";
|
|
65
65
|
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
66
66
|
import { classifyNativeRenameFailure } from "./native-rename-outcome.js";
|
|
67
|
-
import { createStagedFileReceipt,
|
|
67
|
+
import { createStagedFileReceipt, stagedFailure } from "./staged-file-settlement.js";
|
|
68
68
|
export function assertNativeStaging(binding) {
|
|
69
69
|
if ([
|
|
70
70
|
binding.closeOwnedFd,
|
|
@@ -152,7 +152,7 @@ class NativeStagedFile {
|
|
|
152
152
|
}
|
|
153
153
|
catch (error) {
|
|
154
154
|
if (params.overwrite === false && params.input.kind !== "file" && params.input.stageBeforePublish === true) {
|
|
155
|
-
throw
|
|
155
|
+
throw stagedFailure("file", error, { phase: "publish", publication: published });
|
|
156
156
|
}
|
|
157
157
|
throw error;
|
|
158
158
|
}
|
|
@@ -175,7 +175,7 @@ class NativeStagedFile {
|
|
|
175
175
|
return this.#receipt;
|
|
176
176
|
}
|
|
177
177
|
#open() {
|
|
178
|
-
if (this.#state.status
|
|
178
|
+
if (this.#state.status !== "open") {
|
|
179
179
|
throw new FsSafeError("helper-failed", "staged file is closed");
|
|
180
180
|
}
|
|
181
181
|
return this.#state;
|
|
@@ -238,7 +238,7 @@ class NativeStagedFile {
|
|
|
238
238
|
assertDarwinCreationAcl(this.#owner.parentFd, "file");
|
|
239
239
|
// Native copying populates its new file before returning its descriptor.
|
|
240
240
|
const copied = input.kind === "file" && !this.#verifyMode
|
|
241
|
-
? await createNativeCopyFile(this.#owner.binding, input, this.#owner.parentFd, this.#owner.name, maxBytes
|
|
241
|
+
? await createNativeCopyFile(this.#owner.binding, input, this.#owner.parentFd, this.#owner.name, maxBytes)
|
|
242
242
|
: undefined;
|
|
243
243
|
state.fileFd = copied?.fd;
|
|
244
244
|
if (state.fileFd === undefined) {
|
|
@@ -282,10 +282,10 @@ class NativeStagedFile {
|
|
|
282
282
|
catch (error) {
|
|
283
283
|
const { receipt: cleanup, error: cleanupError } = this.#finalize();
|
|
284
284
|
if (cleanupError) {
|
|
285
|
-
throw
|
|
285
|
+
throw stagedFailure("file", new AggregateError([error, cleanupError], "preparation and cleanup failed"), { phase: "prepare", publication: cleanup.publication, cleanup });
|
|
286
286
|
}
|
|
287
287
|
if (cleanup.status === "preserved") {
|
|
288
|
-
throw
|
|
288
|
+
throw stagedFailure("file", error, { phase: "prepare", publication: cleanup.publication, cleanup });
|
|
289
289
|
}
|
|
290
290
|
throw error;
|
|
291
291
|
}
|
|
@@ -319,22 +319,24 @@ class NativeStagedFile {
|
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
catch (error) {
|
|
322
|
+
// Record before inspecting metadata, whose getters can reenter cleanup.
|
|
323
|
+
state.publication = Object.freeze({ status: "indeterminate", basename, overwrite });
|
|
322
324
|
// Only explicit pre-dispatch provenance can rule out a committed rename.
|
|
323
|
-
if (classifyNativeRenameFailure(error) === "
|
|
324
|
-
state.publication =
|
|
325
|
+
if (classifyNativeRenameFailure(error) === "uncommitted") {
|
|
326
|
+
state.publication = NOT_PUBLISHED;
|
|
325
327
|
}
|
|
326
328
|
throw error;
|
|
327
329
|
}
|
|
328
330
|
return this.#completePublication(basename, overwrite, onPublished);
|
|
329
331
|
}
|
|
330
332
|
catch (error) {
|
|
331
|
-
if (error
|
|
333
|
+
if (isMutationAuthorityError(error))
|
|
332
334
|
throw error;
|
|
333
335
|
// Closure rejects further use, not the recorded outcome of an earlier publication.
|
|
334
336
|
const publication = this.#state.status === "closed"
|
|
335
337
|
? this.#state.receipt.publication
|
|
336
338
|
: this.#state.publication;
|
|
337
|
-
throw
|
|
339
|
+
throw stagedFailure("file", error, { phase: "publish", publication });
|
|
338
340
|
}
|
|
339
341
|
}
|
|
340
342
|
#completePublication(basename, overwrite, onPublished) {
|
|
@@ -367,7 +369,9 @@ class NativeStagedFile {
|
|
|
367
369
|
if (this.#state.status === "closed") {
|
|
368
370
|
return this.#state;
|
|
369
371
|
}
|
|
370
|
-
const state = this.#
|
|
372
|
+
const state = this.#open();
|
|
373
|
+
// Consume descriptor authority before native calls or diagnostic getters can reenter.
|
|
374
|
+
state.status = "closing";
|
|
371
375
|
let outcome = "not-needed";
|
|
372
376
|
const errors = [];
|
|
373
377
|
if (state.publication.status === "indeterminate") {
|
|
@@ -404,7 +408,7 @@ class NativeStagedFile {
|
|
|
404
408
|
status: outcome,
|
|
405
409
|
resources,
|
|
406
410
|
});
|
|
407
|
-
const error = errors.length ?
|
|
411
|
+
const error = errors.length ? stagedFailure("file", errors.length === 1 ? errors[0] : new AggregateError(errors, "staged cleanup failed"), { phase: "cleanup", publication: state.publication, cleanup: receipt }) : undefined;
|
|
408
412
|
this.#state = { status: "closed", receipt, error };
|
|
409
413
|
return this.#state;
|
|
410
414
|
}
|
|
@@ -5,7 +5,7 @@ import { captureNativeFdClose } from "./native-binding.js";
|
|
|
5
5
|
import { requireNativeBinding } from "./native.js";
|
|
6
6
|
import { classifyNativeRenameFailure } from "./native-rename-outcome.js";
|
|
7
7
|
import { assertStagedDirectoryCurrent, openStagedDirectory } from "./staged-directory.js";
|
|
8
|
-
import { createStagedFileReceipt } from "./staged-file-settlement.js";
|
|
8
|
+
import { createStagedFileReceipt, stagedFailure } from "./staged-file-settlement.js";
|
|
9
9
|
const NOT_PUBLISHED = Object.freeze({ status: "not-published" });
|
|
10
10
|
function basename(name) {
|
|
11
11
|
if (typeof name !== "string" || !name || name === "." || name === ".." ||
|
|
@@ -13,17 +13,6 @@ function basename(name) {
|
|
|
13
13
|
throw new FsSafeError("invalid-path", "staged symlinks require a direct-child basename");
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
function failure(error, details) {
|
|
17
|
-
let code = "helper-failed";
|
|
18
|
-
try {
|
|
19
|
-
code = error instanceof FsSafeError ? error.code
|
|
20
|
-
: error?.code === "EEXIST" ? "already-exists" : "helper-failed";
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
// Uninspectable error metadata must not interrupt terminal settlement.
|
|
24
|
-
}
|
|
25
|
-
return new FsSafeError(code, `staged symlink ${details.phase} failed`, { cause: error, details });
|
|
26
|
-
}
|
|
27
16
|
function assertAuthority(assertion) {
|
|
28
17
|
assertSynchronousCallbackResult(assertion(), "assertBeforeMutation");
|
|
29
18
|
}
|
|
@@ -101,14 +90,7 @@ class NativeStagedSymlink {
|
|
|
101
90
|
this.#binding.publishStagedSymlink(this.#parentFd, this.#receipt.temporaryBasename, this.#linkFd, name);
|
|
102
91
|
}
|
|
103
92
|
catch (error) {
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
outcome = classifyNativeRenameFailure(error);
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
// Unreadable diagnostics cannot prove that the rename was uncommitted.
|
|
110
|
-
}
|
|
111
|
-
if (outcome === "indeterminate") {
|
|
93
|
+
if (classifyNativeRenameFailure(error) === "indeterminate") {
|
|
112
94
|
this.#publication = Object.freeze({ status: "indeterminate", basename: name, overwrite: false });
|
|
113
95
|
}
|
|
114
96
|
throw error;
|
|
@@ -123,7 +105,7 @@ class NativeStagedSymlink {
|
|
|
123
105
|
return published;
|
|
124
106
|
}
|
|
125
107
|
catch (error) {
|
|
126
|
-
throw
|
|
108
|
+
throw stagedFailure("symlink", error, { phase: "publish", publication: this.#publication });
|
|
127
109
|
}
|
|
128
110
|
finally {
|
|
129
111
|
this.#busy = false;
|
|
@@ -181,7 +163,7 @@ class NativeStagedSymlink {
|
|
|
181
163
|
return receipt;
|
|
182
164
|
}
|
|
183
165
|
catch (error) {
|
|
184
|
-
const wrapped =
|
|
166
|
+
const wrapped = stagedFailure("symlink", error, { phase: "remove-published", publication: this.#publication });
|
|
185
167
|
// A failed unlink is not safe to retry automatically.
|
|
186
168
|
this.#removal = { error: wrapped };
|
|
187
169
|
throw wrapped;
|
|
@@ -225,7 +207,7 @@ class NativeStagedSymlink {
|
|
|
225
207
|
temporaryBasename: this.#receipt.temporaryBasename,
|
|
226
208
|
publication: this.#publication, status, resources,
|
|
227
209
|
});
|
|
228
|
-
const error = errors.length ?
|
|
210
|
+
const error = errors.length ? stagedFailure("symlink", errors.length === 1 ? errors[0] : new AggregateError(errors, "symlink settlement failed"), { phase: "cleanup", publication: this.#publication, cleanup: receipt }) : undefined;
|
|
229
211
|
this.#closed = { receipt, error };
|
|
230
212
|
this.#busy = false;
|
|
231
213
|
if (error)
|
|
@@ -298,6 +280,6 @@ export async function retainSymlinkInDirectory(options) {
|
|
|
298
280
|
}
|
|
299
281
|
}
|
|
300
282
|
// Until admission succeeds, the caller retains every namespace cleanup duty.
|
|
301
|
-
throw
|
|
283
|
+
throw stagedFailure("symlink", errors.length === 1 ? error : new AggregateError(errors, "symlink admission and close failed"), { phase: "prepare", publication: NOT_PUBLISHED });
|
|
302
284
|
}
|
|
303
285
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import { closeSync, openSync, readSync, readdirSync, } from "node:fs";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import { FsSafeError } from "./errors.js";
|
|
4
5
|
import { captureNativeFdClose } from "./native-binding.js";
|
|
5
6
|
import { getFsSafeNativeConfig } from "./native-config.js";
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
8
|
+
// Native registration creates process-lifetime housekeeping. Use the import
|
|
9
|
+
// context rather than retaining the first operation's AsyncLocalStorage stores.
|
|
10
|
+
const runNativeInitialization = AsyncLocalStorage.snapshot();
|
|
7
11
|
let binding;
|
|
8
12
|
let loadError;
|
|
9
13
|
let attempted = false;
|
|
@@ -145,7 +149,7 @@ function loadBundledBinding() {
|
|
|
145
149
|
if (!target) {
|
|
146
150
|
throw new Error(`Unsupported OS or architecture: ${process.platform}-${process.arch}`);
|
|
147
151
|
}
|
|
148
|
-
const loaded = require(nativePackageForTarget(target));
|
|
152
|
+
const loaded = runNativeInitialization(() => require(nativePackageForTarget(target)));
|
|
149
153
|
captureNativeFdClose(loaded);
|
|
150
154
|
return loaded;
|
|
151
155
|
}
|
|
@@ -5,8 +5,9 @@ import { assertDirectoryIdentitySync, inspectDirectoryIdentitySync } from "./dir
|
|
|
5
5
|
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
6
6
|
import { isNotFoundPathError } from "./path.js";
|
|
7
7
|
import { realpathSync } from "./realpath.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { writeTempFile } from "./replace-file-descriptor.js";
|
|
9
|
+
import { AtomicIo, runSync } from "./atomic-io.js";
|
|
10
|
+
import { AtomicTempOwner } from "./replace-file-temp-owner.js";
|
|
10
11
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
11
12
|
function swapAsciiCase(value) {
|
|
12
13
|
return value.replace(/[A-Za-z]/g, character => {
|
|
@@ -54,25 +55,25 @@ function probeEntry(directory, name, names) {
|
|
|
54
55
|
}
|
|
55
56
|
function probeTemporaryEntry(directory) {
|
|
56
57
|
const name = `.fs-safe-case-probe-${randomUUID()}`;
|
|
57
|
-
const
|
|
58
|
+
const io = AtomicIo.sync(fs);
|
|
59
|
+
const owner = new AtomicTempOwner(path.join(directory, name), io);
|
|
58
60
|
try {
|
|
59
61
|
owner.start();
|
|
60
|
-
owner.adopt(
|
|
61
|
-
fsModule: fs,
|
|
62
|
+
owner.adopt(runSync(writeTempFile(io, {
|
|
62
63
|
tempPath: owner.pathname,
|
|
63
64
|
content: "",
|
|
64
65
|
mode: 0o600,
|
|
65
66
|
sync: false,
|
|
66
67
|
onIdentity: owner.onIdentity,
|
|
67
|
-
}));
|
|
68
|
-
owner.assertCurrent(
|
|
68
|
+
})));
|
|
69
|
+
runSync(owner.assertCurrent());
|
|
69
70
|
const observed = probeEntry(directory, name, new Set(fs.readdirSync(directory)));
|
|
70
|
-
owner.assertCurrent(
|
|
71
|
+
runSync(owner.assertCurrent());
|
|
71
72
|
return observed;
|
|
72
73
|
}
|
|
73
74
|
finally {
|
|
74
75
|
// Failed cleanup invalidates the observation; the existing owner retains its exit retry.
|
|
75
|
-
owner.finish({
|
|
76
|
+
runSync(owner.finish({ throwOnCleanupError: true }));
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
function probeDirectory(directory, allowTemporaryProbe, preferredName) {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { isPathInside, isPathRelativeEscape } from "./path.js";
|
|
3
|
-
import { hasWindowsPathAlias, resolvePathFromBasePreservingWindowsRoot } from "./windows-path-alias.js";
|
|
3
|
+
import { hasWindowsPathAlias, isForeignWindowsShareOrDevicePath, resolvePathFromBasePreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
4
4
|
function pathStaysWithinRoot(rootDir, candidatePath) {
|
|
5
5
|
if (process.platform !== "win32") {
|
|
6
6
|
return candidatePath !== rootDir && isPathInside(rootDir, candidatePath);
|
|
7
7
|
}
|
|
8
|
+
// path.relative folds Unicode case, so it alone could admit another host.
|
|
9
|
+
if (isForeignWindowsShareOrDevicePath(candidatePath, [rootDir]))
|
|
10
|
+
return false;
|
|
8
11
|
const relative = path.relative(rootDir, candidatePath);
|
|
9
12
|
return Boolean(relative) && !isPathRelativeEscape(relative);
|
|
10
13
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
export function sameAbsolutePath(left, right) {
|
|
3
|
+
return path.relative(path.resolve(left), path.resolve(right)) === "";
|
|
4
|
+
}
|
|
2
5
|
export function createPathSegmentRoute(segments) {
|
|
3
6
|
const offsets = new Array(segments.length + 1);
|
|
4
7
|
let cursor = 0;
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { realpathSync } from "./realpath.js";
|
|
5
5
|
import { isDriveRelativePath } from "./safe-path-segment.js";
|
|
6
|
-
import { assertNoWindowsPathAlias, hasWindowsPathAlias, pathForWindowsFilesystem, resolvePathFromBasePreservingWindowsRoot, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
6
|
+
import { assertNoWindowsPathAlias, hasWindowsPathAlias, isForeignWindowsShareOrDevicePath, pathForWindowsFilesystem, resolvePathFromBasePreservingWindowsRoot, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
7
7
|
export { assertNoUnsafeDeviceReadPath, isUnsafeDeviceReadPath, matchUnsafeDeviceReadPath, } from "./device-path.js";
|
|
8
8
|
const NOT_FOUND_CODES = new Set(["ENOENT", "ENOTDIR"]);
|
|
9
9
|
const SYMLINK_OPEN_CODES = new Set(["ELOOP", "EINVAL", "ENOTSUP"]);
|
|
@@ -47,6 +47,9 @@ export function isSymlinkOpenError(value) {
|
|
|
47
47
|
}
|
|
48
48
|
export function isPathInside(root, target) {
|
|
49
49
|
if (process.platform === "win32") {
|
|
50
|
+
// Comparison folds Unicode case, so it alone could admit another host.
|
|
51
|
+
if (isForeignWindowsShareOrDevicePath(target, [root]))
|
|
52
|
+
return false;
|
|
50
53
|
const rootForCompare = resolveWindowsPathForComparison(root);
|
|
51
54
|
const targetForCompare = resolveWindowsPathForComparison(target);
|
|
52
55
|
// Resolved drive paths already have canonical separators and case. A full
|
|
@@ -36,7 +36,6 @@ export declare function inspectWindowsAcl(targetPath: string, opts?: {
|
|
|
36
36
|
env?: NodeJS.ProcessEnv;
|
|
37
37
|
exec?: PermissionExec;
|
|
38
38
|
currentUserSid?: string;
|
|
39
|
-
principalSids?: Record<string, string>;
|
|
40
39
|
principalTranslationFailed?: boolean;
|
|
41
40
|
}): Promise<WindowsAclSummary>;
|
|
42
41
|
export declare function formatWindowsAclSummary(summary: WindowsAclSummary): string;
|
|
@@ -3,6 +3,7 @@ import { assertMutationNotDenied } from "./deny-mutations.js";
|
|
|
3
3
|
import { createMutationDenyMatcher } from "./deny-mutation-match.js";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { isPathInside } from "./path.js";
|
|
6
|
+
import { sameAbsolutePath } from "./path-segment-route.js";
|
|
6
7
|
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
7
8
|
import { ordinarySharedAbsoluteInsideRoot, simpleSharedRoute, } from "./pinned-mutation-shared-route.js";
|
|
8
9
|
import { resolvePathViaExistingAncestor } from "./root-path-existing.js";
|
|
@@ -22,9 +23,6 @@ async function canonicalMutationEntryPath(targetPath) {
|
|
|
22
23
|
const canonicalParent = await resolvePathViaExistingAncestor(path.dirname(targetPath));
|
|
23
24
|
return path.join(canonicalParent, path.basename(targetPath));
|
|
24
25
|
}
|
|
25
|
-
function sameAbsolutePath(left, right) {
|
|
26
|
-
return path.relative(path.resolve(left), path.resolve(right)) === "";
|
|
27
|
-
}
|
|
28
26
|
function simplePinnedRoute(rootReal, originalPath) {
|
|
29
27
|
// These routes can depend on raw traversal, platform aliases, home expansion,
|
|
30
28
|
// or live native canonicalization. They retain full admission at every step.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
|
|
2
3
|
import fsSync, {} from "node:fs";
|
|
3
4
|
import fs, {} from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
@@ -41,6 +42,7 @@ export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
|
|
|
41
42
|
let failure;
|
|
42
43
|
try {
|
|
43
44
|
params.assertBeforeMutation?.();
|
|
45
|
+
assertExclusiveCreateLeaf(tempPath);
|
|
44
46
|
handle = params.private
|
|
45
47
|
? await createFileHandle(tempPath, {
|
|
46
48
|
private: true, mode: 0o600, assertBeforeMutation: params.assertBeforeMutation,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { syncFileBestEffort } from "./file-sync.js";
|
|
2
|
+
import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
|
|
2
3
|
import fsSync, {} from "node:fs";
|
|
3
4
|
import fs from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { createFileHandle } from "./create.js";
|
|
6
7
|
import { creationAdmissionFromParent } from "./creation-boundary.js";
|
|
7
8
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
8
|
-
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard,
|
|
9
|
+
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
|
|
9
10
|
import { FsSafeError } from "./errors.js";
|
|
10
11
|
import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
11
12
|
import { sha256Hex } from "./file-identity.js";
|
|
@@ -173,18 +174,17 @@ async function runPinnedWriteFallback(params) {
|
|
|
173
174
|
phase: "parent",
|
|
174
175
|
}));
|
|
175
176
|
}
|
|
176
|
-
if (
|
|
177
|
-
//
|
|
178
|
-
// an in-root symlink (e.g. a skill-bank layout). Use its returned real
|
|
179
|
-
// path for the subsequent guard and target path so we don't re-check the
|
|
180
|
-
// original, possibly-symlinked, lexical path and reject it outright.
|
|
177
|
+
if (!parentGuard) {
|
|
178
|
+
// Guard the returned real parent; the lexical path may contain an allowed symlink.
|
|
181
179
|
const mkdirParams = {
|
|
182
180
|
rootReal: params.rootPath,
|
|
183
181
|
targetPath: parentPath,
|
|
184
182
|
rootIdentity: params.rootIdentity,
|
|
183
|
+
createMissing: params.mkdir,
|
|
185
184
|
assertBeforeMutation: params.assertBeforeMutation,
|
|
186
185
|
beforeComponent: async (componentPath) => {
|
|
187
|
-
|
|
186
|
+
if (params.mkdir)
|
|
187
|
+
await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath);
|
|
188
188
|
},
|
|
189
189
|
};
|
|
190
190
|
parentPath = await mkdirPathComponentsWithGuards(mutationAdmission ? {
|
|
@@ -215,9 +215,7 @@ async function runPinnedWriteFallback(params) {
|
|
|
215
215
|
},
|
|
216
216
|
} : mkdirParams);
|
|
217
217
|
}
|
|
218
|
-
parentGuard ??=
|
|
219
|
-
? await createAsyncDirectoryGuard(parentPath, { bigint: true })
|
|
220
|
-
: await createNearestExistingDirectoryGuard(params.rootPath, parentPath, { bigint: true });
|
|
218
|
+
parentGuard ??= await createAsyncDirectoryGuard(parentPath, { bigint: true });
|
|
221
219
|
const targetPath = path.join(parentPath, params.basename);
|
|
222
220
|
if (mutationAdmission && !parentAdmitted) {
|
|
223
221
|
await mutationAdmission.authorize(Object.freeze({
|
|
@@ -239,6 +237,7 @@ async function runPinnedWriteFallback(params) {
|
|
|
239
237
|
private: true, mode: 0o600, assertBeforeMutation,
|
|
240
238
|
}, creationAdmissionFromParent(parentGuard));
|
|
241
239
|
}
|
|
240
|
+
assertExclusiveCreateLeaf(targetPath);
|
|
242
241
|
return await fs.open(targetPath, fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL, verifyPosixMode ? 0o600 : params.mode);
|
|
243
242
|
}, {
|
|
244
243
|
onPostGuardFailure: async (openedHandle) => {
|
|
@@ -4,6 +4,7 @@ import { FsSafeError } from "./errors.js";
|
|
|
4
4
|
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
5
5
|
import { assertSynchronousCallbackResult } from "./mutation-authority.js";
|
|
6
6
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
7
|
+
import { errorCauseOptions } from "./root-errors.js";
|
|
7
8
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
8
9
|
import { registerTempPathForExit, } from "./temp-cleanup.js";
|
|
9
10
|
function pathMismatch(message) {
|
|
@@ -36,18 +37,14 @@ export function assertParents(params) {
|
|
|
36
37
|
export function normalizeLinkError(error) {
|
|
37
38
|
const code = error?.code;
|
|
38
39
|
if (code === "EEXIST") {
|
|
39
|
-
return new FsSafeError("already-exists", "isolated producer sibling already exists",
|
|
40
|
-
cause: error instanceof Error ? error : undefined,
|
|
41
|
-
});
|
|
40
|
+
return new FsSafeError("already-exists", "isolated producer sibling already exists", errorCauseOptions(error));
|
|
42
41
|
}
|
|
43
42
|
if (code === "EXDEV" ||
|
|
44
43
|
code === "ENOSYS" ||
|
|
45
44
|
code === "ENOTSUP" ||
|
|
46
45
|
code === "EOPNOTSUPP" ||
|
|
47
46
|
code === "EPERM") {
|
|
48
|
-
return new FsSafeError("helper-unavailable", "atomic isolated producer handoff is unavailable",
|
|
49
|
-
cause: error instanceof Error ? error : undefined,
|
|
50
|
-
});
|
|
47
|
+
return new FsSafeError("helper-unavailable", "atomic isolated producer handoff is unavailable", errorCauseOptions(error));
|
|
51
48
|
}
|
|
52
49
|
return error;
|
|
53
50
|
}
|
|
@@ -3,8 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { assertSyncDirectoryGuard } from "./directory-guard.js";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
6
|
-
// Node has no portable no-replace rename.
|
|
7
|
-
// then
|
|
6
|
+
// Node has no portable no-replace rename. Publish the completed stage by link,
|
|
7
|
+
// then remove its verified temporary name without yielding.
|
|
8
8
|
export function publishCopyStage(params) {
|
|
9
9
|
const { identity, temporaryPath, targetPath } = params;
|
|
10
10
|
const assertCurrent = () => {
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import fsSync, {} from "node:fs";
|
|
3
3
|
import fs, {} from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
|
|
5
6
|
import { pinDirectory, } from "./directory-durability.js";
|
|
6
7
|
import { FsSafeError } from "./errors.js";
|
|
7
8
|
import { ownDirectoryReceipt } from "./directory-receipt.js";
|
|
@@ -21,7 +22,6 @@ const NATIVE_COPY_FALLBACK_CODES = new Set(["EINVAL", "ENOSYS", "ENOTSUP", "EOPN
|
|
|
21
22
|
export function isHardlinkFallbackError(error) {
|
|
22
23
|
return HARDLINK_FALLBACK_CODES.has(error?.code ?? "");
|
|
23
24
|
}
|
|
24
|
-
function sourceOpenFlags() { return resolveReadOpenFlags(); }
|
|
25
25
|
function directoryOpenFlags() {
|
|
26
26
|
return (fsSync.constants.O_RDONLY |
|
|
27
27
|
(typeof fsSync.constants.O_DIRECTORY === "number" ? fsSync.constants.O_DIRECTORY : 0));
|
|
@@ -105,7 +105,7 @@ async function copyPinnedSource(params) {
|
|
|
105
105
|
createdIdentity = fsSync.fstatSync(nativeFd, { bigint: true });
|
|
106
106
|
await getFsSafeTestHooks()?.afterPublishTargetCreated?.("exclusive-copy", params.targetPath, createdIdentity);
|
|
107
107
|
const identity = fsSync.lstatSync(params.targetPath, { bigint: true });
|
|
108
|
-
target = await fs.open(params.targetPath,
|
|
108
|
+
target = await fs.open(params.targetPath, resolveReadOpenFlags());
|
|
109
109
|
const opened = fsSync.fstatSync(target.fd, { bigint: true });
|
|
110
110
|
if (identity.isSymbolicLink() ||
|
|
111
111
|
!identity.isFile() ||
|
|
@@ -134,6 +134,7 @@ async function copyPinnedSource(params) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
+
assertExclusiveCreateLeaf(params.targetPath);
|
|
137
138
|
const target = await fs.open(params.targetPath, "wx+", 0o600);
|
|
138
139
|
try {
|
|
139
140
|
const createdIdentity = fsSync.fstatSync(target.fd, { bigint: true });
|
|
@@ -216,7 +217,7 @@ export async function publishFileExclusive(params) {
|
|
|
216
217
|
if (sourcePathStat.isSymbolicLink() || !sourcePathStat.isFile()) {
|
|
217
218
|
throw new FsSafeError("not-file", "publication source must be a regular file");
|
|
218
219
|
}
|
|
219
|
-
const source = await fs.open(sourcePath,
|
|
220
|
+
const source = await fs.open(sourcePath, resolveReadOpenFlags());
|
|
220
221
|
let parent;
|
|
221
222
|
let sourceNativeParent;
|
|
222
223
|
let targetNativeParent;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type {
|
|
3
|
-
export declare function writeAtomicDestination(
|
|
4
|
-
export declare function writeAtomicDestinationSync(fsModule: Pick<typeof syncFs, "ftruncateSync" | "writeSync">, fd: number, data: Buffer, beforeWrite?: () => void, onWriting?: () => void): void;
|
|
1
|
+
import type { AtomicFile, Procedure } from "./atomic-io.js";
|
|
2
|
+
import type { AtomicDestination } from "./replace-file-destination.js";
|
|
3
|
+
export declare function writeAtomicDestination(file: AtomicFile, data: Buffer, destination?: AtomicDestination, restore?: boolean): Procedure<void>;
|
|
@@ -1,36 +1,22 @@
|
|
|
1
|
-
export
|
|
2
|
-
if (
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
onWriting?.();
|
|
1
|
+
export function* writeAtomicDestination(file, data, destination, restore = false) {
|
|
2
|
+
if (destination)
|
|
3
|
+
yield* destination.beforeWrite(restore);
|
|
4
|
+
yield* file.truncate(0);
|
|
5
|
+
destination?.writing();
|
|
7
6
|
let written = 0;
|
|
8
7
|
while (written < data.length) {
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
if (destination)
|
|
9
|
+
yield* destination.beforeWrite(restore);
|
|
10
|
+
let result = file.write(data, written, data.length - written, written);
|
|
11
|
+
if (file.io.asynchronous) {
|
|
12
|
+
const completed = (yield result);
|
|
13
|
+
if (completed.bytesWritten === 0)
|
|
14
|
+
throw new Error("Copy fallback write made no progress");
|
|
15
|
+
result = completed.bytesWritten;
|
|
16
|
+
}
|
|
17
|
+
written += result;
|
|
16
18
|
}
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
await handle.truncate(data.length);
|
|
21
|
-
}
|
|
22
|
-
export function writeAtomicDestinationSync(fsModule, fd, data, beforeWrite, onWriting) {
|
|
23
|
-
beforeWrite?.();
|
|
24
|
-
fsModule.ftruncateSync(fd, 0);
|
|
25
|
-
onWriting?.();
|
|
26
|
-
let written = 0;
|
|
27
|
-
while (written < data.length) {
|
|
28
|
-
beforeWrite?.();
|
|
29
|
-
const bytesWritten = fsModule.writeSync(fd, data, written, data.length - written, written);
|
|
30
|
-
if (bytesWritten === 0)
|
|
31
|
-
throw new Error("Copy fallback write made no progress");
|
|
32
|
-
written += bytesWritten;
|
|
33
|
-
}
|
|
34
|
-
beforeWrite?.();
|
|
35
|
-
fsModule.ftruncateSync(fd, data.length);
|
|
19
|
+
if (destination)
|
|
20
|
+
yield* destination.beforeWrite(restore);
|
|
21
|
+
yield* file.truncate(data.length);
|
|
36
22
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
2
|
+
import { type AtomicIo, type Procedure } from "./atomic-io.js";
|
|
2
3
|
import { AtomicMutation } from "./replace-file-mutation.js";
|
|
3
4
|
export type ReplaceFileDestinationHardlinkPolicy = "reject";
|
|
4
5
|
export type ReplaceFileCopyFallbackRestorePolicy = "restore-original" | "none";
|
|
@@ -6,16 +7,8 @@ export type ReplaceFileAtomicRestoreCleanup = "restored" | "restore-failed";
|
|
|
6
7
|
export type ReplaceFileAtomicRestoreFailureDetails = {
|
|
7
8
|
cleanup: ReplaceFileAtomicRestoreCleanup;
|
|
8
9
|
};
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
open: typeof import("node:fs/promises").open;
|
|
12
|
-
rm: typeof import("node:fs/promises").rm;
|
|
13
|
-
};
|
|
14
|
-
type SyncFallbackFs = Pick<typeof syncFs, "closeSync" | "fstatSync" | "fsyncSync" | "ftruncateSync" | "lstatSync" | "openSync" | "readSync" | "rmSync" | "writeSync">;
|
|
15
|
-
export declare function assertDestinationHardlinkPolicy(fsModule: AsyncFallbackFs, dest: string, policy?: ReplaceFileDestinationHardlinkPolicy): Promise<void>;
|
|
16
|
-
export declare function assertDestinationHardlinkPolicySync(fsModule: SyncFallbackFs, dest: string, policy?: ReplaceFileDestinationHardlinkPolicy): void;
|
|
17
|
-
export declare function copyFallbackReplace(params: {
|
|
18
|
-
fsModule: AsyncFallbackFs;
|
|
10
|
+
export declare function assertDestinationHardlinkPolicy(io: AtomicIo, dest: string, policy?: ReplaceFileDestinationHardlinkPolicy): Procedure<void>;
|
|
11
|
+
export declare function copyFallbackReplace(io: AtomicIo, params: {
|
|
19
12
|
src: string;
|
|
20
13
|
dest: string;
|
|
21
14
|
destinationHardlinks?: ReplaceFileDestinationHardlinkPolicy;
|
|
@@ -24,9 +17,4 @@ export declare function copyFallbackReplace(params: {
|
|
|
24
17
|
expectedSourceIdentity?: BigIntStats;
|
|
25
18
|
sync: boolean;
|
|
26
19
|
mutation?: AtomicMutation;
|
|
27
|
-
}):
|
|
28
|
-
export declare function copyFallbackReplaceSync(params: Omit<Parameters<typeof copyFallbackReplace>[0], "fsModule"> & {
|
|
29
|
-
fsModule: SyncFallbackFs;
|
|
30
|
-
fchmodSync?: (fd: number, mode: number) => void;
|
|
31
|
-
}): void;
|
|
32
|
-
export {};
|
|
20
|
+
}): Procedure<void>;
|