@openclaw/fs-safe 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/README.md +7 -0
- package/dist/archive-deadline.d.ts +3 -0
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +44 -8
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +1 -0
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +3 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +26 -16
- package/dist/archive-limits.d.ts +11 -3
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +24 -0
- package/dist/archive-native.d.ts +3 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +24 -9
- package/dist/archive-policy.d.ts +2 -0
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +9 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +69 -41
- package/dist/archive-staging.d.ts +3 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +91 -43
- package/dist/archive-tar-admission.d.ts +7 -0
- package/dist/archive-tar-admission.d.ts.map +1 -0
- package/dist/archive-tar-admission.js +43 -0
- package/dist/archive-tar-gnu.d.ts +2 -0
- package/dist/archive-tar-gnu.d.ts.map +1 -0
- package/dist/archive-tar-gnu.js +20 -0
- package/dist/archive-tar-header.d.ts +8 -0
- package/dist/archive-tar-header.d.ts.map +1 -0
- package/dist/archive-tar-header.js +47 -0
- package/dist/archive-tar-meta.d.ts +29 -1
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +104 -35
- package/dist/archive-tar-runtime.d.ts +3 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +3 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +8 -2
- package/dist/archive-zip-names.js +2 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +38 -26
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +2 -4
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +3 -10
- package/dist/byte-budget.d.ts +5 -0
- package/dist/byte-budget.d.ts.map +1 -0
- package/dist/byte-budget.js +9 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +10 -5
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +9 -5
- package/dist/file-store-limit.d.ts +2 -0
- package/dist/file-store-limit.d.ts.map +1 -0
- package/dist/file-store-limit.js +8 -0
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +38 -8
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +26 -26
- package/dist/json-durable-queue-directory.d.ts +2 -0
- package/dist/json-durable-queue-directory.d.ts.map +1 -0
- package/dist/json-durable-queue-directory.js +20 -0
- package/dist/json-durable-queue-ownership.d.ts +14 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -0
- package/dist/json-durable-queue-ownership.js +168 -0
- package/dist/json-durable-queue-retirement.d.ts +9 -0
- package/dist/json-durable-queue-retirement.d.ts.map +1 -0
- package/dist/json-durable-queue-retirement.js +126 -0
- package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
- package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
- package/dist/json-durable-queue-transfer-lock.js +19 -0
- package/dist/json-durable-queue.d.ts +1 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +90 -57
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +27 -8
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -2
- package/dist/native-binding.d.ts +12 -3
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +11 -110
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +4 -2
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +27 -48
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +8 -9
- package/dist/private-temp-workspace.d.ts +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +81 -56
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -4
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +6 -4
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +136 -82
- package/dist/replace-file-copy-fallback.d.ts +3 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +21 -36
- package/dist/replace-file-copy-source.d.ts +21 -0
- package/dist/replace-file-copy-source.d.ts.map +1 -0
- package/dist/replace-file-copy-source.js +112 -0
- package/dist/replace-file-descriptor.d.ts +13 -3
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +32 -7
- package/dist/replace-file-rename-policy.d.ts +7 -0
- package/dist/replace-file-rename-policy.d.ts.map +1 -0
- package/dist/replace-file-rename-policy.js +30 -0
- package/dist/replace-file-temp-owner.d.ts +46 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -0
- package/dist/replace-file-temp-owner.js +346 -0
- package/dist/replace-file.d.ts +6 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +72 -58
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +40 -26
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +6 -3
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +6 -5
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +17 -13
- package/dist/sibling-staged-file.d.ts +15 -0
- package/dist/sibling-staged-file.d.ts.map +1 -0
- package/dist/sibling-staged-file.js +148 -0
- package/dist/sibling-temp.d.ts +3 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +30 -74
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +48 -27
- package/dist/sidecar-lock-handle.d.ts +6 -2
- package/dist/sidecar-lock-handle.d.ts.map +1 -1
- package/dist/sidecar-lock-handle.js +17 -3
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +29 -7
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +28 -16
- package/dist/suppressed-error.d.ts +6 -0
- package/dist/suppressed-error.d.ts.map +1 -0
- package/dist/suppressed-error.js +15 -0
- package/dist/temp-cleanup.d.ts +2 -0
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +25 -10
- package/dist/temp-workspace-owner.d.ts +23 -0
- package/dist/temp-workspace-owner.d.ts.map +1 -0
- package/dist/temp-workspace-owner.js +320 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +5 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/docs/advanced.md +8 -1
- package/docs/archive.md +194 -26
- package/docs/atomic.md +11 -2
- package/docs/durability.md +1 -1
- package/docs/file-store.md +2 -0
- package/docs/install.md +8 -4
- package/docs/json.md +8 -4
- package/docs/native-helper.md +15 -3
- package/docs/native.md +33 -6
- package/docs/output.md +10 -0
- package/docs/public-api.md +6 -3
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +1 -1
- package/docs/regular-file.md +9 -2
- package/docs/root.md +2 -0
- package/docs/secret-file.md +5 -1
- package/docs/secure-file.md +4 -2
- package/docs/security-model.md +18 -0
- package/docs/sidecar-lock.md +4 -0
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- package/package.json +10 -10
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import syncFs, {} from "node:fs";
|
|
2
|
+
import fs, {} from "node:fs/promises";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { sameFileIdentityForCleanup, sha256Hex } from "./file-identity.js";
|
|
5
|
+
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
6
|
+
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
7
|
+
const PUBLISHED_READ_FLAGS = syncFs.constants.O_RDONLY |
|
|
8
|
+
(process.platform !== "win32" && typeof syncFs.constants.O_NOFOLLOW === "number"
|
|
9
|
+
? syncFs.constants.O_NOFOLLOW
|
|
10
|
+
: 0) |
|
|
11
|
+
(process.platform !== "win32" && typeof syncFs.constants.O_NONBLOCK === "number"
|
|
12
|
+
? syncFs.constants.O_NONBLOCK
|
|
13
|
+
: 0);
|
|
14
|
+
function assertOwnedFile(stat, pathname, pathnameEntry) {
|
|
15
|
+
if (stat.isSymbolicLink()) {
|
|
16
|
+
throw new FsSafeError("symlink", `Atomic replace owned file became a symlink: ${pathname}`);
|
|
17
|
+
}
|
|
18
|
+
if (!stat.isFile()) {
|
|
19
|
+
throw new FsSafeError("not-file", `Atomic replace owned file must remain regular: ${pathname}`);
|
|
20
|
+
}
|
|
21
|
+
if (stat.nlink > 1n || (pathnameEntry && stat.nlink !== 1n)) {
|
|
22
|
+
throw new FsSafeError("hardlink", `Atomic replace owned file must retain one link: ${pathname}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function missingOwnedFile(pathname, cause) {
|
|
26
|
+
return new FsSafeError("path-mismatch", `Atomic replace owned file disappeared: ${pathname}`, {
|
|
27
|
+
cause,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function cleanupFailure(originalError, cleanupError) {
|
|
31
|
+
if (originalError !== undefined) {
|
|
32
|
+
return new Error(`Atomic file replace failed (${String(originalError)}); cleanup also failed (${String(cleanupError)})`, { cause: originalError });
|
|
33
|
+
}
|
|
34
|
+
return cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError));
|
|
35
|
+
}
|
|
36
|
+
async function cleanupOwnedPath(params) {
|
|
37
|
+
if (!params.identity)
|
|
38
|
+
return true;
|
|
39
|
+
try {
|
|
40
|
+
const current = await params.fsModule.lstat(params.pathname, { bigint: true });
|
|
41
|
+
if (current.isSymbolicLink() ||
|
|
42
|
+
!current.isFile() ||
|
|
43
|
+
current.nlink !== 1n ||
|
|
44
|
+
!sameFileIdentityForCleanup(current, params.identity)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
await params.fsModule.unlink(params.pathname);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch (cleanupError) {
|
|
51
|
+
if (cleanupError.code === "ENOENT")
|
|
52
|
+
return true;
|
|
53
|
+
if (params.throwOnCleanupError) {
|
|
54
|
+
throw cleanupFailure(params.originalError, cleanupError);
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function cleanupOwnedPathSync(params) {
|
|
60
|
+
if (!params.identity)
|
|
61
|
+
return true;
|
|
62
|
+
try {
|
|
63
|
+
const current = params.fsModule.lstatSync(params.pathname, { bigint: true });
|
|
64
|
+
if (current.isSymbolicLink() ||
|
|
65
|
+
!current.isFile() ||
|
|
66
|
+
current.nlink !== 1n ||
|
|
67
|
+
!sameFileIdentityForCleanup(current, params.identity)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
params.fsModule.unlinkSync(params.pathname);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch (cleanupError) {
|
|
74
|
+
if (cleanupError.code === "ENOENT")
|
|
75
|
+
return true;
|
|
76
|
+
if (params.throwOnCleanupError) {
|
|
77
|
+
throw cleanupFailure(params.originalError, cleanupError);
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export class AsyncAtomicTempOwner {
|
|
83
|
+
pathname;
|
|
84
|
+
#handle;
|
|
85
|
+
#identity;
|
|
86
|
+
#exists = false;
|
|
87
|
+
#unregister;
|
|
88
|
+
constructor(pathname) {
|
|
89
|
+
this.pathname = pathname;
|
|
90
|
+
this.#unregister = registerTempPathForExit(pathname, { singleLinkFile: true });
|
|
91
|
+
}
|
|
92
|
+
start() {
|
|
93
|
+
this.#exists = true;
|
|
94
|
+
}
|
|
95
|
+
onIdentity = (identity) => {
|
|
96
|
+
this.#identity = identity;
|
|
97
|
+
this.#unregister.setIdentity(identity);
|
|
98
|
+
};
|
|
99
|
+
adopt(temp) {
|
|
100
|
+
this.#handle = temp.handle;
|
|
101
|
+
this.onIdentity(temp.identity);
|
|
102
|
+
}
|
|
103
|
+
get identity() {
|
|
104
|
+
if (!this.#identity)
|
|
105
|
+
throw new Error("Atomic temp owner has no identity");
|
|
106
|
+
return this.#identity;
|
|
107
|
+
}
|
|
108
|
+
async assertCurrent(fsModule, pathname = this.pathname) {
|
|
109
|
+
const opened = await inspectFileIdentity(async () => {
|
|
110
|
+
const stat = await this.#handle.stat({ bigint: true });
|
|
111
|
+
assertOwnedFile(stat, pathname, false);
|
|
112
|
+
return stat;
|
|
113
|
+
}, this.identity);
|
|
114
|
+
try {
|
|
115
|
+
await inspectFileIdentity(async () => {
|
|
116
|
+
const stat = await fsModule.lstat(pathname, { bigint: true });
|
|
117
|
+
assertOwnedFile(stat, pathname, true);
|
|
118
|
+
return stat;
|
|
119
|
+
}, opened);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error.code === "ENOENT") {
|
|
123
|
+
throw missingOwnedFile(pathname, error);
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async assertPublished(fsModule, pathname, expectedHash) {
|
|
129
|
+
try {
|
|
130
|
+
await this.assertCurrent(fsModule, pathname);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (!(error instanceof FsSafeError) || error.code !== "path-mismatch" || !expectedHash) {
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
let published;
|
|
139
|
+
try {
|
|
140
|
+
try {
|
|
141
|
+
published = await fsModule.open(pathname, PUBLISHED_READ_FLAGS);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error.code === "ELOOP") {
|
|
145
|
+
throw new FsSafeError("symlink", `Atomic replace published file became a symlink: ${pathname}`, {
|
|
146
|
+
cause: error,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
const identity = await inspectFileIdentity(async () => {
|
|
152
|
+
const stat = await published.stat({ bigint: true });
|
|
153
|
+
assertOwnedFile(stat, pathname, false);
|
|
154
|
+
return stat;
|
|
155
|
+
});
|
|
156
|
+
await inspectFileIdentity(async () => {
|
|
157
|
+
const stat = await fsModule.lstat(pathname, { bigint: true });
|
|
158
|
+
assertOwnedFile(stat, pathname, true);
|
|
159
|
+
return stat;
|
|
160
|
+
}, identity);
|
|
161
|
+
if (sha256Hex(await published.readFile()) !== expectedHash) {
|
|
162
|
+
throw new FsSafeError("path-mismatch", `Atomic replace published content changed: ${pathname}`);
|
|
163
|
+
}
|
|
164
|
+
await this.#handle?.close();
|
|
165
|
+
this.#handle = published;
|
|
166
|
+
this.#identity = identity;
|
|
167
|
+
published = undefined;
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
await published?.close().catch(() => undefined);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
markRenamed() {
|
|
174
|
+
this.#exists = false;
|
|
175
|
+
this.#unregister();
|
|
176
|
+
}
|
|
177
|
+
async finish(params) {
|
|
178
|
+
let deferredError;
|
|
179
|
+
let cleanupComplete = !this.#exists;
|
|
180
|
+
if (this.#exists) {
|
|
181
|
+
try {
|
|
182
|
+
cleanupComplete = await cleanupOwnedPath({
|
|
183
|
+
fsModule: params.fsModule,
|
|
184
|
+
pathname: this.pathname,
|
|
185
|
+
identity: this.#identity,
|
|
186
|
+
originalError: params.originalError,
|
|
187
|
+
throwOnCleanupError: params.throwOnCleanupError,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
deferredError = error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (cleanupComplete)
|
|
195
|
+
this.#unregister();
|
|
196
|
+
try {
|
|
197
|
+
await this.#handle?.close();
|
|
198
|
+
}
|
|
199
|
+
catch (closeError) {
|
|
200
|
+
deferredError = deferredError
|
|
201
|
+
? new AggregateError([deferredError, closeError], "Atomic temp cleanup and close failed")
|
|
202
|
+
: params.originalError !== undefined
|
|
203
|
+
? new AggregateError([params.originalError, closeError], "Atomic file replace and close failed")
|
|
204
|
+
: closeError;
|
|
205
|
+
}
|
|
206
|
+
if (deferredError)
|
|
207
|
+
throw deferredError;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export class SyncAtomicTempOwner {
|
|
211
|
+
pathname;
|
|
212
|
+
#fd;
|
|
213
|
+
#identity;
|
|
214
|
+
#exists = false;
|
|
215
|
+
#unregister;
|
|
216
|
+
constructor(pathname) {
|
|
217
|
+
this.pathname = pathname;
|
|
218
|
+
this.#unregister = registerTempPathForExit(pathname, { singleLinkFile: true });
|
|
219
|
+
}
|
|
220
|
+
start() {
|
|
221
|
+
this.#exists = true;
|
|
222
|
+
}
|
|
223
|
+
onIdentity = (identity) => {
|
|
224
|
+
this.#identity = identity;
|
|
225
|
+
this.#unregister.setIdentity(identity);
|
|
226
|
+
};
|
|
227
|
+
adopt(temp) {
|
|
228
|
+
this.#fd = temp.fd;
|
|
229
|
+
this.onIdentity(temp.identity);
|
|
230
|
+
}
|
|
231
|
+
get identity() {
|
|
232
|
+
if (!this.#identity)
|
|
233
|
+
throw new Error("Atomic temp owner has no identity");
|
|
234
|
+
return this.#identity;
|
|
235
|
+
}
|
|
236
|
+
assertCurrent(fsModule, pathname = this.pathname) {
|
|
237
|
+
const opened = inspectFileIdentitySync(() => {
|
|
238
|
+
const stat = fsModule.fstatSync(this.#fd, { bigint: true });
|
|
239
|
+
assertOwnedFile(stat, pathname, false);
|
|
240
|
+
return stat;
|
|
241
|
+
}, this.identity);
|
|
242
|
+
try {
|
|
243
|
+
inspectFileIdentitySync(() => {
|
|
244
|
+
const stat = fsModule.lstatSync(pathname, { bigint: true });
|
|
245
|
+
assertOwnedFile(stat, pathname, true);
|
|
246
|
+
return stat;
|
|
247
|
+
}, opened);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
if (error.code === "ENOENT") {
|
|
251
|
+
throw missingOwnedFile(pathname, error);
|
|
252
|
+
}
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
assertPublished(fsModule, pathname, expectedHash) {
|
|
257
|
+
try {
|
|
258
|
+
this.assertCurrent(fsModule, pathname);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (!(error instanceof FsSafeError) || error.code !== "path-mismatch" || !expectedHash) {
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
let publishedFd;
|
|
267
|
+
try {
|
|
268
|
+
try {
|
|
269
|
+
publishedFd = fsModule.openSync(pathname, PUBLISHED_READ_FLAGS);
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
if (error.code === "ELOOP") {
|
|
273
|
+
throw new FsSafeError("symlink", `Atomic replace published file became a symlink: ${pathname}`, {
|
|
274
|
+
cause: error,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
const identity = inspectFileIdentitySync(() => {
|
|
280
|
+
const stat = fsModule.fstatSync(publishedFd, { bigint: true });
|
|
281
|
+
assertOwnedFile(stat, pathname, false);
|
|
282
|
+
return stat;
|
|
283
|
+
});
|
|
284
|
+
inspectFileIdentitySync(() => {
|
|
285
|
+
const stat = fsModule.lstatSync(pathname, { bigint: true });
|
|
286
|
+
assertOwnedFile(stat, pathname, true);
|
|
287
|
+
return stat;
|
|
288
|
+
}, identity);
|
|
289
|
+
if (sha256Hex(fsModule.readFileSync(publishedFd)) !== expectedHash) {
|
|
290
|
+
throw new FsSafeError("path-mismatch", `Atomic replace published content changed: ${pathname}`);
|
|
291
|
+
}
|
|
292
|
+
fsModule.closeSync(this.#fd);
|
|
293
|
+
this.#fd = publishedFd;
|
|
294
|
+
this.#identity = identity;
|
|
295
|
+
publishedFd = undefined;
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
if (publishedFd !== undefined) {
|
|
299
|
+
try {
|
|
300
|
+
fsModule.closeSync(publishedFd);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// Best-effort close after a rejected content verification.
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
markRenamed() {
|
|
309
|
+
this.#exists = false;
|
|
310
|
+
this.#unregister();
|
|
311
|
+
}
|
|
312
|
+
finish(params) {
|
|
313
|
+
let deferredError;
|
|
314
|
+
let cleanupComplete = !this.#exists;
|
|
315
|
+
if (this.#exists) {
|
|
316
|
+
try {
|
|
317
|
+
cleanupComplete = cleanupOwnedPathSync({
|
|
318
|
+
fsModule: params.fsModule,
|
|
319
|
+
pathname: this.pathname,
|
|
320
|
+
identity: this.#identity,
|
|
321
|
+
originalError: params.originalError,
|
|
322
|
+
throwOnCleanupError: params.throwOnCleanupError,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
deferredError = error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (cleanupComplete)
|
|
330
|
+
this.#unregister();
|
|
331
|
+
if (this.#fd !== undefined) {
|
|
332
|
+
try {
|
|
333
|
+
params.fsModule.closeSync(this.#fd);
|
|
334
|
+
}
|
|
335
|
+
catch (closeError) {
|
|
336
|
+
deferredError = deferredError
|
|
337
|
+
? new AggregateError([deferredError, closeError], "Atomic temp cleanup and close failed")
|
|
338
|
+
: params.originalError !== undefined
|
|
339
|
+
? new AggregateError([params.originalError, closeError], "Atomic file replace and close failed")
|
|
340
|
+
: closeError;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (deferredError)
|
|
344
|
+
throw deferredError;
|
|
345
|
+
}
|
|
346
|
+
}
|
package/dist/replace-file.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import syncFs from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { type ReplaceFileAtomicRestoreCleanup, type ReplaceFileAtomicRestoreFailureDetails, type ReplaceFileCopyFallbackRestorePolicy, type ReplaceFileDestinationHardlinkPolicy } from "./replace-file-copy-fallback.js";
|
|
4
|
+
import { type RenameIdentityPolicy } from "./replace-file-rename-policy.js";
|
|
4
5
|
export type ReplaceFileAtomicFileSystem = {
|
|
5
6
|
promises: Pick<typeof fs, "mkdir" | "writeFile" | "rename" | "copyFile" | "unlink" | "rm" | "open" | "stat" | "lstat"> & {
|
|
6
7
|
/** @deprecated Accepted for adapter compatibility but never called. */
|
|
@@ -12,7 +13,7 @@ export type ReplaceFileAtomicSyncFileSystem = Pick<typeof syncFs, "mkdirSync" |
|
|
|
12
13
|
chmodSync?: typeof syncFs.chmodSync;
|
|
13
14
|
fchmodSync?: typeof syncFs.fchmodSync;
|
|
14
15
|
};
|
|
15
|
-
export type { ReplaceFileAtomicRestoreCleanup, ReplaceFileAtomicRestoreFailureDetails, ReplaceFileCopyFallbackRestorePolicy, ReplaceFileDestinationHardlinkPolicy, };
|
|
16
|
+
export type { RenameIdentityPolicy, ReplaceFileAtomicRestoreCleanup, ReplaceFileAtomicRestoreFailureDetails, ReplaceFileCopyFallbackRestorePolicy, ReplaceFileDestinationHardlinkPolicy, };
|
|
16
17
|
type ReplaceFileAtomicBaseOptions = {
|
|
17
18
|
filePath: string;
|
|
18
19
|
content: string | Uint8Array;
|
|
@@ -26,12 +27,15 @@ type ReplaceFileAtomicBaseOptions = {
|
|
|
26
27
|
copyFallbackRestore?: ReplaceFileCopyFallbackRestorePolicy;
|
|
27
28
|
maxRestoreBytes?: number;
|
|
28
29
|
destinationHardlinks?: ReplaceFileDestinationHardlinkPolicy;
|
|
30
|
+
/** Strict by default; locked content verification is an explicit FUSE compatibility policy. */
|
|
31
|
+
renameIdentity?: RenameIdentityPolicy;
|
|
29
32
|
syncTempFile?: boolean;
|
|
30
33
|
syncParentDir?: boolean;
|
|
31
34
|
throwOnCleanupError?: boolean;
|
|
32
35
|
};
|
|
33
36
|
export type ReplaceFileAtomicOptions = ReplaceFileAtomicBaseOptions & {
|
|
34
37
|
fileSystem?: ReplaceFileAtomicFileSystem;
|
|
38
|
+
/** Runs while the exact staged file is retained; replacing or hardlinking it is rejected. */
|
|
35
39
|
beforeRename?: (params: {
|
|
36
40
|
filePath: string;
|
|
37
41
|
tempPath: string;
|
|
@@ -39,6 +43,7 @@ export type ReplaceFileAtomicOptions = ReplaceFileAtomicBaseOptions & {
|
|
|
39
43
|
};
|
|
40
44
|
export type ReplaceFileAtomicSyncOptions = ReplaceFileAtomicBaseOptions & {
|
|
41
45
|
fileSystem?: ReplaceFileAtomicSyncFileSystem;
|
|
46
|
+
/** Runs while the exact staged file is retained; replacing or hardlinking it is rejected. */
|
|
42
47
|
beforeRename?: (params: {
|
|
43
48
|
filePath: string;
|
|
44
49
|
tempPath: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replace-file.d.ts","sourceRoot":"","sources":["../src/replace-file.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"replace-file.d.ts","sourceRoot":"","sources":["../src/replace-file.ts"],"names":[],"mappings":"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"}
|
package/dist/replace-file.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import syncFs from "node:fs";
|
|
2
|
+
import syncFs, {} from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { assertDestinationHardlinkPolicy, assertDestinationHardlinkPolicySync, copyFallbackReplace, copyFallbackReplaceSync, } from "./replace-file-copy-fallback.js";
|
|
6
6
|
import { applyDirectoryMode, applyDirectoryModeSync, writeTempFile, writeTempFileSync, } from "./replace-file-descriptor.js";
|
|
7
|
+
import { atomicExpectedContentHash, validateRenameIdentity, withAtomicRenameIdentityLock, withAtomicRenameIdentityLockSync, } from "./replace-file-rename-policy.js";
|
|
8
|
+
import { AsyncAtomicTempOwner, SyncAtomicTempOwner } from "./replace-file-temp-owner.js";
|
|
7
9
|
import { assertSafePathPrefix } from "./safe-path-segment.js";
|
|
8
|
-
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
9
10
|
import { sleep, sleepSync } from "./timing.js";
|
|
10
11
|
import { serializePathWrite } from "./write-queue.js";
|
|
11
12
|
function isRetryableRenameError(error) {
|
|
@@ -17,6 +18,7 @@ function isPermissionRenameError(error) {
|
|
|
17
18
|
}
|
|
18
19
|
async function renameWithRetry(params) {
|
|
19
20
|
for (let attempt = 0; attempt <= params.maxRetries; attempt++) {
|
|
21
|
+
await params.assertSourceCurrent();
|
|
20
22
|
try {
|
|
21
23
|
await params.fsModule.rename(params.src, params.dest);
|
|
22
24
|
return { method: "rename" };
|
|
@@ -34,6 +36,7 @@ async function renameWithRetry(params) {
|
|
|
34
36
|
destinationHardlinks: params.destinationHardlinks,
|
|
35
37
|
restore: params.copyFallbackRestore,
|
|
36
38
|
maxRestoreBytes: params.maxRestoreBytes,
|
|
39
|
+
expectedSourceIdentity: params.sourceIdentity,
|
|
37
40
|
sync: params.syncFallback,
|
|
38
41
|
});
|
|
39
42
|
return { method: "copy-fallback" };
|
|
@@ -45,6 +48,7 @@ async function renameWithRetry(params) {
|
|
|
45
48
|
}
|
|
46
49
|
function renameWithRetrySync(params) {
|
|
47
50
|
for (let attempt = 0; attempt <= params.maxRetries; attempt++) {
|
|
51
|
+
params.assertSourceCurrent();
|
|
48
52
|
try {
|
|
49
53
|
params.fsModule.renameSync(params.src, params.dest);
|
|
50
54
|
return { method: "rename" };
|
|
@@ -62,6 +66,7 @@ function renameWithRetrySync(params) {
|
|
|
62
66
|
destinationHardlinks: params.destinationHardlinks,
|
|
63
67
|
restore: params.copyFallbackRestore,
|
|
64
68
|
maxRestoreBytes: params.maxRestoreBytes,
|
|
69
|
+
expectedSourceIdentity: params.sourceIdentity,
|
|
65
70
|
fchmodSync: params.fchmodSync,
|
|
66
71
|
sync: params.syncFallback,
|
|
67
72
|
});
|
|
@@ -158,26 +163,20 @@ function syncDirectoryBestEffortSync(fsModule, dirPath) {
|
|
|
158
163
|
}
|
|
159
164
|
}
|
|
160
165
|
}
|
|
161
|
-
async function cleanupTempFile(params) {
|
|
162
|
-
const cleanupError = await params.fsModule
|
|
163
|
-
.rm(params.tempPath, { force: true })
|
|
164
|
-
.catch((error) => error);
|
|
165
|
-
if (!cleanupError)
|
|
166
|
-
return true;
|
|
167
|
-
if (params.throwOnCleanupError) {
|
|
168
|
-
if (params.originalError !== undefined) {
|
|
169
|
-
throw new Error(`Atomic file replace failed (${String(params.originalError)}); cleanup also failed (${String(cleanupError)})`, { cause: params.originalError });
|
|
170
|
-
}
|
|
171
|
-
throw cleanupError;
|
|
172
|
-
}
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
166
|
export async function replaceFileAtomic(options) {
|
|
176
167
|
const filePath = options.filePath;
|
|
177
168
|
validateReplaceFilePath(filePath);
|
|
178
169
|
validateRestoreOptions(options);
|
|
170
|
+
validateRenameIdentity(options.renameIdentity);
|
|
179
171
|
return await serializePathWrite(path.resolve(filePath), async () => {
|
|
180
|
-
|
|
172
|
+
if (options.renameIdentity !== "verify-content-with-lock") {
|
|
173
|
+
return await replaceFileAtomicUnserialized(options);
|
|
174
|
+
}
|
|
175
|
+
await (options.fileSystem?.promises ?? fs).mkdir(path.dirname(filePath), {
|
|
176
|
+
recursive: true,
|
|
177
|
+
mode: options.dirMode ?? 0o700,
|
|
178
|
+
});
|
|
179
|
+
return await withAtomicRenameIdentityLock(filePath, async () => await replaceFileAtomicUnserialized(options));
|
|
181
180
|
});
|
|
182
181
|
}
|
|
183
182
|
async function replaceFileAtomicUnserialized(options) {
|
|
@@ -186,24 +185,27 @@ async function replaceFileAtomicUnserialized(options) {
|
|
|
186
185
|
const dir = path.dirname(filePath);
|
|
187
186
|
const dirMode = options.dirMode ?? 0o700;
|
|
188
187
|
const mode = await resolveMode(options);
|
|
188
|
+
const expectedHash = atomicExpectedContentHash(options.renameIdentity, options.content);
|
|
189
189
|
const tempPath = buildReplaceTempPath(filePath, options.tempPrefix);
|
|
190
|
-
const
|
|
191
|
-
let tempExists = false;
|
|
190
|
+
const tempOwner = new AsyncAtomicTempOwner(tempPath);
|
|
192
191
|
let originalError;
|
|
193
192
|
try {
|
|
194
193
|
await fsModule.mkdir(dir, { recursive: true, mode: dirMode });
|
|
195
194
|
await applyDirectoryMode({ fsModule, dirPath: dir, mode: dirMode });
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
tempOwner.start();
|
|
196
|
+
tempOwner.adopt(await writeTempFile({
|
|
198
197
|
fsModule,
|
|
199
198
|
tempPath,
|
|
200
199
|
content: options.content,
|
|
201
200
|
mode,
|
|
202
201
|
sync: options.syncTempFile === true,
|
|
202
|
+
onIdentity: tempOwner.onIdentity,
|
|
203
203
|
}));
|
|
204
|
+
await tempOwner.assertCurrent(fsModule);
|
|
204
205
|
if (options.beforeRename) {
|
|
205
206
|
await options.beforeRename({ filePath, tempPath });
|
|
206
207
|
}
|
|
208
|
+
await tempOwner.assertCurrent(fsModule);
|
|
207
209
|
await assertDestinationHardlinkPolicy(fsModule, filePath, options.destinationHardlinks);
|
|
208
210
|
const result = await renameWithRetry({
|
|
209
211
|
fsModule,
|
|
@@ -215,15 +217,23 @@ async function replaceFileAtomicUnserialized(options) {
|
|
|
215
217
|
copyFallbackRestore: options.copyFallbackRestore ?? "none",
|
|
216
218
|
maxRestoreBytes: options.maxRestoreBytes,
|
|
217
219
|
destinationHardlinks: options.destinationHardlinks,
|
|
220
|
+
sourceIdentity: tempOwner.identity,
|
|
221
|
+
assertSourceCurrent: () => tempOwner.assertCurrent(fsModule),
|
|
218
222
|
syncFallback: options.syncTempFile === true,
|
|
219
223
|
});
|
|
220
224
|
if (result.method === "rename") {
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
tempOwner.markRenamed();
|
|
226
|
+
await tempOwner.assertPublished(fsModule, filePath, expectedHash);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
await tempOwner.assertCurrent(fsModule);
|
|
223
230
|
}
|
|
224
231
|
if (options.syncParentDir) {
|
|
225
232
|
await syncDirectoryBestEffort(fsModule, dir);
|
|
226
233
|
}
|
|
234
|
+
if (result.method === "rename") {
|
|
235
|
+
await tempOwner.assertPublished(fsModule, filePath, expectedHash);
|
|
236
|
+
}
|
|
227
237
|
return result;
|
|
228
238
|
}
|
|
229
239
|
catch (error) {
|
|
@@ -231,27 +241,34 @@ async function replaceFileAtomicUnserialized(options) {
|
|
|
231
241
|
throw error;
|
|
232
242
|
}
|
|
233
243
|
finally {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
originalError,
|
|
240
|
-
throwOnCleanupError: options.throwOnCleanupError === true,
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
if (tempRemoved)
|
|
244
|
-
unregisterTempPath();
|
|
244
|
+
await tempOwner.finish({
|
|
245
|
+
fsModule,
|
|
246
|
+
originalError,
|
|
247
|
+
throwOnCleanupError: options.throwOnCleanupError === true,
|
|
248
|
+
});
|
|
245
249
|
}
|
|
246
250
|
}
|
|
247
251
|
export function replaceFileAtomicSync(options) {
|
|
248
252
|
const filePath = options.filePath;
|
|
249
253
|
validateReplaceFilePath(filePath);
|
|
250
254
|
validateRestoreOptions(options);
|
|
255
|
+
validateRenameIdentity(options.renameIdentity);
|
|
256
|
+
if (options.renameIdentity !== "verify-content-with-lock") {
|
|
257
|
+
return replaceFileAtomicSyncUnserialized(options);
|
|
258
|
+
}
|
|
259
|
+
(options.fileSystem ?? syncFs).mkdirSync(path.dirname(filePath), {
|
|
260
|
+
recursive: true,
|
|
261
|
+
mode: options.dirMode ?? 0o700,
|
|
262
|
+
});
|
|
263
|
+
return withAtomicRenameIdentityLockSync(filePath, () => replaceFileAtomicSyncUnserialized(options));
|
|
264
|
+
}
|
|
265
|
+
function replaceFileAtomicSyncUnserialized(options) {
|
|
266
|
+
const filePath = options.filePath;
|
|
251
267
|
const fsModule = options.fileSystem ?? syncFs;
|
|
252
268
|
const dir = path.dirname(filePath);
|
|
253
269
|
const dirMode = options.dirMode ?? 0o700;
|
|
254
270
|
const mode = resolveModeSync(options);
|
|
271
|
+
const expectedHash = atomicExpectedContentHash(options.renameIdentity, options.content);
|
|
255
272
|
const fchmodSync = options.fileSystem?.fchmodSync ?? (options.fileSystem === undefined ? syncFs.fchmodSync : undefined);
|
|
256
273
|
if (!fchmodSync &&
|
|
257
274
|
(options.mode !== undefined ||
|
|
@@ -260,24 +277,26 @@ export function replaceFileAtomicSync(options) {
|
|
|
260
277
|
throw missingFchmodSyncError();
|
|
261
278
|
}
|
|
262
279
|
const tempPath = buildReplaceTempPath(filePath, options.tempPrefix);
|
|
263
|
-
const
|
|
264
|
-
let tempExists = false;
|
|
280
|
+
const tempOwner = new SyncAtomicTempOwner(tempPath);
|
|
265
281
|
let originalError;
|
|
266
282
|
try {
|
|
267
283
|
fsModule.mkdirSync(dir, { recursive: true, mode: dirMode });
|
|
268
284
|
applyDirectoryModeSync({ fsModule, dirPath: dir, mode: dirMode, fchmodSync });
|
|
269
|
-
|
|
270
|
-
|
|
285
|
+
tempOwner.start();
|
|
286
|
+
tempOwner.adopt(writeTempFileSync({
|
|
271
287
|
fsModule,
|
|
272
288
|
tempPath,
|
|
273
289
|
content: options.content,
|
|
274
290
|
mode,
|
|
275
291
|
fchmodSync,
|
|
276
292
|
sync: options.syncTempFile === true,
|
|
293
|
+
onIdentity: tempOwner.onIdentity,
|
|
277
294
|
}));
|
|
295
|
+
tempOwner.assertCurrent(fsModule);
|
|
278
296
|
if (options.beforeRename) {
|
|
279
297
|
options.beforeRename({ filePath, tempPath });
|
|
280
298
|
}
|
|
299
|
+
tempOwner.assertCurrent(fsModule);
|
|
281
300
|
assertDestinationHardlinkPolicySync(fsModule, filePath, options.destinationHardlinks);
|
|
282
301
|
const result = renameWithRetrySync({
|
|
283
302
|
fsModule,
|
|
@@ -289,16 +308,24 @@ export function replaceFileAtomicSync(options) {
|
|
|
289
308
|
copyFallbackRestore: options.copyFallbackRestore ?? "none",
|
|
290
309
|
maxRestoreBytes: options.maxRestoreBytes,
|
|
291
310
|
destinationHardlinks: options.destinationHardlinks,
|
|
311
|
+
sourceIdentity: tempOwner.identity,
|
|
312
|
+
assertSourceCurrent: () => tempOwner.assertCurrent(fsModule),
|
|
292
313
|
fchmodSync,
|
|
293
314
|
syncFallback: options.syncTempFile === true,
|
|
294
315
|
});
|
|
295
316
|
if (result.method === "rename") {
|
|
296
|
-
|
|
297
|
-
|
|
317
|
+
tempOwner.markRenamed();
|
|
318
|
+
tempOwner.assertPublished(fsModule, filePath, expectedHash);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
tempOwner.assertCurrent(fsModule);
|
|
298
322
|
}
|
|
299
323
|
if (options.syncParentDir) {
|
|
300
324
|
syncDirectoryBestEffortSync(fsModule, dir);
|
|
301
325
|
}
|
|
326
|
+
if (result.method === "rename") {
|
|
327
|
+
tempOwner.assertPublished(fsModule, filePath, expectedHash);
|
|
328
|
+
}
|
|
302
329
|
return result;
|
|
303
330
|
}
|
|
304
331
|
catch (error) {
|
|
@@ -306,23 +333,10 @@ export function replaceFileAtomicSync(options) {
|
|
|
306
333
|
throw error;
|
|
307
334
|
}
|
|
308
335
|
finally {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
catch (cleanupError) {
|
|
316
|
-
if (options.throwOnCleanupError) {
|
|
317
|
-
if (originalError !== undefined) {
|
|
318
|
-
throw new Error(`Atomic file replace failed (${String(originalError)}); cleanup also failed (${String(cleanupError)})`, { cause: originalError });
|
|
319
|
-
}
|
|
320
|
-
throw cleanupError;
|
|
321
|
-
}
|
|
322
|
-
// The temp file is best-effort cleanup after write failure.
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
if (tempRemoved)
|
|
326
|
-
unregisterTempPath();
|
|
336
|
+
tempOwner.finish({
|
|
337
|
+
fsModule,
|
|
338
|
+
originalError,
|
|
339
|
+
throwOnCleanupError: options.throwOnCleanupError === true,
|
|
340
|
+
});
|
|
327
341
|
}
|
|
328
342
|
}
|