@openclaw/fs-safe 0.6.0 → 0.7.1
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 +79 -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 -27
- 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/directory-guard.d.ts +2 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +10 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +81 -30
- package/dist/file-observation.d.ts +9 -0
- package/dist/file-observation.d.ts.map +1 -0
- package/dist/file-observation.js +22 -0
- 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 +32 -13
- 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/native-pinned-write-windows.js +1 -1
- package/dist/opened-file-failure.d.ts +7 -0
- package/dist/opened-file-failure.d.ts.map +1 -0
- package/dist/opened-file-failure.js +41 -0
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +8 -2
- 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 +10 -10
- 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 +113 -82
- 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 +91 -37
- 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 +16 -5
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +60 -28
- package/dist/sidecar-lock-root.d.ts +3 -0
- package/dist/sidecar-lock-root.d.ts.map +1 -0
- package/dist/sidecar-lock-root.js +76 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +28 -16
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +4 -1
- 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 +202 -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 +11 -4
- package/docs/native-helper.md +15 -3
- package/docs/native.md +33 -6
- package/docs/output.md +16 -3
- 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 +60 -4
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- package/docs/writing.md +6 -0
- package/package.json +10 -10
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import syncFs, { type BigIntStats } from "node:fs";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
2
|
+
import fs, { type FileHandle } from "node:fs/promises";
|
|
3
3
|
type AsyncTempFileSystem = Pick<typeof fs, "lstat" | "open" | "writeFile">;
|
|
4
4
|
type SyncTempFileSystem = Pick<typeof syncFs, "closeSync" | "fstatSync" | "fsyncSync" | "lstatSync" | "openSync" | "writeFileSync">;
|
|
5
5
|
export type SyncFchmod = (fd: number, mode: number) => void;
|
|
@@ -7,6 +7,8 @@ export declare function applyDirectoryMode(params: {
|
|
|
7
7
|
fsModule: AsyncTempFileSystem;
|
|
8
8
|
dirPath: string;
|
|
9
9
|
mode: number;
|
|
10
|
+
/** Compatibility for best-effort directory modes; admission and close still fail closed. */
|
|
11
|
+
ignoreChmodError?: boolean;
|
|
10
12
|
}): Promise<void>;
|
|
11
13
|
export declare function applyDirectoryModeSync(params: {
|
|
12
14
|
fsModule: SyncTempFileSystem;
|
|
@@ -20,7 +22,11 @@ export declare function writeTempFile(params: {
|
|
|
20
22
|
content: string | Uint8Array;
|
|
21
23
|
mode: number;
|
|
22
24
|
sync: boolean;
|
|
23
|
-
|
|
25
|
+
onIdentity?: (identity: BigIntStats) => void;
|
|
26
|
+
}): Promise<{
|
|
27
|
+
handle: FileHandle;
|
|
28
|
+
identity: BigIntStats;
|
|
29
|
+
}>;
|
|
24
30
|
export declare function writeTempFileSync(params: {
|
|
25
31
|
fsModule: SyncTempFileSystem;
|
|
26
32
|
tempPath: string;
|
|
@@ -28,6 +34,10 @@ export declare function writeTempFileSync(params: {
|
|
|
28
34
|
mode: number;
|
|
29
35
|
fchmodSync?: SyncFchmod;
|
|
30
36
|
sync: boolean;
|
|
31
|
-
|
|
37
|
+
onIdentity?: (identity: BigIntStats) => void;
|
|
38
|
+
}): {
|
|
39
|
+
fd: number;
|
|
40
|
+
identity: BigIntStats;
|
|
41
|
+
};
|
|
32
42
|
export {};
|
|
33
43
|
//# sourceMappingURL=replace-file-descriptor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replace-file-descriptor.d.ts","sourceRoot":"","sources":["../src/replace-file-descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAC/D,OAAO,
|
|
1
|
+
{"version":3,"file":"replace-file-descriptor.d.ts","sourceRoot":"","sources":["../src/replace-file-descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKvD,KAAK,mBAAmB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAC3E,KAAK,kBAAkB,GAAG,IAAI,CAC5B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,eAAe,CACrF,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AA2B5D,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBhB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,GAAG,IAAI,CAcP;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CAAC,CA0BzD;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CA0BxC"}
|
|
@@ -2,6 +2,7 @@ import syncFs, {} from "node:fs";
|
|
|
2
2
|
import fs, {} from "node:fs/promises";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { sameFileIdentity } from "./file-identity.js";
|
|
5
|
+
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
5
6
|
function directoryOpenFlags() {
|
|
6
7
|
return (syncFs.constants.O_RDONLY |
|
|
7
8
|
syncFs.constants.O_DIRECTORY |
|
|
@@ -31,7 +32,13 @@ export async function applyDirectoryMode(params) {
|
|
|
31
32
|
const handle = await params.fsModule.open(params.dirPath, directoryOpenFlags());
|
|
32
33
|
try {
|
|
33
34
|
assertSameDirectory(expected, await handle.stat(), params.dirPath);
|
|
34
|
-
|
|
35
|
+
try {
|
|
36
|
+
await handle.chmod(params.mode);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (params.ignoreChmodError !== true)
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
35
42
|
}
|
|
36
43
|
finally {
|
|
37
44
|
await handle.close();
|
|
@@ -55,6 +62,8 @@ export function applyDirectoryModeSync(params) {
|
|
|
55
62
|
export async function writeTempFile(params) {
|
|
56
63
|
const handle = await params.fsModule.open(params.tempPath, "wx", params.mode);
|
|
57
64
|
try {
|
|
65
|
+
const identity = await inspectFileIdentity(() => handle.stat({ bigint: true }));
|
|
66
|
+
params.onIdentity?.(identity);
|
|
58
67
|
await params.fsModule.writeFile(handle, params.content);
|
|
59
68
|
await handle.chmod(params.mode);
|
|
60
69
|
if (params.sync) {
|
|
@@ -67,15 +76,24 @@ export async function writeTempFile(params) {
|
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
78
|
}
|
|
70
|
-
|
|
79
|
+
await inspectFileIdentity(() => handle.stat({ bigint: true }), identity);
|
|
80
|
+
return { handle, identity };
|
|
71
81
|
}
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
catch (error) {
|
|
83
|
+
try {
|
|
84
|
+
await handle.close();
|
|
85
|
+
}
|
|
86
|
+
catch (closeError) {
|
|
87
|
+
throw new AggregateError([error, closeError], "Atomic temp write and close failed");
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
74
90
|
}
|
|
75
91
|
}
|
|
76
92
|
export function writeTempFileSync(params) {
|
|
77
93
|
const fd = params.fsModule.openSync(params.tempPath, "wx", params.mode);
|
|
78
94
|
try {
|
|
95
|
+
const identity = inspectFileIdentitySync(() => params.fsModule.fstatSync(fd, { bigint: true }));
|
|
96
|
+
params.onIdentity?.(identity);
|
|
79
97
|
params.fsModule.writeFileSync(fd, params.content);
|
|
80
98
|
params.fchmodSync?.(fd, params.mode);
|
|
81
99
|
if (params.sync) {
|
|
@@ -88,9 +106,16 @@ export function writeTempFileSync(params) {
|
|
|
88
106
|
}
|
|
89
107
|
}
|
|
90
108
|
}
|
|
91
|
-
|
|
109
|
+
inspectFileIdentitySync(() => params.fsModule.fstatSync(fd, { bigint: true }), identity);
|
|
110
|
+
return { fd, identity };
|
|
92
111
|
}
|
|
93
|
-
|
|
94
|
-
|
|
112
|
+
catch (error) {
|
|
113
|
+
try {
|
|
114
|
+
params.fsModule.closeSync(fd);
|
|
115
|
+
}
|
|
116
|
+
catch (closeError) {
|
|
117
|
+
throw new AggregateError([error, closeError], "Atomic temp write and close failed");
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
95
120
|
}
|
|
96
121
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RenameIdentityPolicy } from "./pinned-write.js";
|
|
2
|
+
export type { RenameIdentityPolicy };
|
|
3
|
+
export declare function validateRenameIdentity(policy: RenameIdentityPolicy | undefined): void;
|
|
4
|
+
export declare function atomicExpectedContentHash(policy: RenameIdentityPolicy | undefined, content: string | Uint8Array): string | undefined;
|
|
5
|
+
export declare function withAtomicRenameIdentityLock<T>(filePath: string, fn: () => Promise<T>): Promise<T>;
|
|
6
|
+
export declare function withAtomicRenameIdentityLockSync<T>(filePath: string, fn: () => T): T;
|
|
7
|
+
//# sourceMappingURL=replace-file-rename-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replace-file-rename-policy.d.ts","sourceRoot":"","sources":["../src/replace-file-rename-policy.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAE9D,YAAY,EAAE,oBAAoB,EAAE,CAAC;AAErC,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,oBAAoB,GAAG,SAAS,GAAG,IAAI,CAIrF;AAED,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,oBAAoB,GAAG,SAAS,EACxC,OAAO,EAAE,MAAM,GAAG,UAAU,GAC3B,MAAM,GAAG,SAAS,CAGpB;AAcD,wBAAsB,4BAA4B,CAAC,CAAC,EAClD,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED,wBAAgB,gCAAgC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEpF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { withFileLock, withFileLockSync } from "./file-lock.js";
|
|
3
|
+
import { sha256Hex } from "./file-identity.js";
|
|
4
|
+
export function validateRenameIdentity(policy) {
|
|
5
|
+
if (policy !== undefined && policy !== "strict" && policy !== "verify-content-with-lock") {
|
|
6
|
+
throw new RangeError("renameIdentity must be strict or verify-content-with-lock");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function atomicExpectedContentHash(policy, content) {
|
|
10
|
+
if (policy !== "verify-content-with-lock")
|
|
11
|
+
return undefined;
|
|
12
|
+
return sha256Hex(typeof content === "string" ? content : Buffer.from(content));
|
|
13
|
+
}
|
|
14
|
+
function lockOptions(filePath) {
|
|
15
|
+
const resolved = path.resolve(filePath);
|
|
16
|
+
return {
|
|
17
|
+
managerKey: `fs-safe.atomic:${resolved}`,
|
|
18
|
+
lockPath: path.join(path.dirname(resolved), `.fs-safe-atomic-${sha256Hex(resolved)}.lock`),
|
|
19
|
+
staleMs: 30_000,
|
|
20
|
+
timeoutMs: 5_000,
|
|
21
|
+
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
|
|
22
|
+
retry: { retries: 5, minTimeout: 100, maxTimeout: 2_000, factor: 2 },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export async function withAtomicRenameIdentityLock(filePath, fn) {
|
|
26
|
+
return await withFileLock(filePath, lockOptions(filePath), fn);
|
|
27
|
+
}
|
|
28
|
+
export function withAtomicRenameIdentityLockSync(filePath, fn) {
|
|
29
|
+
return withFileLockSync(filePath, lockOptions(filePath), fn);
|
|
30
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import syncFs, { type BigIntStats } from "node:fs";
|
|
2
|
+
import fs, { type FileHandle } from "node:fs/promises";
|
|
3
|
+
type AsyncOwnerFileSystem = Pick<typeof fs, "lstat" | "open" | "unlink">;
|
|
4
|
+
type SyncOwnerFileSystem = Pick<typeof syncFs, "closeSync" | "fstatSync" | "lstatSync" | "openSync" | "readFileSync" | "unlinkSync">;
|
|
5
|
+
export declare class AsyncAtomicTempOwner {
|
|
6
|
+
#private;
|
|
7
|
+
readonly pathname: string;
|
|
8
|
+
constructor(pathname: string);
|
|
9
|
+
start(): void;
|
|
10
|
+
readonly onIdentity: (identity: BigIntStats) => void;
|
|
11
|
+
adopt(temp: {
|
|
12
|
+
handle: FileHandle;
|
|
13
|
+
identity: BigIntStats;
|
|
14
|
+
}): void;
|
|
15
|
+
get identity(): BigIntStats;
|
|
16
|
+
assertCurrent(fsModule: AsyncOwnerFileSystem, pathname?: string): Promise<void>;
|
|
17
|
+
assertPublished(fsModule: AsyncOwnerFileSystem, pathname: string, expectedHash?: string): Promise<void>;
|
|
18
|
+
markRenamed(): void;
|
|
19
|
+
finish(params: {
|
|
20
|
+
fsModule: AsyncOwnerFileSystem;
|
|
21
|
+
originalError?: unknown;
|
|
22
|
+
throwOnCleanupError: boolean;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export declare class SyncAtomicTempOwner {
|
|
26
|
+
#private;
|
|
27
|
+
readonly pathname: string;
|
|
28
|
+
constructor(pathname: string);
|
|
29
|
+
start(): void;
|
|
30
|
+
readonly onIdentity: (identity: BigIntStats) => void;
|
|
31
|
+
adopt(temp: {
|
|
32
|
+
fd: number;
|
|
33
|
+
identity: BigIntStats;
|
|
34
|
+
}): void;
|
|
35
|
+
get identity(): BigIntStats;
|
|
36
|
+
assertCurrent(fsModule: SyncOwnerFileSystem, pathname?: string): void;
|
|
37
|
+
assertPublished(fsModule: SyncOwnerFileSystem, pathname: string, expectedHash?: string): void;
|
|
38
|
+
markRenamed(): void;
|
|
39
|
+
finish(params: {
|
|
40
|
+
fsModule: SyncOwnerFileSystem;
|
|
41
|
+
originalError?: unknown;
|
|
42
|
+
throwOnCleanupError: boolean;
|
|
43
|
+
}): void;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
|
46
|
+
//# sourceMappingURL=replace-file-temp-owner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replace-file-temp-owner.d.ts","sourceRoot":"","sources":["../src/replace-file-temp-owner.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMvD,KAAK,oBAAoB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC;AACzE,KAAK,mBAAmB,GAAG,IAAI,CAC7B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,cAAc,GAAG,YAAY,CACrF,CAAC;AAiGF,qBAAa,oBAAoB;;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAM1B,YAAY,QAAQ,EAAE,MAAM,EAG3B;IAED,KAAK,IAAI,IAAI,CAEZ;IAED,QAAQ,CAAC,UAAU,aAAc,WAAW,KAAG,IAAI,CAGjD;IAEF,KAAK,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,WAAW,CAAA;KAAE,GAAG,IAAI,CAG/D;IAED,IAAI,QAAQ,IAAI,WAAW,CAG1B;IAEK,aAAa,CAAC,QAAQ,EAAE,oBAAoB,EAAE,QAAQ,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkB3F;IAEK,eAAe,CACnB,QAAQ,EAAE,oBAAoB,EAC9B,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CA0Cf;IAED,WAAW,IAAI,IAAI,CAGlB;IAEK,MAAM,CAAC,MAAM,EAAE;QACnB,QAAQ,EAAE,oBAAoB,CAAC;QAC/B,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,mBAAmB,EAAE,OAAO,CAAC;KAC9B,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BhB;CACF;AAED,qBAAa,mBAAmB;;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAM1B,YAAY,QAAQ,EAAE,MAAM,EAG3B;IAED,KAAK,IAAI,IAAI,CAEZ;IAED,QAAQ,CAAC,UAAU,aAAc,WAAW,KAAG,IAAI,CAGjD;IAEF,KAAK,CAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,WAAW,CAAA;KAAE,GAAG,IAAI,CAGvD;IAED,IAAI,QAAQ,IAAI,WAAW,CAG1B;IAED,aAAa,CAAC,QAAQ,EAAE,mBAAmB,EAAE,QAAQ,SAAgB,GAAG,IAAI,CAkB3E;IAED,eAAe,CACb,QAAQ,EAAE,mBAAmB,EAC7B,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,IAAI,CAgDN;IAED,WAAW,IAAI,IAAI,CAGlB;IAED,MAAM,CAAC,MAAM,EAAE;QACb,QAAQ,EAAE,mBAAmB,CAAC;QAC9B,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,mBAAmB,EAAE,OAAO,CAAC;KAC9B,GAAG,IAAI,CAgCP;CACF"}
|
|
@@ -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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"}
|