@openclaw/openshell-sandbox 2026.9.6 → 2026.9.7
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/assets/icon.png +0 -0
- package/dist/index.js +75 -229
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +102 -0
- package/node_modules/@openclaw/fs-safe/README.md +24 -6
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +6 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +1 -2
- package/node_modules/@openclaw/fs-safe/dist/archive-plan.d.ts +2 -7
- package/node_modules/@openclaw/fs-safe/dist/archive-read.js +9 -18
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -2
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-directory.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-entry.d.ts +11 -11
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-entry.js +3 -35
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-integrity.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-integrity.js +2 -12
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.d.ts +7 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +13 -10
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-manifest.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-preflight.d.ts +2 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-preflight.js +16 -8
- package/node_modules/@openclaw/fs-safe/dist/archive.d.ts +6 -3
- package/node_modules/@openclaw/fs-safe/dist/archive.js +22 -19
- package/node_modules/@openclaw/fs-safe/dist/atomic.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-publication.d.ts +1 -3
- package/node_modules/@openclaw/fs-safe/dist/copy-publication.js +2 -6
- package/node_modules/@openclaw/fs-safe/dist/deny-mutation-match.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/deny-mutation-match.js +71 -0
- package/node_modules/@openclaw/fs-safe/dist/deny-mutations.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +0 -3
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +20 -2
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +77 -1
- package/node_modules/@openclaw/fs-safe/dist/directory-receipt.js +5 -7
- package/node_modules/@openclaw/fs-safe/dist/effective-uid.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +3 -1
- package/node_modules/@openclaw/fs-safe/dist/errors.js +3 -2
- package/node_modules/@openclaw/fs-safe/dist/file-identity.js +10 -2
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-acquisition.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-admission.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-held.d.ts +4 -11
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-held.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.d.ts +3 -6
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-options.d.ts +2 -10
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.d.ts +2 -4
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.d.ts +3 -7
- package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +7 -10
- package/node_modules/@openclaw/fs-safe/dist/file-store-prune.js +7 -6
- package/node_modules/@openclaw/fs-safe/dist/file-store.d.ts +4 -7
- package/node_modules/@openclaw/fs-safe/dist/file-store.js +19 -21
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +23 -31
- package/node_modules/@openclaw/fs-safe/dist/guest.js +21 -12
- package/node_modules/@openclaw/fs-safe/dist/json-document-store.d.ts +4 -9
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.d.ts +1 -5
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue.js +2 -7
- package/node_modules/@openclaw/fs-safe/dist/local-file-access.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/local-file-descriptor.d.ts +2 -5
- package/node_modules/@openclaw/fs-safe/dist/local-roots.d.ts +2 -7
- package/node_modules/@openclaw/fs-safe/dist/move-path-cleanup.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +51 -14
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +8 -2
- package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +303 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch-worker.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch-worker.js +54 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.js +64 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/path.js +17 -1
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +3 -1
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +3 -6
- package/node_modules/@openclaw/fs-safe/dist/permissions.d.ts +3 -10
- package/node_modules/@openclaw/fs-safe/dist/permissions.js +0 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-shared-route.d.ts +2 -8
- package/node_modules/@openclaw/fs-safe/dist/pinned-open.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-open.js +2 -3
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/read-opened-file.d.ts +2 -5
- package/node_modules/@openclaw/fs-safe/dist/regular-file.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +5 -17
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +36 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +4 -10
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +66 -38
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +7 -11
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +10 -2
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +21 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +123 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-mutation.d.ts +26 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-mutation.js +47 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +16 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +55 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file.d.ts +3 -55
- package/node_modules/@openclaw/fs-safe/dist/replace-file.js +31 -11
- package/node_modules/@openclaw/fs-safe/dist/retained-directory-replacement.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/retained-directory-replacement.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/retained-file-types.d.ts +50 -0
- package/node_modules/@openclaw/fs-safe/dist/retained-file-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/retained-file.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/retained-file.js +121 -0
- package/node_modules/@openclaw/fs-safe/dist/root-context.js +3 -2
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +28 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +9 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +88 -37
- package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.js +1 -6
- package/node_modules/@openclaw/fs-safe/dist/root-file.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/root-handle-context.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-handle-context.js +12 -0
- package/node_modules/@openclaw/fs-safe/dist/root-impl.d.ts +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +8 -5
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +2 -7
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -3
- package/node_modules/@openclaw/fs-safe/dist/root-path-observation.d.ts +4 -11
- package/node_modules/@openclaw/fs-safe/dist/root-path.js +7 -10
- package/node_modules/@openclaw/fs-safe/dist/root-paths.d.ts +2 -6
- package/node_modules/@openclaw/fs-safe/dist/root-remove-identity.d.ts +1 -3
- package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +19 -12
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +56 -20
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +1 -6
- package/node_modules/@openclaw/fs-safe/dist/root-write-complete-parent.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-write-complete-parent.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +8 -2
- package/node_modules/@openclaw/fs-safe/dist/secret-file.d.ts +6 -2
- package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -16
- package/node_modules/@openclaw/fs-safe/dist/secret-read-policy.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/secure-file-windows.js +5 -14
- package/node_modules/@openclaw/fs-safe/dist/secure-file.js +3 -2
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +2 -7
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission-parser.d.ts +1 -2
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-handle.d.ts +2 -8
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-policy.d.ts +2 -7
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +11 -12
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-stale-admission.d.ts +1 -5
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +9 -7
- package/node_modules/@openclaw/fs-safe/dist/staged-symlink-types.d.ts +39 -0
- package/node_modules/@openclaw/fs-safe/dist/staged-symlink-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/stat-observation.js +3 -10
- package/node_modules/@openclaw/fs-safe/dist/strict-file-identity.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/strict-file-identity.js +6 -6
- package/node_modules/@openclaw/fs-safe/dist/symlink-parents.js +58 -7
- package/node_modules/@openclaw/fs-safe/dist/temp-target.js +8 -10
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +28 -33
- 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 +16 -17
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +1 -2
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +4 -9
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.js +52 -0
- package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +5 -1
- package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/text-atomic.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/trash.js +27 -1
- package/node_modules/@openclaw/fs-safe/dist/walk.d.ts +2 -5
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +88 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +93 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +36 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +73 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +28 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +300 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +32 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-types.d.ts +60 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/watch.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/watch.js +530 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-owner.d.ts +3 -7
- package/node_modules/@openclaw/fs-safe/dist/windows-owner.js +0 -1
- package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +6 -6
- package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.ps1 +78 -3
- package/node_modules/@openclaw/fs-safe/dist/windows-security-command.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +66 -15
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +6 -2
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -2
- package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +93 -3
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +102 -4
- package/node_modules/@openclaw/fs-safe/docs/copy.md +2 -0
- package/node_modules/@openclaw/fs-safe/docs/durability.md +7 -0
- package/node_modules/@openclaw/fs-safe/docs/file-store.md +5 -0
- package/node_modules/@openclaw/fs-safe/docs/guest.md +7 -1
- package/node_modules/@openclaw/fs-safe/docs/index.md +1 -0
- package/node_modules/@openclaw/fs-safe/docs/install.md +30 -0
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +20 -4
- package/node_modules/@openclaw/fs-safe/docs/native.md +63 -1
- package/node_modules/@openclaw/fs-safe/docs/permissions.md +66 -0
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +30 -3
- package/node_modules/@openclaw/fs-safe/docs/retained-file.md +115 -0
- package/node_modules/@openclaw/fs-safe/docs/root.md +35 -9
- package/node_modules/@openclaw/fs-safe/docs/secret-file.md +19 -0
- package/node_modules/@openclaw/fs-safe/docs/security-model.md +4 -1
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +5 -1
- package/node_modules/@openclaw/fs-safe/docs/staged-symlink.md +124 -0
- package/node_modules/@openclaw/fs-safe/docs/store.md +3 -1
- package/node_modules/@openclaw/fs-safe/docs/temp.md +24 -4
- package/node_modules/@openclaw/fs-safe/docs/testing.md +210 -0
- package/node_modules/@openclaw/fs-safe/docs/types.md +6 -0
- package/node_modules/@openclaw/fs-safe/docs/walk.md +34 -1
- package/node_modules/@openclaw/fs-safe/docs/watch.md +251 -0
- package/node_modules/@openclaw/fs-safe/docs/writing.md +21 -0
- package/node_modules/@openclaw/fs-safe/package.json +13 -9
- 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/node_modules/p-limit/index.d.ts +2 -0
- package/node_modules/p-limit/index.js +17 -1
- package/node_modules/p-limit/package.json +1 -1
- package/node_modules/p-limit/readme.md +2 -0
- package/package.json +6 -6
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-owner.d.ts +0 -20
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-owner.js +0 -78
|
@@ -3,7 +3,7 @@ import syncFs, {} from "node:fs";
|
|
|
3
3
|
import fs, {} from "node:fs/promises";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
6
|
-
import { ownDirectoryMode } from "./directory-mode-
|
|
6
|
+
import { ownDirectoryMode } from "./directory-mode-node.js";
|
|
7
7
|
export async function syncDirectoryBestEffort(fsModule, dirPath) {
|
|
8
8
|
let handle;
|
|
9
9
|
try {
|
|
@@ -73,7 +73,10 @@ export async function pinDirectoryForMode(params) {
|
|
|
73
73
|
: await inspectFileIdentity(async () => assertDirectory(await handle.stat({ bigint: true }), params.dirPath), expected);
|
|
74
74
|
return Number(opened.mode & 4095n);
|
|
75
75
|
},
|
|
76
|
-
chmod: (mode) =>
|
|
76
|
+
chmod: (mode) => {
|
|
77
|
+
params.mutation?.assert();
|
|
78
|
+
return handle.chmod(mode);
|
|
79
|
+
},
|
|
77
80
|
close: () => handle.close(),
|
|
78
81
|
ignoreChmodError: params.ignoreChmodError,
|
|
79
82
|
});
|
|
@@ -103,6 +106,7 @@ export function applyDirectoryModeSync(params) {
|
|
|
103
106
|
try {
|
|
104
107
|
inspectFileIdentitySync(() => assertDirectory(params.fsModule.fstatSync(fd, { bigint: true }), params.dirPath), expected);
|
|
105
108
|
// chmod ignores file-type bits; mask so raw stat modes are tolerated.
|
|
109
|
+
params.mutation?.assert();
|
|
106
110
|
params.fchmodSync?.(fd, params.mode & 0o7777);
|
|
107
111
|
}
|
|
108
112
|
finally {
|
|
@@ -110,6 +114,7 @@ export function applyDirectoryModeSync(params) {
|
|
|
110
114
|
}
|
|
111
115
|
}
|
|
112
116
|
export async function writeTempFile(params) {
|
|
117
|
+
params.mutation?.assert();
|
|
113
118
|
const handle = await params.fsModule.open(params.tempPath, "wx", params.mode);
|
|
114
119
|
try {
|
|
115
120
|
// Custom adapters retain their async-only metadata contract.
|
|
@@ -117,6 +122,7 @@ export async function writeTempFile(params) {
|
|
|
117
122
|
? syncFs.fstatSync(handle.fd, { bigint: true }) : handle.stat({ bigint: true });
|
|
118
123
|
const identity = await inspectFileIdentity(inspect);
|
|
119
124
|
params.onIdentity?.(identity);
|
|
125
|
+
params.mutation?.assert();
|
|
120
126
|
await params.fsModule.writeFile(handle, params.content);
|
|
121
127
|
await handle.chmod(params.mode);
|
|
122
128
|
if (params.sync) {
|
|
@@ -136,10 +142,12 @@ export async function writeTempFile(params) {
|
|
|
136
142
|
}
|
|
137
143
|
}
|
|
138
144
|
export function writeTempFileSync(params) {
|
|
145
|
+
params.mutation?.assert();
|
|
139
146
|
const fd = params.fsModule.openSync(params.tempPath, "wx", params.mode);
|
|
140
147
|
try {
|
|
141
148
|
const identity = inspectFileIdentitySync(() => params.fsModule.fstatSync(fd, { bigint: true }));
|
|
142
149
|
params.onIdentity?.(identity);
|
|
150
|
+
params.mutation?.assert();
|
|
143
151
|
params.fsModule.writeFileSync(fd, params.content);
|
|
144
152
|
params.fchmodSync?.(fd, params.mode);
|
|
145
153
|
if (params.sync) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
|
+
import fs, { type FileHandle } from "node:fs/promises";
|
|
3
|
+
import type { AtomicMutation } from "./replace-file-mutation.js";
|
|
4
|
+
/** Borrow the writer's descriptor; never adopt an identity from a later pathname open. */
|
|
5
|
+
export declare function captureAtomicDestination(fsModule: Pick<typeof fs, "lstat">, handle: FileHandle, pathname: string, mutation: AtomicMutation, rejectHardlinks: boolean): Promise<{
|
|
6
|
+
verify: () => Promise<void>;
|
|
7
|
+
writing: () => void;
|
|
8
|
+
beforeWrite: () => Promise<void>;
|
|
9
|
+
beforeRestore: () => Promise<void>;
|
|
10
|
+
verifyRestore: () => Promise<void>;
|
|
11
|
+
assertBeforeMutation: () => void;
|
|
12
|
+
published: () => void;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function captureAtomicDestinationSync(fsModule: Pick<typeof fsSync, "lstatSync" | "fstatSync">, fd: number, pathname: string, mutation: AtomicMutation, rejectHardlinks: boolean): {
|
|
15
|
+
verify: () => void;
|
|
16
|
+
writing: () => void;
|
|
17
|
+
beforeWrite: () => void;
|
|
18
|
+
beforeRestore: () => void;
|
|
19
|
+
verifyRestore: () => void;
|
|
20
|
+
published: () => void;
|
|
21
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fsSync, {} from "node:fs";
|
|
2
|
+
import fs, {} from "node:fs/promises";
|
|
3
|
+
import { hasErrorCode } from "./file-cleanup.js";
|
|
4
|
+
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
6
|
+
function regular(stat, pathname, rejectHardlinks) {
|
|
7
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
8
|
+
throw new FsSafeError("path-mismatch", `Atomic replace destination changed: ${pathname}`);
|
|
9
|
+
}
|
|
10
|
+
if (rejectHardlinks && stat.nlink !== 1n) {
|
|
11
|
+
throw new FsSafeError("hardlink", `Hardlinked atomic replace destination not allowed: ${pathname}`);
|
|
12
|
+
}
|
|
13
|
+
return stat;
|
|
14
|
+
}
|
|
15
|
+
/** Borrow the writer's descriptor; never adopt an identity from a later pathname open. */
|
|
16
|
+
export async function captureAtomicDestination(fsModule, handle, pathname, mutation, rejectHardlinks) {
|
|
17
|
+
const inspect = () => fsModule === fs
|
|
18
|
+
? fsSync.fstatSync(handle.fd, { bigint: true }) : handle.stat({ bigint: true });
|
|
19
|
+
const identity = await inspectFileIdentity(inspect);
|
|
20
|
+
regular(identity, pathname, rejectHardlinks);
|
|
21
|
+
let writing = false;
|
|
22
|
+
let restoreDescriptorOnly = false;
|
|
23
|
+
const verifyDescriptor = async () => {
|
|
24
|
+
await inspectFileIdentity(async () => regular(await inspect(), pathname, rejectHardlinks), identity);
|
|
25
|
+
};
|
|
26
|
+
const verify = async () => {
|
|
27
|
+
let metadataFailure = false;
|
|
28
|
+
const observe = async (read) => {
|
|
29
|
+
try {
|
|
30
|
+
return await read();
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
metadataFailure = hasErrorCode(error, "EIO");
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
await inspectFileIdentity(async () => regular(await observe(inspect), pathname, rejectHardlinks), identity);
|
|
39
|
+
await inspectFileIdentity(async () => regular(await observe(() => fsModule.lstat(pathname, { bigint: true })), pathname, rejectHardlinks), identity);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (writing && metadataFailure) {
|
|
43
|
+
// A metadata observation failed, but the already-owned descriptor can
|
|
44
|
+
// still restore its bytes after fresh exact descriptor verification.
|
|
45
|
+
restoreDescriptorOnly = true;
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
mutation.refuse(error);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const verifyRestore = async () => {
|
|
52
|
+
if (!restoreDescriptorOnly)
|
|
53
|
+
return await verify();
|
|
54
|
+
try {
|
|
55
|
+
await verifyDescriptor();
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
mutation.refuse(error);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
verify,
|
|
63
|
+
writing: () => { writing = true; mutation.destination("writing", pathname, identity); },
|
|
64
|
+
beforeWrite: async () => {
|
|
65
|
+
mutation.assert();
|
|
66
|
+
await verify();
|
|
67
|
+
},
|
|
68
|
+
beforeRestore: async () => { mutation.assert(); await verifyRestore(); },
|
|
69
|
+
verifyRestore,
|
|
70
|
+
assertBeforeMutation: () => mutation.assert(),
|
|
71
|
+
published: () => mutation.destination("published", pathname, identity),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function captureAtomicDestinationSync(fsModule, fd, pathname, mutation, rejectHardlinks) {
|
|
75
|
+
const identity = inspectFileIdentitySync(() => fsModule.fstatSync(fd, { bigint: true }));
|
|
76
|
+
regular(identity, pathname, rejectHardlinks);
|
|
77
|
+
let writing = false;
|
|
78
|
+
let restoreDescriptorOnly = false;
|
|
79
|
+
const verify = () => {
|
|
80
|
+
let metadataFailure = false;
|
|
81
|
+
const observe = (read) => {
|
|
82
|
+
try {
|
|
83
|
+
return read();
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
metadataFailure = hasErrorCode(error, "EIO");
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
inspectFileIdentitySync(() => regular(observe(() => fsModule.fstatSync(fd, { bigint: true })), pathname, rejectHardlinks), identity);
|
|
92
|
+
inspectFileIdentitySync(() => regular(observe(() => fsModule.lstatSync(pathname, { bigint: true })), pathname, rejectHardlinks), identity);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (writing && metadataFailure) {
|
|
96
|
+
restoreDescriptorOnly = true;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
mutation.refuse(error);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const verifyRestore = () => {
|
|
103
|
+
if (!restoreDescriptorOnly)
|
|
104
|
+
return verify();
|
|
105
|
+
try {
|
|
106
|
+
inspectFileIdentitySync(() => regular(fsModule.fstatSync(fd, { bigint: true }), pathname, rejectHardlinks), identity);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
mutation.refuse(error);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
verify,
|
|
114
|
+
writing: () => { writing = true; mutation.destination("writing", pathname, identity); },
|
|
115
|
+
beforeWrite: () => {
|
|
116
|
+
mutation.assert();
|
|
117
|
+
verify();
|
|
118
|
+
},
|
|
119
|
+
beforeRestore: () => { mutation.assert(); verifyRestore(); },
|
|
120
|
+
verifyRestore,
|
|
121
|
+
published: () => mutation.destination("published", pathname, identity),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BigIntStats } from "node:fs";
|
|
2
|
+
export type ReplaceFileAtomicDestinationState = Readonly<{
|
|
3
|
+
state: "removed";
|
|
4
|
+
path: string;
|
|
5
|
+
}> | Readonly<{
|
|
6
|
+
state: "writing" | "published";
|
|
7
|
+
path: string;
|
|
8
|
+
dev: bigint;
|
|
9
|
+
ino: bigint;
|
|
10
|
+
}>;
|
|
11
|
+
export type AtomicMutationOptions = {
|
|
12
|
+
/** Recheck live authority before new effects; retained descriptor completion may settle. */
|
|
13
|
+
assertBeforeMutation?: () => void;
|
|
14
|
+
/** Retain observed destination facts even when later completion fails. */
|
|
15
|
+
onDestinationState?: (state: ReplaceFileAtomicDestinationState) => void;
|
|
16
|
+
};
|
|
17
|
+
export declare class AtomicMutation {
|
|
18
|
+
#private;
|
|
19
|
+
readonly active: boolean;
|
|
20
|
+
constructor(options: AtomicMutationOptions);
|
|
21
|
+
rethrowRefusal(): void;
|
|
22
|
+
refuse(error: unknown): never;
|
|
23
|
+
assert(): void;
|
|
24
|
+
destination(state: "writing" | "published", path: string, identity: BigIntStats): void;
|
|
25
|
+
removed(path: string): void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { assertSynchronousCallbackResult } from "./mutation-authority.js";
|
|
2
|
+
export class AtomicMutation {
|
|
3
|
+
active;
|
|
4
|
+
#assertion;
|
|
5
|
+
#observer;
|
|
6
|
+
#refusal;
|
|
7
|
+
#writingReported = false;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.#assertion = options.assertBeforeMutation;
|
|
10
|
+
this.#observer = options.onDestinationState;
|
|
11
|
+
this.active = Boolean(this.#assertion || this.#observer);
|
|
12
|
+
}
|
|
13
|
+
rethrowRefusal() {
|
|
14
|
+
if (this.#refusal)
|
|
15
|
+
throw this.#refusal.error;
|
|
16
|
+
}
|
|
17
|
+
refuse(error) {
|
|
18
|
+
this.#refusal ??= { error };
|
|
19
|
+
throw this.#refusal.error;
|
|
20
|
+
}
|
|
21
|
+
#invoke(operation, name) {
|
|
22
|
+
this.rethrowRefusal();
|
|
23
|
+
try {
|
|
24
|
+
const result = operation();
|
|
25
|
+
assertSynchronousCallbackResult(result, name);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
this.refuse(error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
assert() {
|
|
32
|
+
this.#invoke(() => this.#assertion?.(), "assertBeforeMutation");
|
|
33
|
+
}
|
|
34
|
+
destination(state, path, identity) {
|
|
35
|
+
if (state === "writing") {
|
|
36
|
+
if (this.#writingReported)
|
|
37
|
+
return;
|
|
38
|
+
this.#writingReported = true;
|
|
39
|
+
}
|
|
40
|
+
this.#invoke(() => this.#observer?.(Object.freeze({
|
|
41
|
+
state, path, dev: identity.dev, ino: identity.ino,
|
|
42
|
+
})), "onDestinationState");
|
|
43
|
+
}
|
|
44
|
+
removed(path) {
|
|
45
|
+
this.#invoke(() => this.#observer?.(Object.freeze({ state: "removed", path })), "onDestinationState");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -26,7 +26,7 @@ export declare class AsyncAtomicTempOwner extends AtomicTempOwner<FileHandle> {
|
|
|
26
26
|
identity: BigIntStats;
|
|
27
27
|
}): void;
|
|
28
28
|
assertCurrent(fsModule: AsyncOwnerFileSystem, pathname?: string): Promise<void>;
|
|
29
|
-
assertPublished(fsModule: AsyncOwnerFileSystem, pathname: string, expectedHash?: string): Promise<void>;
|
|
29
|
+
assertPublished(fsModule: AsyncOwnerFileSystem, pathname: string, expectedHash?: string, onVerified?: (identity: BigIntStats) => void): Promise<void>;
|
|
30
30
|
finish(params: {
|
|
31
31
|
fsModule: AsyncOwnerFileSystem;
|
|
32
32
|
originalFailure?: AtomicTempFailure;
|
|
@@ -39,7 +39,7 @@ export declare class SyncAtomicTempOwner extends AtomicTempOwner<number> {
|
|
|
39
39
|
identity: BigIntStats;
|
|
40
40
|
}): void;
|
|
41
41
|
assertCurrent(fsModule: SyncOwnerFileSystem, pathname?: string): void;
|
|
42
|
-
assertPublished(fsModule: SyncOwnerFileSystem, pathname: string, expectedHash?: string): void;
|
|
42
|
+
assertPublished(fsModule: SyncOwnerFileSystem, pathname: string, expectedHash?: string, onVerified?: (identity: BigIntStats) => void): void;
|
|
43
43
|
finish(params: {
|
|
44
44
|
fsModule: SyncOwnerFileSystem;
|
|
45
45
|
originalFailure?: AtomicTempFailure;
|
|
@@ -161,16 +161,21 @@ export class AsyncAtomicTempOwner extends AtomicTempOwner {
|
|
|
161
161
|
throw error;
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
-
async assertPublished(fsModule, pathname, expectedHash) {
|
|
164
|
+
async assertPublished(fsModule, pathname, expectedHash, onVerified) {
|
|
165
|
+
let identityCurrent = false;
|
|
165
166
|
try {
|
|
166
167
|
await this.assertCurrent(fsModule, pathname);
|
|
167
|
-
|
|
168
|
+
identityCurrent = true;
|
|
168
169
|
}
|
|
169
170
|
catch (error) {
|
|
170
171
|
if (!(error instanceof FsSafeError) || !hasErrorCode(error, "path-mismatch") || !expectedHash) {
|
|
171
172
|
throw error;
|
|
172
173
|
}
|
|
173
174
|
}
|
|
175
|
+
if (identityCurrent) {
|
|
176
|
+
onVerified?.(this.identity);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
174
179
|
let published;
|
|
175
180
|
try {
|
|
176
181
|
try {
|
|
@@ -199,6 +204,7 @@ export class AsyncAtomicTempOwner extends AtomicTempOwner {
|
|
|
199
204
|
if (sha256Hex(await published.readFile()) !== expectedHash) {
|
|
200
205
|
throw new FsSafeError("path-mismatch", `Atomic replace published content changed: ${pathname}`);
|
|
201
206
|
}
|
|
207
|
+
onVerified?.(identity);
|
|
202
208
|
const previousHandle = this.takeResource();
|
|
203
209
|
await previousHandle?.close();
|
|
204
210
|
this.resource = published;
|
|
@@ -269,16 +275,21 @@ export class SyncAtomicTempOwner extends AtomicTempOwner {
|
|
|
269
275
|
throw error;
|
|
270
276
|
}
|
|
271
277
|
}
|
|
272
|
-
assertPublished(fsModule, pathname, expectedHash) {
|
|
278
|
+
assertPublished(fsModule, pathname, expectedHash, onVerified) {
|
|
279
|
+
let identityCurrent = false;
|
|
273
280
|
try {
|
|
274
281
|
this.assertCurrent(fsModule, pathname);
|
|
275
|
-
|
|
282
|
+
identityCurrent = true;
|
|
276
283
|
}
|
|
277
284
|
catch (error) {
|
|
278
285
|
if (!(error instanceof FsSafeError) || !hasErrorCode(error, "path-mismatch") || !expectedHash) {
|
|
279
286
|
throw error;
|
|
280
287
|
}
|
|
281
288
|
}
|
|
289
|
+
if (identityCurrent) {
|
|
290
|
+
onVerified?.(this.identity);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
282
293
|
let publishedFd;
|
|
283
294
|
try {
|
|
284
295
|
try {
|
|
@@ -305,6 +316,7 @@ export class SyncAtomicTempOwner extends AtomicTempOwner {
|
|
|
305
316
|
if (sha256Hex(fsModule.readFileSync(publishedFd)) !== expectedHash) {
|
|
306
317
|
throw new FsSafeError("path-mismatch", `Atomic replace published content changed: ${pathname}`);
|
|
307
318
|
}
|
|
319
|
+
onVerified?.(identity);
|
|
308
320
|
const previousFd = this.takeResource();
|
|
309
321
|
fsModule.closeSync(previousFd);
|
|
310
322
|
this.resource = publishedFd;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type syncFs from "node:fs";
|
|
2
|
+
import type fs from "node:fs/promises";
|
|
3
|
+
import type { RenameIdentityPolicy } from "./pinned-write-types.js";
|
|
4
|
+
import type { AtomicMutationOptions } from "./replace-file-mutation.js";
|
|
5
|
+
import type { ReplaceFileCopyFallbackRestorePolicy, ReplaceFileDestinationHardlinkPolicy } from "./replace-file-copy-fallback.js";
|
|
6
|
+
export type ReplaceFileAtomicFileSystem = {
|
|
7
|
+
promises: Pick<typeof fs, "mkdir" | "writeFile" | "rename" | "copyFile" | "unlink" | "rm" | "open" | "stat" | "lstat"> & {
|
|
8
|
+
/** @deprecated Accepted for adapter compatibility but never called. */
|
|
9
|
+
chmod?: typeof fs.chmod;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export type ReplaceFileAtomicSyncFileSystem = Pick<typeof syncFs, "mkdirSync" | "readFileSync" | "writeFileSync" | "renameSync" | "copyFileSync" | "unlinkSync" | "rmSync" | "openSync" | "fsyncSync" | "closeSync" | "fstatSync" | "statSync" | "lstatSync" | "ftruncateSync" | "readSync" | "writeSync"> & {
|
|
13
|
+
/** @deprecated Accepted for adapter compatibility but never called. */
|
|
14
|
+
chmodSync?: typeof syncFs.chmodSync;
|
|
15
|
+
fchmodSync?: typeof syncFs.fchmodSync;
|
|
16
|
+
};
|
|
17
|
+
export type ReplaceFileAtomicBaseOptions = AtomicMutationOptions & {
|
|
18
|
+
filePath: string;
|
|
19
|
+
content: string | Uint8Array;
|
|
20
|
+
dirMode?: number;
|
|
21
|
+
mode?: number;
|
|
22
|
+
/** Inherit only rwx bits from an existing non-symlink regular file. */
|
|
23
|
+
preserveExistingMode?: boolean;
|
|
24
|
+
tempPrefix?: string;
|
|
25
|
+
renameMaxRetries?: number;
|
|
26
|
+
renameRetryBaseDelayMs?: number;
|
|
27
|
+
copyFallbackOnPermissionError?: boolean;
|
|
28
|
+
copyFallbackRestore?: ReplaceFileCopyFallbackRestorePolicy;
|
|
29
|
+
maxRestoreBytes?: number;
|
|
30
|
+
destinationHardlinks?: ReplaceFileDestinationHardlinkPolicy;
|
|
31
|
+
/** Strict by default; locked content verification is an explicit FUSE compatibility policy. */
|
|
32
|
+
renameIdentity?: RenameIdentityPolicy;
|
|
33
|
+
syncTempFile?: boolean;
|
|
34
|
+
syncParentDir?: boolean;
|
|
35
|
+
throwOnCleanupError?: boolean;
|
|
36
|
+
};
|
|
37
|
+
export type ReplaceFileAtomicOptions = ReplaceFileAtomicBaseOptions & {
|
|
38
|
+
fileSystem?: ReplaceFileAtomicFileSystem;
|
|
39
|
+
/** Runs while the exact staged file is retained; replacing or hardlinking it is rejected. */
|
|
40
|
+
beforeRename?: (params: {
|
|
41
|
+
filePath: string;
|
|
42
|
+
tempPath: string;
|
|
43
|
+
}) => Promise<void>;
|
|
44
|
+
};
|
|
45
|
+
export type ReplaceFileAtomicSyncOptions = ReplaceFileAtomicBaseOptions & {
|
|
46
|
+
fileSystem?: ReplaceFileAtomicSyncFileSystem;
|
|
47
|
+
/** Runs while the exact staged file is retained; replacing or hardlinking it is rejected. */
|
|
48
|
+
beforeRename?: (params: {
|
|
49
|
+
filePath: string;
|
|
50
|
+
tempPath: string;
|
|
51
|
+
}) => void;
|
|
52
|
+
};
|
|
53
|
+
export type ReplaceFileAtomicResult = {
|
|
54
|
+
method: "rename" | "copy-fallback";
|
|
55
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,58 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { type ReplaceFileCopyFallbackRestorePolicy, type ReplaceFileDestinationHardlinkPolicy } from "./replace-file-copy-fallback.js";
|
|
5
|
-
export type ReplaceFileAtomicFileSystem = {
|
|
6
|
-
promises: Pick<typeof fs, "mkdir" | "writeFile" | "rename" | "copyFile" | "unlink" | "rm" | "open" | "stat" | "lstat"> & {
|
|
7
|
-
/** @deprecated Accepted for adapter compatibility but never called. */
|
|
8
|
-
chmod?: typeof fs.chmod;
|
|
9
|
-
};
|
|
10
|
-
};
|
|
11
|
-
export type ReplaceFileAtomicSyncFileSystem = Pick<typeof syncFs, "mkdirSync" | "readFileSync" | "writeFileSync" | "renameSync" | "copyFileSync" | "unlinkSync" | "rmSync" | "openSync" | "fsyncSync" | "closeSync" | "fstatSync" | "statSync" | "lstatSync" | "ftruncateSync" | "readSync" | "writeSync"> & {
|
|
12
|
-
/** @deprecated Accepted for adapter compatibility but never called. */
|
|
13
|
-
chmodSync?: typeof syncFs.chmodSync;
|
|
14
|
-
fchmodSync?: typeof syncFs.fchmodSync;
|
|
15
|
-
};
|
|
16
|
-
type ReplaceFileAtomicBaseOptions = {
|
|
17
|
-
filePath: string;
|
|
18
|
-
content: string | Uint8Array;
|
|
19
|
-
dirMode?: number;
|
|
20
|
-
mode?: number;
|
|
21
|
-
/** Inherit only rwx bits from an existing non-symlink regular file. */
|
|
22
|
-
preserveExistingMode?: boolean;
|
|
23
|
-
tempPrefix?: string;
|
|
24
|
-
renameMaxRetries?: number;
|
|
25
|
-
renameRetryBaseDelayMs?: number;
|
|
26
|
-
copyFallbackOnPermissionError?: boolean;
|
|
27
|
-
copyFallbackRestore?: ReplaceFileCopyFallbackRestorePolicy;
|
|
28
|
-
maxRestoreBytes?: number;
|
|
29
|
-
destinationHardlinks?: ReplaceFileDestinationHardlinkPolicy;
|
|
30
|
-
/** Strict by default; locked content verification is an explicit FUSE compatibility policy. */
|
|
31
|
-
renameIdentity?: RenameIdentityPolicy;
|
|
32
|
-
syncTempFile?: boolean;
|
|
33
|
-
syncParentDir?: boolean;
|
|
34
|
-
throwOnCleanupError?: boolean;
|
|
35
|
-
};
|
|
36
|
-
export type ReplaceFileAtomicOptions = ReplaceFileAtomicBaseOptions & {
|
|
37
|
-
fileSystem?: ReplaceFileAtomicFileSystem;
|
|
38
|
-
/** Runs while the exact staged file is retained; replacing or hardlinking it is rejected. */
|
|
39
|
-
beforeRename?: (params: {
|
|
40
|
-
filePath: string;
|
|
41
|
-
tempPath: string;
|
|
42
|
-
}) => Promise<void>;
|
|
43
|
-
};
|
|
44
|
-
export type ReplaceFileAtomicSyncOptions = ReplaceFileAtomicBaseOptions & {
|
|
45
|
-
fileSystem?: ReplaceFileAtomicSyncFileSystem;
|
|
46
|
-
/** Runs while the exact staged file is retained; replacing or hardlinking it is rejected. */
|
|
47
|
-
beforeRename?: (params: {
|
|
48
|
-
filePath: string;
|
|
49
|
-
tempPath: string;
|
|
50
|
-
}) => void;
|
|
51
|
-
};
|
|
52
|
-
export type ReplaceFileAtomicResult = {
|
|
53
|
-
method: "rename" | "copy-fallback";
|
|
54
|
-
};
|
|
1
|
+
import type { ReplaceFileAtomicOptions, ReplaceFileAtomicSyncOptions, ReplaceFileAtomicResult } from "./replace-file-types.js";
|
|
2
|
+
export type { ReplaceFileAtomicDestinationState } from "./replace-file-mutation.js";
|
|
3
|
+
export type { ReplaceFileAtomicFileSystem, ReplaceFileAtomicSyncFileSystem, ReplaceFileAtomicOptions, ReplaceFileAtomicSyncOptions, ReplaceFileAtomicResult, } from "./replace-file-types.js";
|
|
55
4
|
export declare function replaceFileAtomic(options: ReplaceFileAtomicOptions): Promise<ReplaceFileAtomicResult>;
|
|
56
5
|
export declare function replaceFileAtomicWithDirectorySync(options: ReplaceFileAtomicOptions, syncParent?: (directoryPath: string) => Promise<unknown>): Promise<ReplaceFileAtomicResult>;
|
|
57
6
|
export declare function replaceFileAtomicSync(options: ReplaceFileAtomicSyncOptions): ReplaceFileAtomicResult;
|
|
58
|
-
export {};
|
|
@@ -13,15 +13,19 @@ import { admitStandalonePublicationPath } from "./windows-path-alias.js";
|
|
|
13
13
|
import { sleep, sleepSync } from "./timing.js";
|
|
14
14
|
import { serializePathWrite } from "./write-queue.js";
|
|
15
15
|
import { hasErrorCode, readErrorCode } from "./file-cleanup.js";
|
|
16
|
+
import { AtomicMutation } from "./replace-file-mutation.js";
|
|
17
|
+
import { assertSynchronousCallbackResult } from "./mutation-authority.js";
|
|
16
18
|
async function renameWithRetry(params) {
|
|
17
19
|
for (let attempt = 0; attempt <= params.maxRetries; attempt++) {
|
|
18
20
|
if (attempt > 0)
|
|
19
21
|
await params.assertSourceCurrent();
|
|
20
22
|
try {
|
|
23
|
+
params.mutation.assert();
|
|
21
24
|
await params.fsModule.rename(params.src, params.dest);
|
|
22
25
|
return { method: "rename" };
|
|
23
26
|
}
|
|
24
27
|
catch (error) {
|
|
28
|
+
params.mutation.rethrowRefusal();
|
|
25
29
|
const code = readErrorCode(error);
|
|
26
30
|
if (code === "EBUSY" && attempt < params.maxRetries) {
|
|
27
31
|
await sleep(params.baseDelayMs * 2 ** attempt);
|
|
@@ -37,6 +41,7 @@ async function renameWithRetry(params) {
|
|
|
37
41
|
maxRestoreBytes: params.maxRestoreBytes,
|
|
38
42
|
expectedSourceIdentity: params.sourceIdentity,
|
|
39
43
|
sync: params.syncFallback,
|
|
44
|
+
mutation: params.mutation,
|
|
40
45
|
});
|
|
41
46
|
return { method: "copy-fallback" };
|
|
42
47
|
}
|
|
@@ -50,10 +55,12 @@ function renameWithRetrySync(params) {
|
|
|
50
55
|
if (attempt > 0)
|
|
51
56
|
params.assertSourceCurrent();
|
|
52
57
|
try {
|
|
58
|
+
params.mutation.assert();
|
|
53
59
|
params.fsModule.renameSync(params.src, params.dest);
|
|
54
60
|
return { method: "rename" };
|
|
55
61
|
}
|
|
56
62
|
catch (error) {
|
|
63
|
+
params.mutation.rethrowRefusal();
|
|
57
64
|
const code = readErrorCode(error);
|
|
58
65
|
if (code === "EBUSY" && attempt < params.maxRetries) {
|
|
59
66
|
sleepSync(params.baseDelayMs * 2 ** attempt);
|
|
@@ -70,6 +77,7 @@ function renameWithRetrySync(params) {
|
|
|
70
77
|
expectedSourceIdentity: params.sourceIdentity,
|
|
71
78
|
fchmodSync: params.fchmodSync,
|
|
72
79
|
sync: params.syncFallback,
|
|
80
|
+
mutation: params.mutation,
|
|
73
81
|
});
|
|
74
82
|
return { method: "copy-fallback" };
|
|
75
83
|
}
|
|
@@ -142,24 +150,27 @@ export async function replaceFileAtomic(options) {
|
|
|
142
150
|
}
|
|
143
151
|
// Internal owner hook: keep directory durability inside publication verification and serialization.
|
|
144
152
|
export async function replaceFileAtomicWithDirectorySync(options, syncParent) {
|
|
153
|
+
const mutation = new AtomicMutation(options);
|
|
145
154
|
const filePath = validateReplaceFilePath(options.filePath);
|
|
146
155
|
validateRestoreOptions(options);
|
|
147
156
|
const renameIdentity = options.renameIdentity;
|
|
148
157
|
validateRenameIdentity(renameIdentity);
|
|
149
158
|
return await serializePathWrite(path.resolve(filePath), async () => {
|
|
150
159
|
if (renameIdentity !== "verify-content-with-lock") {
|
|
151
|
-
return await replaceFileAtomicUnserialized(options, filePath, renameIdentity, syncParent);
|
|
160
|
+
return await replaceFileAtomicUnserialized(options, filePath, renameIdentity, mutation, syncParent);
|
|
152
161
|
}
|
|
153
162
|
const fsModule = options.fileSystem?.promises ?? fs;
|
|
154
163
|
const dir = path.dirname(filePath);
|
|
164
|
+
mutation.assert();
|
|
155
165
|
await fsModule.mkdir(fsModule === fs ? recursiveMkdirPath(dir) : dir, {
|
|
156
166
|
recursive: true,
|
|
157
167
|
mode: options.dirMode ?? 0o700,
|
|
158
168
|
});
|
|
159
|
-
|
|
169
|
+
mutation.assert();
|
|
170
|
+
return await withAtomicRenameIdentityLock(filePath, async () => await replaceFileAtomicUnserialized(options, filePath, renameIdentity, mutation, syncParent));
|
|
160
171
|
});
|
|
161
172
|
}
|
|
162
|
-
async function replaceFileAtomicUnserialized(options, filePath, renameIdentity, syncParent) {
|
|
173
|
+
async function replaceFileAtomicUnserialized(options, filePath, renameIdentity, mutation, syncParent) {
|
|
163
174
|
const fsModule = options.fileSystem?.promises ?? fs;
|
|
164
175
|
const dir = path.dirname(filePath);
|
|
165
176
|
const dirMode = options.dirMode ?? 0o700;
|
|
@@ -169,8 +180,9 @@ async function replaceFileAtomicUnserialized(options, filePath, renameIdentity,
|
|
|
169
180
|
const tempOwner = new AsyncAtomicTempOwner(tempPath);
|
|
170
181
|
let originalFailure;
|
|
171
182
|
try {
|
|
183
|
+
mutation.assert();
|
|
172
184
|
await fsModule.mkdir(fsModule === fs ? recursiveMkdirPath(dir) : dir, { recursive: true, mode: dirMode });
|
|
173
|
-
await applyDirectoryMode({ fsModule, dirPath: dir, mode: dirMode });
|
|
185
|
+
await applyDirectoryMode({ fsModule, dirPath: dir, mode: dirMode, mutation });
|
|
174
186
|
tempOwner.start();
|
|
175
187
|
tempOwner.adopt(await writeTempFile({
|
|
176
188
|
fsModule,
|
|
@@ -179,6 +191,7 @@ async function replaceFileAtomicUnserialized(options, filePath, renameIdentity,
|
|
|
179
191
|
mode,
|
|
180
192
|
sync: options.syncTempFile === true,
|
|
181
193
|
onIdentity: tempOwner.onIdentity,
|
|
194
|
+
mutation,
|
|
182
195
|
}));
|
|
183
196
|
await tempOwner.assertCurrent(fsModule);
|
|
184
197
|
if (options.beforeRename) {
|
|
@@ -202,10 +215,11 @@ async function replaceFileAtomicUnserialized(options, filePath, renameIdentity,
|
|
|
202
215
|
sourceIdentity: tempOwner.identity,
|
|
203
216
|
assertSourceCurrent: () => tempOwner.assertCurrent(fsModule),
|
|
204
217
|
syncFallback: options.syncTempFile === true,
|
|
218
|
+
mutation,
|
|
205
219
|
});
|
|
206
220
|
if (result.method === "rename") {
|
|
207
221
|
tempOwner.markRenamed();
|
|
208
|
-
await tempOwner.assertPublished(fsModule, filePath, expectedHash);
|
|
222
|
+
await tempOwner.assertPublished(fsModule, filePath, expectedHash, identity => mutation.destination("published", filePath, identity));
|
|
209
223
|
}
|
|
210
224
|
else {
|
|
211
225
|
await tempOwner.assertCurrent(fsModule);
|
|
@@ -237,22 +251,25 @@ async function replaceFileAtomicUnserialized(options, filePath, renameIdentity,
|
|
|
237
251
|
}
|
|
238
252
|
}
|
|
239
253
|
export function replaceFileAtomicSync(options) {
|
|
254
|
+
const mutation = new AtomicMutation(options);
|
|
240
255
|
const filePath = validateReplaceFilePath(options.filePath);
|
|
241
256
|
validateRestoreOptions(options);
|
|
242
257
|
const renameIdentity = options.renameIdentity;
|
|
243
258
|
validateRenameIdentity(renameIdentity);
|
|
244
259
|
if (renameIdentity !== "verify-content-with-lock") {
|
|
245
|
-
return replaceFileAtomicSyncUnserialized(options, filePath, renameIdentity);
|
|
260
|
+
return replaceFileAtomicSyncUnserialized(options, filePath, renameIdentity, mutation);
|
|
246
261
|
}
|
|
247
262
|
const fsModule = options.fileSystem ?? syncFs;
|
|
248
263
|
const dir = path.dirname(filePath);
|
|
264
|
+
mutation.assert();
|
|
249
265
|
fsModule.mkdirSync(fsModule === syncFs ? recursiveMkdirPath(dir) : dir, {
|
|
250
266
|
recursive: true,
|
|
251
267
|
mode: options.dirMode ?? 0o700,
|
|
252
268
|
});
|
|
253
|
-
|
|
269
|
+
mutation.assert();
|
|
270
|
+
return withAtomicRenameIdentityLockSync(filePath, () => replaceFileAtomicSyncUnserialized(options, filePath, renameIdentity, mutation));
|
|
254
271
|
}
|
|
255
|
-
function replaceFileAtomicSyncUnserialized(options, filePath, renameIdentity) {
|
|
272
|
+
function replaceFileAtomicSyncUnserialized(options, filePath, renameIdentity, mutation) {
|
|
256
273
|
const fsModule = options.fileSystem ?? syncFs;
|
|
257
274
|
const dir = path.dirname(filePath);
|
|
258
275
|
const dirMode = options.dirMode ?? 0o700;
|
|
@@ -269,8 +286,9 @@ function replaceFileAtomicSyncUnserialized(options, filePath, renameIdentity) {
|
|
|
269
286
|
const tempOwner = new SyncAtomicTempOwner(tempPath);
|
|
270
287
|
let originalFailure;
|
|
271
288
|
try {
|
|
289
|
+
mutation.assert();
|
|
272
290
|
fsModule.mkdirSync(fsModule === syncFs ? recursiveMkdirPath(dir) : dir, { recursive: true, mode: dirMode });
|
|
273
|
-
applyDirectoryModeSync({ fsModule, dirPath: dir, mode: dirMode, fchmodSync });
|
|
291
|
+
applyDirectoryModeSync({ fsModule, dirPath: dir, mode: dirMode, fchmodSync, mutation });
|
|
274
292
|
tempOwner.start();
|
|
275
293
|
tempOwner.adopt(writeTempFileSync({
|
|
276
294
|
fsModule,
|
|
@@ -280,10 +298,11 @@ function replaceFileAtomicSyncUnserialized(options, filePath, renameIdentity) {
|
|
|
280
298
|
fchmodSync,
|
|
281
299
|
sync: options.syncTempFile === true,
|
|
282
300
|
onIdentity: tempOwner.onIdentity,
|
|
301
|
+
mutation,
|
|
283
302
|
}));
|
|
284
303
|
tempOwner.assertCurrent(fsModule);
|
|
285
304
|
if (options.beforeRename) {
|
|
286
|
-
options.beforeRename({ filePath, tempPath });
|
|
305
|
+
assertSynchronousCallbackResult(options.beforeRename({ filePath, tempPath }), "beforeRename");
|
|
287
306
|
tempOwner.assertCurrent(fsModule);
|
|
288
307
|
}
|
|
289
308
|
if (options.destinationHardlinks === "reject") {
|
|
@@ -304,10 +323,11 @@ function replaceFileAtomicSyncUnserialized(options, filePath, renameIdentity) {
|
|
|
304
323
|
assertSourceCurrent: () => tempOwner.assertCurrent(fsModule),
|
|
305
324
|
fchmodSync,
|
|
306
325
|
syncFallback: options.syncTempFile === true,
|
|
326
|
+
mutation,
|
|
307
327
|
});
|
|
308
328
|
if (result.method === "rename") {
|
|
309
329
|
tempOwner.markRenamed();
|
|
310
|
-
tempOwner.assertPublished(fsModule, filePath, expectedHash);
|
|
330
|
+
tempOwner.assertPublished(fsModule, filePath, expectedHash, identity => mutation.destination("published", filePath, identity));
|
|
311
331
|
}
|
|
312
332
|
else {
|
|
313
333
|
tempOwner.assertCurrent(fsModule);
|
|
@@ -6,6 +6,7 @@ export type RetainedReplacementParent = Readonly<{
|
|
|
6
6
|
realPath: string;
|
|
7
7
|
identity: Pick<BigIntStats, "dev" | "ino">;
|
|
8
8
|
}>;
|
|
9
|
+
export declare function copyOperationalCode(target: Error, source: unknown): void;
|
|
9
10
|
export declare class RetainedDirectoryReplacement {
|
|
10
11
|
#private;
|
|
11
12
|
private constructor();
|