@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.
Files changed (183) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +7 -0
  3. package/dist/archive-deadline.d.ts +3 -0
  4. package/dist/archive-deadline.d.ts.map +1 -1
  5. package/dist/archive-deadline.js +44 -8
  6. package/dist/archive-entry.d.ts.map +1 -1
  7. package/dist/archive-entry.js +1 -0
  8. package/dist/archive-errors.d.ts +1 -0
  9. package/dist/archive-errors.d.ts.map +1 -1
  10. package/dist/archive-errors.js +3 -0
  11. package/dist/archive-input.d.ts.map +1 -1
  12. package/dist/archive-input.js +26 -16
  13. package/dist/archive-limits.d.ts +11 -3
  14. package/dist/archive-limits.d.ts.map +1 -1
  15. package/dist/archive-limits.js +24 -0
  16. package/dist/archive-native.d.ts +3 -2
  17. package/dist/archive-native.d.ts.map +1 -1
  18. package/dist/archive-native.js +24 -9
  19. package/dist/archive-policy.d.ts +2 -0
  20. package/dist/archive-policy.d.ts.map +1 -1
  21. package/dist/archive-policy.js +9 -1
  22. package/dist/archive-read.d.ts.map +1 -1
  23. package/dist/archive-read.js +69 -41
  24. package/dist/archive-staging.d.ts +3 -0
  25. package/dist/archive-staging.d.ts.map +1 -1
  26. package/dist/archive-staging.js +91 -43
  27. package/dist/archive-tar-admission.d.ts +7 -0
  28. package/dist/archive-tar-admission.d.ts.map +1 -0
  29. package/dist/archive-tar-admission.js +43 -0
  30. package/dist/archive-tar-gnu.d.ts +2 -0
  31. package/dist/archive-tar-gnu.d.ts.map +1 -0
  32. package/dist/archive-tar-gnu.js +20 -0
  33. package/dist/archive-tar-header.d.ts +8 -0
  34. package/dist/archive-tar-header.d.ts.map +1 -0
  35. package/dist/archive-tar-header.js +47 -0
  36. package/dist/archive-tar-meta.d.ts +29 -1
  37. package/dist/archive-tar-meta.d.ts.map +1 -1
  38. package/dist/archive-tar-meta.js +104 -35
  39. package/dist/archive-tar-runtime.d.ts +3 -0
  40. package/dist/archive-tar-runtime.d.ts.map +1 -1
  41. package/dist/archive-tar-runtime.js +3 -0
  42. package/dist/archive-tar.d.ts.map +1 -1
  43. package/dist/archive-tar.js +8 -2
  44. package/dist/archive-zip-names.js +2 -2
  45. package/dist/archive.d.ts.map +1 -1
  46. package/dist/archive.js +38 -26
  47. package/dist/atomic.d.ts +1 -1
  48. package/dist/atomic.d.ts.map +1 -1
  49. package/dist/bounded-read-stream.d.ts.map +1 -1
  50. package/dist/bounded-read-stream.js +2 -4
  51. package/dist/bounded-read.d.ts.map +1 -1
  52. package/dist/bounded-read.js +3 -10
  53. package/dist/byte-budget.d.ts +5 -0
  54. package/dist/byte-budget.d.ts.map +1 -0
  55. package/dist/byte-budget.js +9 -0
  56. package/dist/file-lock-sync.d.ts.map +1 -1
  57. package/dist/file-lock-sync.js +10 -5
  58. package/dist/file-store-boundary.d.ts.map +1 -1
  59. package/dist/file-store-boundary.js +9 -5
  60. package/dist/file-store-limit.d.ts +2 -0
  61. package/dist/file-store-limit.d.ts.map +1 -0
  62. package/dist/file-store-limit.js +8 -0
  63. package/dist/file-store-sync-write.d.ts.map +1 -1
  64. package/dist/file-store-sync-write.js +38 -8
  65. package/dist/file-store.d.ts.map +1 -1
  66. package/dist/file-store.js +26 -26
  67. package/dist/json-durable-queue-directory.d.ts +2 -0
  68. package/dist/json-durable-queue-directory.d.ts.map +1 -0
  69. package/dist/json-durable-queue-directory.js +20 -0
  70. package/dist/json-durable-queue-ownership.d.ts +14 -0
  71. package/dist/json-durable-queue-ownership.d.ts.map +1 -0
  72. package/dist/json-durable-queue-ownership.js +168 -0
  73. package/dist/json-durable-queue-retirement.d.ts +9 -0
  74. package/dist/json-durable-queue-retirement.d.ts.map +1 -0
  75. package/dist/json-durable-queue-retirement.js +126 -0
  76. package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
  77. package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
  78. package/dist/json-durable-queue-transfer-lock.js +19 -0
  79. package/dist/json-durable-queue.d.ts +1 -0
  80. package/dist/json-durable-queue.d.ts.map +1 -1
  81. package/dist/json-durable-queue.js +90 -57
  82. package/dist/json.d.ts.map +1 -1
  83. package/dist/json.js +27 -8
  84. package/dist/local-roots.d.ts.map +1 -1
  85. package/dist/local-roots.js +4 -2
  86. package/dist/native-binding.d.ts +12 -3
  87. package/dist/native-binding.d.ts.map +1 -1
  88. package/dist/output-sibling.d.ts.map +1 -1
  89. package/dist/output-sibling.js +11 -110
  90. package/dist/output.d.ts.map +1 -1
  91. package/dist/output.js +4 -2
  92. package/dist/pinned-open.d.ts.map +1 -1
  93. package/dist/pinned-open.js +27 -48
  94. package/dist/pinned-write.d.ts.map +1 -1
  95. package/dist/pinned-write.js +8 -9
  96. package/dist/private-temp-workspace.d.ts +3 -1
  97. package/dist/private-temp-workspace.d.ts.map +1 -1
  98. package/dist/private-temp-workspace.js +81 -56
  99. package/dist/publish-file.d.ts.map +1 -1
  100. package/dist/publish-file.js +2 -4
  101. package/dist/read-opened-file.d.ts.map +1 -1
  102. package/dist/read-opened-file.js +6 -4
  103. package/dist/regular-file.d.ts.map +1 -1
  104. package/dist/regular-file.js +136 -82
  105. package/dist/replace-file-copy-fallback.d.ts +3 -1
  106. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  107. package/dist/replace-file-copy-fallback.js +21 -36
  108. package/dist/replace-file-copy-source.d.ts +21 -0
  109. package/dist/replace-file-copy-source.d.ts.map +1 -0
  110. package/dist/replace-file-copy-source.js +112 -0
  111. package/dist/replace-file-descriptor.d.ts +13 -3
  112. package/dist/replace-file-descriptor.d.ts.map +1 -1
  113. package/dist/replace-file-descriptor.js +32 -7
  114. package/dist/replace-file-rename-policy.d.ts +7 -0
  115. package/dist/replace-file-rename-policy.d.ts.map +1 -0
  116. package/dist/replace-file-rename-policy.js +30 -0
  117. package/dist/replace-file-temp-owner.d.ts +46 -0
  118. package/dist/replace-file-temp-owner.d.ts.map +1 -0
  119. package/dist/replace-file-temp-owner.js +346 -0
  120. package/dist/replace-file.d.ts +6 -1
  121. package/dist/replace-file.d.ts.map +1 -1
  122. package/dist/replace-file.js +72 -58
  123. package/dist/root-impl.d.ts.map +1 -1
  124. package/dist/root-impl.js +40 -26
  125. package/dist/secret-file.d.ts.map +1 -1
  126. package/dist/secret-file.js +6 -3
  127. package/dist/secret-read-async.d.ts.map +1 -1
  128. package/dist/secret-read-async.js +6 -5
  129. package/dist/secure-file.d.ts.map +1 -1
  130. package/dist/secure-file.js +17 -13
  131. package/dist/sibling-staged-file.d.ts +15 -0
  132. package/dist/sibling-staged-file.d.ts.map +1 -0
  133. package/dist/sibling-staged-file.js +148 -0
  134. package/dist/sibling-temp.d.ts +3 -0
  135. package/dist/sibling-temp.d.ts.map +1 -1
  136. package/dist/sibling-temp.js +30 -74
  137. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  138. package/dist/sidecar-lock-acquire.js +48 -27
  139. package/dist/sidecar-lock-handle.d.ts +6 -2
  140. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  141. package/dist/sidecar-lock-handle.js +17 -3
  142. package/dist/sidecar-lock-policy.d.ts +2 -0
  143. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  144. package/dist/sidecar-lock-policy.js +29 -0
  145. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  146. package/dist/sidecar-lock-reclaim.js +29 -7
  147. package/dist/sidecar-lock.d.ts.map +1 -1
  148. package/dist/sidecar-lock.js +28 -16
  149. package/dist/suppressed-error.d.ts +6 -0
  150. package/dist/suppressed-error.d.ts.map +1 -0
  151. package/dist/suppressed-error.js +15 -0
  152. package/dist/temp-cleanup.d.ts +2 -0
  153. package/dist/temp-cleanup.d.ts.map +1 -1
  154. package/dist/temp-cleanup.js +25 -10
  155. package/dist/temp-workspace-owner.d.ts +23 -0
  156. package/dist/temp-workspace-owner.d.ts.map +1 -0
  157. package/dist/temp-workspace-owner.js +320 -0
  158. package/dist/temp.d.ts +1 -1
  159. package/dist/temp.d.ts.map +1 -1
  160. package/dist/test-hooks.d.ts +5 -0
  161. package/dist/test-hooks.d.ts.map +1 -1
  162. package/docs/advanced.md +8 -1
  163. package/docs/archive.md +194 -26
  164. package/docs/atomic.md +11 -2
  165. package/docs/durability.md +1 -1
  166. package/docs/file-store.md +2 -0
  167. package/docs/install.md +8 -4
  168. package/docs/json.md +8 -4
  169. package/docs/native-helper.md +15 -3
  170. package/docs/native.md +33 -6
  171. package/docs/output.md +10 -0
  172. package/docs/public-api.md +6 -3
  173. package/docs/quickstart.md +1 -1
  174. package/docs/reading.md +1 -1
  175. package/docs/regular-file.md +9 -2
  176. package/docs/root.md +2 -0
  177. package/docs/secret-file.md +5 -1
  178. package/docs/secure-file.md +4 -2
  179. package/docs/security-model.md +18 -0
  180. package/docs/sidecar-lock.md +4 -0
  181. package/docs/store.md +13 -0
  182. package/docs/temp.md +128 -14
  183. 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
+ }
@@ -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,MAAM,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAElC,OAAO,EAKL,KAAK,+BAA+B,EACpC,KAAK,sCAAsC,EAC3C,KAAK,oCAAoC,EACzC,KAAK,oCAAoC,EAC1C,MAAM,iCAAiC,CAAC;AAazC,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,EAAE,IAAI,CACZ,OAAO,EAAE,EACP,OAAO,GACP,WAAW,GACX,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,IAAI,GACJ,MAAM,GACN,MAAM,GACN,OAAO,CACV,GAAG;QACF,uEAAuE;QACvE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC,KAAK,CAAC;KACzB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,+BAA+B,GAAG,IAAI,CAChD,OAAO,MAAM,EACX,WAAW,GACX,cAAc,GACd,eAAe,GACf,YAAY,GACZ,cAAc,GACd,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,WAAW,GACX,WAAW,GACX,WAAW,GACX,UAAU,GACV,WAAW,GACX,eAAe,GACf,UAAU,GACV,WAAW,CACd,GAAG;IACF,uEAAuE;IACvE,SAAS,CAAC,EAAE,OAAO,MAAM,CAAC,SAAS,CAAC;IACpC,UAAU,CAAC,EAAE,OAAO,MAAM,CAAC,UAAU,CAAC;CACvC,CAAC;AAEF,YAAY,EACV,+BAA+B,EAC/B,sCAAsC,EACtC,oCAAoC,EACpC,oCAAoC,GACrC,CAAC;AAEF,KAAK,4BAA4B,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,mBAAmB,CAAC,EAAE,oCAAoC,CAAC;IAC3D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,oCAAoC,CAAC;IAC5D,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,4BAA4B,GAAG;IACpE,UAAU,CAAC,EAAE,2BAA2B,CAAC;IACzC,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAClF,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG,4BAA4B,GAAG;IACxE,UAAU,CAAC,EAAE,+BAA+B,CAAC;IAC7C,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACzE,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,QAAQ,GAAG,eAAe,CAAC;CACpC,CAAC;AAgNF,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,uBAAuB,CAAC,CAOlC;AAkED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,4BAA4B,GACpC,uBAAuB,CAoFzB"}
1
+ {"version":3,"file":"replace-file.d.ts","sourceRoot":"","sources":["../src/replace-file.ts"],"names":[],"mappings":"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"}
@@ -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
- return await replaceFileAtomicUnserialized(options);
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 unregisterTempPath = registerTempPathForExit(tempPath);
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
- tempExists = true;
197
- unregisterTempPath.setIdentity(await writeTempFile({
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
- tempExists = false;
222
- unregisterTempPath();
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
- let tempRemoved = !tempExists;
235
- if (tempExists) {
236
- tempRemoved = await cleanupTempFile({
237
- fsModule,
238
- tempPath,
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 unregisterTempPath = registerTempPathForExit(tempPath);
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
- tempExists = true;
270
- unregisterTempPath.setIdentity(writeTempFileSync({
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
- tempExists = false;
297
- unregisterTempPath();
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
- let tempRemoved = !tempExists;
310
- if (tempExists) {
311
- try {
312
- fsModule.rmSync(tempPath, { force: true });
313
- tempRemoved = true;
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
  }