@openclaw/fs-safe 0.8.6 → 0.10.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 +93 -0
- package/README.md +17 -0
- package/dist/advanced.d.ts +1 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -0
- package/dist/archive-crc32.d.ts.map +1 -1
- package/dist/archive-crc32.js +6 -1
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +3 -4
- package/dist/archive-durability.d.ts +24 -0
- package/dist/archive-durability.d.ts.map +1 -0
- package/dist/archive-durability.js +180 -0
- package/dist/archive-gzip-tail.d.ts +2 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +22 -4
- package/dist/archive-input.js +4 -4
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +3 -2
- package/dist/archive-merge.d.ts +1 -0
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +42 -9
- package/dist/archive-native.d.ts +1 -0
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +4 -2
- package/dist/archive-options.d.ts +2 -0
- package/dist/archive-options.d.ts.map +1 -1
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +72 -70
- package/dist/archive-staging.d.ts +1 -1
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +24 -19
- package/dist/archive-tar-stream.d.ts +11 -4
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +20 -8
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +2 -1
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +9 -4
- package/dist/bounded-read.d.ts +7 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +73 -43
- package/dist/clone-metadata.d.ts +19 -0
- package/dist/clone-metadata.d.ts.map +1 -0
- package/dist/clone-metadata.js +32 -0
- package/dist/copy-file-input.d.ts +23 -0
- package/dist/copy-file-input.d.ts.map +1 -0
- package/dist/copy-file-input.js +91 -0
- package/dist/copy-policy.d.ts +3 -0
- package/dist/copy-policy.d.ts.map +1 -0
- package/dist/copy-policy.js +8 -0
- package/dist/copy-publication.d.ts +15 -0
- package/dist/copy-publication.d.ts.map +1 -0
- package/dist/copy-publication.js +30 -0
- package/dist/copy-tree-portable.d.ts +9 -0
- package/dist/copy-tree-portable.d.ts.map +1 -0
- package/dist/copy-tree-portable.js +191 -0
- package/dist/copy.d.ts +16 -0
- package/dist/copy.d.ts.map +1 -0
- package/dist/copy.js +123 -0
- package/dist/directory-mode-node.d.ts.map +1 -1
- package/dist/directory-mode-node.js +4 -4
- package/dist/durability.d.ts +1 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/error-detail.d.ts.map +1 -1
- package/dist/error-detail.js +4 -1
- package/dist/file-hash.d.ts +6 -2
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +49 -12
- package/dist/file-store-sync-write.d.ts +1 -0
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +9 -6
- package/dist/file-store.d.ts +4 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +10 -1
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +2 -12
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +1 -2
- package/dist/guarded-mkdir.d.ts +1 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +1 -0
- package/dist/guarded-mutation.d.ts +2 -0
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/guarded-mutation.js +9 -5
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +9 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +9 -13
- package/dist/json-document-store.d.ts +3 -0
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +1 -0
- package/dist/json-durable-queue-directory.js +3 -3
- package/dist/json-durable-queue-ownership.js +3 -2
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +31 -27
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +22 -24
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +4 -3
- package/dist/move-path-stage.d.ts +8 -0
- package/dist/move-path-stage.d.ts.map +1 -0
- package/dist/move-path-stage.js +55 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +55 -47
- package/dist/mutation-authority.d.ts +8 -0
- package/dist/mutation-authority.d.ts.map +1 -0
- package/dist/mutation-authority.js +36 -0
- package/dist/native-binding.d.ts +24 -1
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +11 -5
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +18 -7
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +1 -0
- package/dist/native-staged-file.d.ts +3 -3
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +35 -8
- package/dist/opened-file-failure.d.ts +1 -1
- package/dist/opened-file-failure.d.ts.map +1 -1
- package/dist/opened-file-failure.js +3 -2
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +3 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +38 -48
- package/dist/permissions.js +3 -3
- package/dist/pinned-operation.js +1 -1
- package/dist/pinned-write.d.ts +5 -1
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +50 -30
- package/dist/positional-read.d.ts +9 -0
- package/dist/positional-read.d.ts.map +1 -0
- package/dist/positional-read.js +36 -0
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +9 -3
- package/dist/publish-copy-stage.d.ts +13 -0
- package/dist/publish-copy-stage.d.ts.map +1 -0
- package/dist/publish-copy-stage.js +47 -0
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +26 -25
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +30 -17
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +10 -5
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +6 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +8 -3
- package/dist/root-context.d.ts +3 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +51 -26
- package/dist/root-directory-list.d.ts +24 -0
- package/dist/root-directory-list.d.ts.map +1 -0
- package/dist/root-directory-list.js +201 -0
- package/dist/root-errors.d.ts +1 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +3 -0
- package/dist/root-file.d.ts +2 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +12 -4
- package/dist/root-impl.d.ts +6 -50
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +342 -208
- package/dist/root-options.d.ts +66 -0
- package/dist/root-options.d.ts.map +1 -0
- package/dist/root-options.js +18 -0
- package/dist/root-path-existing.d.ts +5 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +56 -1
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +74 -162
- package/dist/root-symlink-policy.d.ts +13 -0
- package/dist/root-symlink-policy.d.ts.map +1 -0
- package/dist/root-symlink-policy.js +34 -0
- package/dist/root-walk.d.ts +5 -4
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +99 -50
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +1 -4
- package/dist/root.d.ts +3 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/secret-file.d.ts +1 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +1 -0
- package/dist/secret-read-async.js +5 -5
- package/dist/secure-file.d.ts +1 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +23 -13
- package/dist/sibling-staged-file.js +7 -7
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +16 -7
- package/dist/sidecar-lock-acquire.d.ts +1 -1
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +6 -4
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +5 -3
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +16 -4
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +2 -1
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +3 -2
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +9 -4
- package/dist/timing.d.ts +1 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +25 -6
- package/dist/trash.js +2 -2
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +7 -27
- package/dist/windows-owner.d.ts +9 -12
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +24 -58
- package/dist/write-file-handle.d.ts +6 -0
- package/dist/write-file-handle.d.ts.map +1 -0
- package/dist/write-file-handle.js +25 -0
- package/dist/write-open-flags.d.ts.map +1 -1
- package/dist/write-open-flags.js +1 -2
- package/docs/advanced.md +18 -4
- package/docs/archive.md +57 -9
- package/docs/atomic.md +7 -0
- package/docs/contributing.md +7 -0
- package/docs/copy.md +86 -0
- package/docs/durability.md +17 -2
- package/docs/errors.md +1 -1
- package/docs/file-store.md +48 -3
- package/docs/json-store.md +10 -0
- package/docs/local-roots.md +8 -1
- package/docs/native.md +14 -1
- package/docs/permissions.md +20 -10
- package/docs/positional-read.md +63 -0
- package/docs/reading.md +6 -0
- package/docs/root.md +168 -6
- package/docs/secret-file.md +6 -0
- package/docs/security-model.md +14 -0
- package/docs/sidecar-lock.md +2 -0
- package/docs/timing.md +2 -0
- package/docs/types.md +18 -11
- package/docs/walk.md +54 -5
- package/docs/writing.md +25 -5
- package/package.json +16 -11
package/dist/local-roots.js
CHANGED
|
@@ -2,10 +2,9 @@ import fsSync from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
|
-
import { expandHomePrefix,
|
|
5
|
+
import { expandHomePrefix, resolveRequiredHomeDir } from "./home-dir.js";
|
|
6
6
|
import { isFileUrl, safeFileURLToPath } from "./local-file-access.js";
|
|
7
|
-
import {
|
|
8
|
-
import { resolveRootPathSync } from "./root-path.js";
|
|
7
|
+
import { ROOT_PATH_ALIAS_POLICIES, resolveRootPathSync } from "./root-path.js";
|
|
9
8
|
import { root } from "./root.js";
|
|
10
9
|
function resolveLocalPathInput(input, label) {
|
|
11
10
|
if (isFileUrl(input)) {
|
|
@@ -20,7 +19,14 @@ function resolveLocalPathInput(input, label) {
|
|
|
20
19
|
if (input.includes("\0")) {
|
|
21
20
|
throw new FsSafeError("invalid-path", `${label} must not contain NUL bytes`);
|
|
22
21
|
}
|
|
23
|
-
|
|
22
|
+
const homePrefix = input === "~" || input.startsWith("~/") ||
|
|
23
|
+
(path.sep === "\\" && input.startsWith("~\\"));
|
|
24
|
+
const expanded = homePrefix ? `${resolveRequiredHomeDir()}${path.sep}${input.slice(2)}` : input;
|
|
25
|
+
if (path.isAbsolute(expanded))
|
|
26
|
+
return expanded;
|
|
27
|
+
const drive = path.parse(expanded).root;
|
|
28
|
+
const base = drive ? path.resolve(drive) : process.cwd();
|
|
29
|
+
return `${base}${path.sep}${expanded.slice(drive.length)}`;
|
|
24
30
|
}
|
|
25
31
|
function resolveLocalRootInput(input, label) {
|
|
26
32
|
const trimmed = input.trim();
|
|
@@ -54,7 +60,7 @@ function resolveRootRealSync(rootDir) {
|
|
|
54
60
|
}
|
|
55
61
|
export function resolveLocalPathFromRootsSync(options) {
|
|
56
62
|
const label = options.label ?? "local roots";
|
|
57
|
-
const requestedPath =
|
|
63
|
+
const requestedPath = resolveLocalPathInput(options.filePath, "file path");
|
|
58
64
|
const rootDirs = options.roots.map((rootEntry) => resolveLocalRootInput(rootEntry, label));
|
|
59
65
|
for (const rootDir of rootDirs) {
|
|
60
66
|
const rootReal = resolveRootRealSync(rootDir);
|
|
@@ -69,6 +75,7 @@ export function resolveLocalPathFromRootsSync(options) {
|
|
|
69
75
|
rootCanonicalPath: rootReal,
|
|
70
76
|
boundaryLabel: label,
|
|
71
77
|
rejectUnresolvedSymlinks: true,
|
|
78
|
+
policy: options.requireFile ? ROOT_PATH_ALIAS_POLICIES.unlinkTarget : undefined,
|
|
72
79
|
});
|
|
73
80
|
}
|
|
74
81
|
catch {
|
|
@@ -77,11 +84,12 @@ export function resolveLocalPathFromRootsSync(options) {
|
|
|
77
84
|
if (!candidate.exists && options.allowMissing !== true) {
|
|
78
85
|
continue;
|
|
79
86
|
}
|
|
87
|
+
if (candidate.exists && options.requireFile === true && candidate.kind !== "file")
|
|
88
|
+
continue;
|
|
80
89
|
if (candidate.exists && options.requireFile === true) {
|
|
81
90
|
try {
|
|
82
|
-
if (!fsSync.lstatSync(requestedPath).isFile())
|
|
91
|
+
if (!fsSync.lstatSync(requestedPath).isFile())
|
|
83
92
|
continue;
|
|
84
|
-
}
|
|
85
93
|
}
|
|
86
94
|
catch {
|
|
87
95
|
continue;
|
|
@@ -94,7 +102,7 @@ export function resolveLocalPathFromRootsSync(options) {
|
|
|
94
102
|
export async function readLocalFileFromRoots(options) {
|
|
95
103
|
const maxBytes = normalizeMaxBytes(options.maxBytes);
|
|
96
104
|
const label = options.label ?? "local roots";
|
|
97
|
-
const requestedPath =
|
|
105
|
+
const requestedPath = resolveLocalPathInput(options.filePath, "file path");
|
|
98
106
|
const rootDirs = options.roots.map((rootEntry) => resolveLocalRootInput(rootEntry, label));
|
|
99
107
|
for (const rootDir of rootDirs) {
|
|
100
108
|
let scopedRoot;
|
|
@@ -113,22 +121,12 @@ export async function readLocalFileFromRoots(options) {
|
|
|
113
121
|
if (maxBytes !== undefined) {
|
|
114
122
|
readOptions.maxBytes = maxBytes;
|
|
115
123
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.map((rootPath) => path.relative(rootPath, requestedPath))
|
|
123
|
-
.filter(Boolean);
|
|
124
|
-
for (const relativePath of relativePaths) {
|
|
125
|
-
try {
|
|
126
|
-
const result = await scopedRoot.read(relativePath, readOptions);
|
|
127
|
-
return { ...result, root: scopedRoot.rootReal };
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
// Try the canonical spelling before moving to the next configured root.
|
|
131
|
-
}
|
|
124
|
+
try {
|
|
125
|
+
const result = await scopedRoot.readAbsolute(requestedPath, readOptions);
|
|
126
|
+
return { ...result, root: scopedRoot.rootReal };
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Root handles both trusted spellings; try the next configured root.
|
|
132
130
|
}
|
|
133
131
|
}
|
|
134
132
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"move-path-cleanup.d.ts","sourceRoot":"","sources":["../src/move-path-cleanup.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"move-path-cleanup.d.ts","sourceRoot":"","sources":["../src/move-path-cleanup.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAC3B,CAAC,aAAa,GAAG;IACf,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,mBAAmB,CAAA;KAAE,CAAC,CAAC;IACjE,IAAI,EAAE,WAAW,CAAC;CACnB,CAAC,GACF,CAAC,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEvC,KAAK,wBAAwB,GAAG,SAAS,GAAG,OAAO,CAAC;AAEpD,KAAK,iBAAiB,GAAG;IACvB,QAAQ,EAAE,aAAa,CAAC;IACxB,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5B,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC7C,CAAC;AAEF,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,aAAa,CAUhB;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,aAAa,GAAG,OAAO,CAUxE;AAMD,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,KAAK,CAI5D;AAED,wBAAsB,wBAAwB,CAC5C,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,aAAa,GACtB,OAAO,CAAC,IAAI,CAAC,CAIf;AA0BD,wBAAgB,6BAA6B,CAC3C,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,mBAAmB,GAC5B,uBAAuB,CAuBzB;AAsDD,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,uBAAuB,EAC9B,oBAAoB,EAAE,MAAM,IAAI,GAC/B,OAAO,CAAC,wBAAwB,CAAC,CAyDnC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
export function entryIdentity(stat) {
|
|
@@ -29,7 +30,7 @@ export function sourceChangedError(sourcePath) {
|
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
32
|
export async function assertSourceStillMatches(sourcePath, identity) {
|
|
32
|
-
if (!sameIdentity(identity, entryIdentity(
|
|
33
|
+
if (!sameIdentity(identity, entryIdentity(fsSync.lstatSync(sourcePath)))) {
|
|
33
34
|
throw sourceChangedError(sourcePath);
|
|
34
35
|
}
|
|
35
36
|
}
|
|
@@ -93,7 +94,7 @@ async function observeOwnedAliasUnlink(sourcePath, group) {
|
|
|
93
94
|
}
|
|
94
95
|
let observed;
|
|
95
96
|
try {
|
|
96
|
-
observed =
|
|
97
|
+
observed = fsSync.lstatSync(remainingPath);
|
|
97
98
|
}
|
|
98
99
|
catch (error) {
|
|
99
100
|
if (error?.code === "ENOENT") {
|
|
@@ -118,7 +119,7 @@ export async function cleanupCopiedEntry(sourcePath, manifest, state, assertBefo
|
|
|
118
119
|
}
|
|
119
120
|
let currentStat;
|
|
120
121
|
try {
|
|
121
|
-
currentStat =
|
|
122
|
+
currentStat = fsSync.lstatSync(sourcePath);
|
|
122
123
|
}
|
|
123
124
|
catch (error) {
|
|
124
125
|
if (error?.code === "ENOENT") {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
2
|
+
export declare function createMoveStageOwner(staged: string): Promise<{
|
|
3
|
+
record(created: BigIntStats): void;
|
|
4
|
+
assertCurrent: () => void;
|
|
5
|
+
published(): void;
|
|
6
|
+
cleanup(): Promise<void>;
|
|
7
|
+
}>;
|
|
8
|
+
//# sourceMappingURL=move-path-stage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"move-path-stage.d.ts","sourceRoot":"","sources":["../src/move-path-stage.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAQnD,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,MAAM;oBAkBrC,WAAW,GAAG,IAAI;yBAPV,IAAI;iBAkBf,IAAI;eACA,OAAO,CAAC,IAAI,CAAC;GAajC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fsSync, {} from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
4
|
+
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { guardedRm, guardedRmSync } from "./guarded-mutation.js";
|
|
6
|
+
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
7
|
+
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
8
|
+
export async function createMoveStageOwner(staged) {
|
|
9
|
+
const parent = await createAsyncDirectoryGuard(path.dirname(staged), { bigint: true });
|
|
10
|
+
let identity;
|
|
11
|
+
let unregister;
|
|
12
|
+
const changed = () => new FsSafeError("path-mismatch", "move staging path changed before publication");
|
|
13
|
+
const assertParent = () => {
|
|
14
|
+
const current = fsSync.lstatSync(parent.dir, { bigint: true });
|
|
15
|
+
if (!current.isDirectory() || current.isSymbolicLink() ||
|
|
16
|
+
!sameFileIdentityForCleanup(current, parent.stat) ||
|
|
17
|
+
fsSync.realpathSync.native(parent.dir) !== parent.realPath)
|
|
18
|
+
throw changed();
|
|
19
|
+
};
|
|
20
|
+
const assertCurrent = () => {
|
|
21
|
+
assertParent();
|
|
22
|
+
if (!identity || !sameFileIdentityForCleanup(fsSync.lstatSync(staged, { bigint: true }), identity)) {
|
|
23
|
+
throw changed();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
record(created) {
|
|
28
|
+
if (!sameFileIdentityForCleanup(created, created))
|
|
29
|
+
throw changed();
|
|
30
|
+
identity = created;
|
|
31
|
+
unregister = registerTempPathForExit(staged, {
|
|
32
|
+
identity,
|
|
33
|
+
cleanupSync: () => {
|
|
34
|
+
guardedRmSync({ target: staged, recursive: created.isDirectory(), assertBeforeMutation: assertCurrent });
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
assertCurrent,
|
|
39
|
+
published() { unregister?.(); },
|
|
40
|
+
async cleanup() {
|
|
41
|
+
if (!identity)
|
|
42
|
+
return;
|
|
43
|
+
try {
|
|
44
|
+
await guardedRm({ target: staged, recursive: identity.isDirectory(), assertBeforeMutation: assertCurrent });
|
|
45
|
+
unregister?.();
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error.code === "ENOENT" || error instanceof FsSafeError) {
|
|
49
|
+
unregister?.();
|
|
50
|
+
}
|
|
51
|
+
// Retry only the original receipt after transient cleanup I/O failures.
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
package/dist/move-path.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"move-path.d.ts","sourceRoot":"","sources":["../src/move-path.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"move-path.d.ts","sourceRoot":"","sources":["../src/move-path.ts"],"names":[],"mappings":"AAsBA,MAAM,MAAM,0BAA0B,GAAG,QAAQ,CAAC;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb,CAAC,CAAC;AAEH,MAAM,MAAM,+BAA+B,GAAG;IAC5C,qFAAqF;IACrF,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,uFAAuF;IACvF,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,qFAAqF;IACrF,sBAAsB,CAAC,EAAE,CAAC,OAAO,EAAE,0BAA0B,KAAK,IAAI,CAAC;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IACrC,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,KAAK,sBAAsB,GAAG,cAAc,GAAG,uBAAuB,CAAC;AAEvE,wBAAgB,oCAAoC,CAClD,KAAK,EAAE,OAAO,EACd,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,sBAAsB,GAAG,SAAS,CASpC;AAiRD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CAwGf"}
|
package/dist/move-path.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import fsSync, { constants as fsConstants } from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
5
6
|
import { FsSafeError } from "./errors.js";
|
|
6
7
|
import { guardedRename } from "./guarded-mutation.js";
|
|
7
8
|
import { assertSourceStillMatches, cleanupCopiedEntry, createCleanupCopiedEntryState, entryIdentity, sameIdentity, sourceChangedError, } from "./move-path-cleanup.js";
|
|
8
9
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
9
|
-
import {
|
|
10
|
+
import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
|
|
11
|
+
import { createMoveStageOwner } from "./move-path-stage.js";
|
|
10
12
|
export function moveCopyFallbackReasonForRenameError(error, platform = process.platform) {
|
|
11
13
|
const code = error?.code;
|
|
12
14
|
if (code === "EXDEV") {
|
|
@@ -29,7 +31,7 @@ async function preflightSourceHardlinks(sourcePath) {
|
|
|
29
31
|
let discovered = 1;
|
|
30
32
|
while (pending.length > 0) {
|
|
31
33
|
const current = pending.pop();
|
|
32
|
-
const stat =
|
|
34
|
+
const stat = fsSync.lstatSync(current);
|
|
33
35
|
if (stat.isFile() && stat.nlink > 1) {
|
|
34
36
|
throw hardlinkedSourceError(current);
|
|
35
37
|
}
|
|
@@ -52,21 +54,19 @@ function isSameOrDescendant(parentPath, candidatePath) {
|
|
|
52
54
|
(!path.isAbsolute(relative) && !relative.startsWith(`..${path.sep}`) && relative !== ".."));
|
|
53
55
|
}
|
|
54
56
|
async function assertCopyDestinationOutsideSource(sourcePath, targetPath, expectedIdentity) {
|
|
55
|
-
const sourceStat =
|
|
57
|
+
const sourceStat = fsSync.lstatSync(sourcePath);
|
|
56
58
|
const sourceIdentity = entryIdentity(sourceStat);
|
|
57
59
|
if (expectedIdentity && !sameIdentity(expectedIdentity, sourceIdentity)) {
|
|
58
60
|
throw sourceChangedError(sourcePath);
|
|
59
61
|
}
|
|
60
62
|
const normalizedSource = path.resolve(sourcePath);
|
|
61
63
|
const normalizedTarget = path.resolve(targetPath);
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
fs.realpath(path.dirname(normalizedTarget)),
|
|
65
|
-
]);
|
|
64
|
+
const sourceParentReal = fsSync.realpathSync.native(path.dirname(normalizedSource));
|
|
65
|
+
const targetParentReal = fsSync.realpathSync.native(path.dirname(normalizedTarget));
|
|
66
66
|
const sourceCandidate = path.join(sourceParentReal, path.basename(normalizedSource));
|
|
67
67
|
const targetCandidate = path.join(targetParentReal, path.basename(normalizedTarget));
|
|
68
68
|
const sourceBoundary = sourceStat.isDirectory()
|
|
69
|
-
?
|
|
69
|
+
? fsSync.realpathSync.native(sourcePath)
|
|
70
70
|
: sourceCandidate;
|
|
71
71
|
const unsafeTarget = sourceStat.isDirectory()
|
|
72
72
|
? isSameOrDescendant(sourceBoundary, targetCandidate)
|
|
@@ -98,11 +98,13 @@ async function writeAll(handle, buffer, bytesRead) {
|
|
|
98
98
|
let offset = 0;
|
|
99
99
|
while (offset < bytesRead) {
|
|
100
100
|
const { bytesWritten } = await handle.write(buffer, offset, bytesRead - offset);
|
|
101
|
+
if (bytesWritten <= 0)
|
|
102
|
+
throw new FsSafeError("helper-failed", "move copy made no progress");
|
|
101
103
|
offset += bytesWritten;
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
async function copyRegularFilePinned(params) {
|
|
105
|
-
let
|
|
107
|
+
let openedIdentity;
|
|
106
108
|
let sourceHandle;
|
|
107
109
|
try {
|
|
108
110
|
sourceHandle = await fs.open(params.from, resolveReadOpenFlags());
|
|
@@ -115,16 +117,30 @@ async function copyRegularFilePinned(params) {
|
|
|
115
117
|
throw error;
|
|
116
118
|
}
|
|
117
119
|
try {
|
|
118
|
-
const openedStat =
|
|
120
|
+
const openedStat = fsSync.fstatSync(sourceHandle.fd);
|
|
121
|
+
openedIdentity = entryIdentity(openedStat);
|
|
119
122
|
if (params.rejectHardlinks && openedStat.nlink > 1) {
|
|
120
123
|
throw hardlinkedSourceError(params.from);
|
|
121
124
|
}
|
|
122
|
-
|
|
125
|
+
const openAdvancedWindowsCtime = process.platform === "win32" &&
|
|
126
|
+
params.identity.dev === openedIdentity.dev &&
|
|
127
|
+
params.identity.ino === openedIdentity.ino &&
|
|
128
|
+
params.identity.mode === openedIdentity.mode &&
|
|
129
|
+
params.identity.nlink === openedIdentity.nlink &&
|
|
130
|
+
params.identity.size === openedIdentity.size &&
|
|
131
|
+
params.identity.mtimeMs === openedIdentity.mtimeMs &&
|
|
132
|
+
openedIdentity.ctimeMs >= params.identity.ctimeMs;
|
|
133
|
+
if (!openedStat.isFile() ||
|
|
134
|
+
(!sameIdentity(params.identity, openedIdentity) && !openAdvancedWindowsCtime)) {
|
|
123
135
|
throw sourceChangedError(params.from);
|
|
124
136
|
}
|
|
137
|
+
await assertSourceStillMatches(params.from, openedIdentity);
|
|
138
|
+
const parentGuard = await createAsyncDirectoryGuard(path.dirname(params.to), { bigint: true });
|
|
125
139
|
const destinationHandle = await fs.open(params.to, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, modeBits(params.mode) || 0o666);
|
|
126
|
-
|
|
140
|
+
let destinationIdentity;
|
|
127
141
|
try {
|
|
142
|
+
destinationIdentity = fsSync.fstatSync(destinationHandle.fd, { bigint: true });
|
|
143
|
+
params.onCreated?.(destinationIdentity);
|
|
128
144
|
const scratch = Buffer.allocUnsafe(64 * 1024);
|
|
129
145
|
while (true) {
|
|
130
146
|
const { bytesRead } = await sourceHandle.read(scratch, 0, scratch.length, null);
|
|
@@ -135,37 +151,41 @@ async function copyRegularFilePinned(params) {
|
|
|
135
151
|
}
|
|
136
152
|
// Re-check the opened source before the staged tree can be committed. If
|
|
137
153
|
// it changed while we copied, the caller should retry the move.
|
|
138
|
-
const finalSourceStat =
|
|
154
|
+
const finalSourceStat = fsSync.fstatSync(sourceHandle.fd);
|
|
139
155
|
if (params.rejectHardlinks && finalSourceStat.nlink > 1) {
|
|
140
156
|
throw hardlinkedSourceError(params.from);
|
|
141
157
|
}
|
|
142
|
-
if (!sameIdentity(
|
|
158
|
+
if (!sameIdentity(openedIdentity, entryIdentity(finalSourceStat))) {
|
|
143
159
|
throw sourceChangedError(params.from);
|
|
144
160
|
}
|
|
145
161
|
await destinationHandle.chmod(modeBits(params.mode));
|
|
146
162
|
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
await cleanupPinnedFilePath({
|
|
165
|
+
pathname: params.to, handle: destinationHandle, identity: destinationIdentity, parentGuard,
|
|
166
|
+
});
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
147
169
|
finally {
|
|
148
170
|
await destinationHandle.close();
|
|
149
171
|
}
|
|
150
172
|
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
if (destinationCreated) {
|
|
153
|
-
await fs.rm(params.to, { force: true }).catch(() => undefined);
|
|
154
|
-
}
|
|
155
|
-
throw error;
|
|
156
|
-
}
|
|
157
173
|
finally {
|
|
158
174
|
await sourceHandle.close();
|
|
159
175
|
}
|
|
176
|
+
return openedIdentity;
|
|
160
177
|
}
|
|
161
178
|
async function copyEntryWithManifest(from, to, options, expectedIdentity) {
|
|
162
|
-
const sourceStat =
|
|
179
|
+
const sourceStat = fsSync.lstatSync(from);
|
|
163
180
|
const identity = entryIdentity(sourceStat);
|
|
164
181
|
if (expectedIdentity && !sameIdentity(expectedIdentity, identity)) {
|
|
165
182
|
throw sourceChangedError(from);
|
|
166
183
|
}
|
|
167
184
|
if (sourceStat.isSymbolicLink()) {
|
|
168
|
-
|
|
185
|
+
const target = await fs.readlink(from);
|
|
186
|
+
const targetType = process.platform === "win32" && fsSync.statSync(from).isDirectory() ? "junction" : undefined;
|
|
187
|
+
await fs.symlink(target, to, targetType);
|
|
188
|
+
options.onCreated?.(fsSync.lstatSync(to, { bigint: true }));
|
|
169
189
|
// readlink() is path-based; verify the symlink we copied is still the one
|
|
170
190
|
// we inspected before letting the staged destination become visible.
|
|
171
191
|
await assertSourceStillMatches(from, identity);
|
|
@@ -173,6 +193,7 @@ async function copyEntryWithManifest(from, to, options, expectedIdentity) {
|
|
|
173
193
|
}
|
|
174
194
|
if (sourceStat.isDirectory()) {
|
|
175
195
|
await fs.mkdir(to, { mode: modeBits(sourceStat.mode) || 0o755 });
|
|
196
|
+
options.onCreated?.(fsSync.lstatSync(to, { bigint: true }));
|
|
176
197
|
const children = [];
|
|
177
198
|
const childNames = [];
|
|
178
199
|
const directory = await fs.opendir(from);
|
|
@@ -185,7 +206,9 @@ async function copyEntryWithManifest(from, to, options, expectedIdentity) {
|
|
|
185
206
|
for (const child of childNames) {
|
|
186
207
|
children.push({
|
|
187
208
|
name: child,
|
|
188
|
-
manifest: await copyEntryWithManifest(path.join(from, child), path.join(to, child),
|
|
209
|
+
manifest: await copyEntryWithManifest(path.join(from, child), path.join(to, child), {
|
|
210
|
+
sourceHardlinks: options.sourceHardlinks, budget: options.budget,
|
|
211
|
+
}),
|
|
189
212
|
});
|
|
190
213
|
}
|
|
191
214
|
// Directory traversal is path-based in Node. Treat a changed parent as a
|
|
@@ -202,14 +225,15 @@ async function copyEntryWithManifest(from, to, options, expectedIdentity) {
|
|
|
202
225
|
if (options.sourceHardlinks === "reject" && sourceStat.nlink > 1) {
|
|
203
226
|
throw hardlinkedSourceError(from);
|
|
204
227
|
}
|
|
205
|
-
await copyRegularFilePinned({
|
|
228
|
+
const copiedIdentity = await copyRegularFilePinned({
|
|
206
229
|
from,
|
|
207
230
|
identity,
|
|
208
231
|
mode: sourceStat.mode,
|
|
209
232
|
rejectHardlinks: options.sourceHardlinks === "reject",
|
|
210
233
|
to,
|
|
234
|
+
onCreated: options.onCreated,
|
|
211
235
|
});
|
|
212
|
-
return { ...
|
|
236
|
+
return { ...copiedIdentity, kind: "leaf" };
|
|
213
237
|
}
|
|
214
238
|
function assertSynchronousResult(returned, name) {
|
|
215
239
|
if (returned !== undefined) {
|
|
@@ -286,22 +310,22 @@ export async function movePathWithCopyFallback(options) {
|
|
|
286
310
|
const sourceIdentity = await assertCopyDestinationOutsideSource(sourcePath, targetPath);
|
|
287
311
|
const targetDir = path.dirname(targetPath);
|
|
288
312
|
const staged = path.join(targetDir, `.fs-safe-move-${process.pid}-${randomUUID()}.tmp`);
|
|
289
|
-
const
|
|
313
|
+
const stage = await createMoveStageOwner(staged);
|
|
290
314
|
try {
|
|
291
315
|
const manifest = await copyEntryWithManifest(sourcePath, staged, {
|
|
292
316
|
sourceHardlinks: rejectHardlinks ? "reject" : "allow",
|
|
317
|
+
onCreated: stage.record,
|
|
293
318
|
...(rejectHardlinks ? { budget: { discovered: 1 } } : {}),
|
|
294
319
|
}, sourceIdentity);
|
|
295
320
|
const cleanupState = createCleanupCopiedEntryState(sourcePath, manifest);
|
|
296
|
-
unregisterStaged.setIdentity(await fs.lstat(staged, { bigint: true }));
|
|
297
321
|
await assertCopyDestinationOutsideSource(sourcePath, targetPath, manifest);
|
|
298
322
|
await guardedRename({
|
|
299
323
|
from: staged,
|
|
300
324
|
to: targetPath,
|
|
301
|
-
assertBeforeRename,
|
|
325
|
+
assertBeforeRename: () => { assertBeforeRename(); stage.assertCurrent(); },
|
|
302
326
|
onSourceInspected,
|
|
303
327
|
onRenamed: () => {
|
|
304
|
-
|
|
328
|
+
stage.published();
|
|
305
329
|
onRenamed();
|
|
306
330
|
},
|
|
307
331
|
});
|
|
@@ -312,23 +336,7 @@ export async function movePathWithCopyFallback(options) {
|
|
|
312
336
|
}
|
|
313
337
|
finally {
|
|
314
338
|
if (!destinationPublished) {
|
|
315
|
-
|
|
316
|
-
const stagedIdentity = await fs.lstat(staged, { bigint: true });
|
|
317
|
-
if (!stagedIdentity.isSymbolicLink())
|
|
318
|
-
unregisterStaged.setIdentity(stagedIdentity);
|
|
319
|
-
}
|
|
320
|
-
catch (error) {
|
|
321
|
-
if (error.code === "ENOENT") {
|
|
322
|
-
unregisterStaged();
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
try {
|
|
326
|
-
await fs.rm(staged, { recursive: true, force: true });
|
|
327
|
-
unregisterStaged();
|
|
328
|
-
}
|
|
329
|
-
catch {
|
|
330
|
-
// Keep the identity-bound exit cleanup registered for a later retry.
|
|
331
|
-
}
|
|
339
|
+
await stage.cleanup();
|
|
332
340
|
}
|
|
333
341
|
}
|
|
334
342
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
export declare class MutationAuthorityError extends FsSafeError {
|
|
3
|
+
readonly rejection: unknown;
|
|
4
|
+
constructor(rejection: unknown);
|
|
5
|
+
}
|
|
6
|
+
export declare function composeMutationAssertions(defaultAssertion?: () => void, callAssertion?: () => void): (() => void) | undefined;
|
|
7
|
+
export declare function rethrowMutationAuthorityError(error: unknown): never;
|
|
8
|
+
//# sourceMappingURL=mutation-authority.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutation-authority.d.ts","sourceRoot":"","sources":["../src/mutation-authority.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,qBAAa,sBAAuB,SAAQ,WAAW;IACzC,QAAQ,CAAC,SAAS,EAAE,OAAO;IAAvC,YAAqB,SAAS,EAAE,OAAO,EAEtC;CACF;AAcD,wBAAgB,yBAAyB,CACvC,gBAAgB,CAAC,EAAE,MAAM,IAAI,EAC7B,aAAa,CAAC,EAAE,MAAM,IAAI,GACzB,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAU1B;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAInE"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
export class MutationAuthorityError extends FsSafeError {
|
|
3
|
+
rejection;
|
|
4
|
+
constructor(rejection) {
|
|
5
|
+
super("denied-path", "mutation authority rejected the operation", { cause: rejection });
|
|
6
|
+
this.rejection = rejection;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function assertSynchronous(returned) {
|
|
10
|
+
if (returned !== null &&
|
|
11
|
+
(typeof returned === "object" || typeof returned === "function") &&
|
|
12
|
+
typeof returned.then === "function") {
|
|
13
|
+
// TypeScript permits async functions for () => void. Do not admit their work.
|
|
14
|
+
void Promise.resolve(returned).catch(() => undefined);
|
|
15
|
+
throw new TypeError("assertBeforeMutation must be synchronous");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function composeMutationAssertions(defaultAssertion, callAssertion) {
|
|
19
|
+
if (!defaultAssertion && !callAssertion)
|
|
20
|
+
return undefined;
|
|
21
|
+
return () => {
|
|
22
|
+
try {
|
|
23
|
+
assertSynchronous(defaultAssertion?.());
|
|
24
|
+
assertSynchronous(callAssertion?.());
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new MutationAuthorityError(error);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function rethrowMutationAuthorityError(error) {
|
|
32
|
+
if (error instanceof MutationAuthorityError)
|
|
33
|
+
throw error.rejection;
|
|
34
|
+
// Cleanup failures and indeterminate publication receipts must retain their context.
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
package/dist/native-binding.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TarMeterLimits } from "./archive-limits.js";
|
|
2
2
|
import type { ArchiveMemberKind } from "./archive-plan.js";
|
|
3
|
+
import type { CopyCloneMode } from "./copy-policy.js";
|
|
3
4
|
export interface NativeFileHash {
|
|
4
5
|
bytes: number;
|
|
5
6
|
digest: string;
|
|
@@ -29,6 +30,12 @@ export interface NativeCopyResult {
|
|
|
29
30
|
errorCode?: string;
|
|
30
31
|
errorMessage?: string;
|
|
31
32
|
}
|
|
33
|
+
export interface NativeFileCopyResult {
|
|
34
|
+
fd: number;
|
|
35
|
+
method: "clone" | "copy-file-range" | "copy";
|
|
36
|
+
errorCode?: string;
|
|
37
|
+
errorMessage?: string;
|
|
38
|
+
}
|
|
32
39
|
export interface NativeOpenBeneathResult {
|
|
33
40
|
fd: number;
|
|
34
41
|
containment: "kernel-atomic" | "best-effort";
|
|
@@ -69,9 +76,25 @@ export interface NativeWindowsSecurityFacts {
|
|
|
69
76
|
aces: NativeWindowsAccessControlEntry[];
|
|
70
77
|
}
|
|
71
78
|
export interface NativeBinding {
|
|
79
|
+
/** Windows-only byte transfer; callers retain both admitted descriptors until settlement. */
|
|
80
|
+
copyFileContents?(sourceFd: number, targetFd: number, signal?: AbortSignal): Promise<void>;
|
|
81
|
+
/** Internal: same private, immutable input ownership as the ZIP buffer reader. */
|
|
82
|
+
openTarBufferNative(buffer: Buffer, kind: string, limits: TarMeterLimits, signal?: AbortSignal): Promise<{
|
|
83
|
+
readonly entries: NativeArchiveEntry[];
|
|
84
|
+
readEntry(index: number, maxBytes: number, signal?: AbortSignal): Promise<Buffer>;
|
|
85
|
+
}>;
|
|
86
|
+
/** Internal: input remains private, unpooled and immutable until the reader is released. */
|
|
87
|
+
openZipBufferNative(buffer: Buffer, limits: TarMeterLimits, signal?: AbortSignal): Promise<{
|
|
88
|
+
readonly entries: NativeArchiveEntry[];
|
|
89
|
+
readEntry(index: number, maxBytes: number, signal?: AbortSignal): Promise<Buffer>;
|
|
90
|
+
}>;
|
|
91
|
+
readCloneFileMetadata(paths: string[]): Promise<(Buffer | null)[]>;
|
|
92
|
+
probeTreeClone(parentFd: number): "apfs" | "btrfs" | "refs" | "xfs" | null;
|
|
93
|
+
cloneTree(sourceFd: number | null, parentFd: number, basename: string, concurrency: number, signal?: AbortSignal): Promise<void>;
|
|
72
94
|
createStagedFile?(parentFd: number, basename: string): number;
|
|
73
95
|
stagedFileMatches?(parentFd: number, basename: string, fileFd: number): boolean;
|
|
74
96
|
removeStagedFile?(parentFd: number, basename: string, fileFd: number): "removed" | "name-absent" | "preserved";
|
|
97
|
+
copyFileExclusive?(sourceFd: number, parentFd: number, basename: string, clone: CopyCloneMode, maxBytes: number | undefined, signal: AbortSignal | undefined, sync: boolean): Promise<NativeFileCopyResult>;
|
|
75
98
|
cloneFileExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): number;
|
|
76
99
|
copyFileRangeExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): Promise<NativeCopyResult>;
|
|
77
100
|
createPrivateDirectory(path: string): void;
|
|
@@ -88,6 +111,6 @@ export interface NativeBinding {
|
|
|
88
111
|
removeOwnedTreeSync?(parentFd: number, basename: string, directoryFd: number): NativeOwnedTreeRemovalResult;
|
|
89
112
|
renameNoReplace(sourceRootFd: number, sourceRelPath: string, targetRootFd: number, targetRelPath: string): void;
|
|
90
113
|
renameReplace(sourceRootFd: number, sourceRelPath: string, targetRootFd: number, targetRelPath: string): void;
|
|
91
|
-
sha256File(fd: number): Promise<NativeFileHash>;
|
|
114
|
+
sha256File(fd: number, maxBytes?: number, signal?: AbortSignal): Promise<NativeFileHash>;
|
|
92
115
|
}
|
|
93
116
|
//# sourceMappingURL=native-binding.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-binding.d.ts","sourceRoot":"","sources":["../src/native-binding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"native-binding.d.ts","sourceRoot":"","sources":["../src/native-binding.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,7 +10,7 @@ export type NativeFileHandle = {
|
|
|
10
10
|
};
|
|
11
11
|
export declare function nativeOpenFlags(flags: number): number;
|
|
12
12
|
export declare function writeNativeFd(fd: number, data: Buffer): void;
|
|
13
|
-
export declare function writeNativeInput(fd: number, input: PinnedWriteInput, maxBytes?: number): Promise<void>;
|
|
13
|
+
export declare function writeNativeInput(fd: number, input: PinnedWriteInput, maxBytes?: number, assertBeforeMutation?: () => void): Promise<void>;
|
|
14
14
|
export declare function removeNativeCreatedFileIfStillPinned(params: {
|
|
15
15
|
parentPath: string;
|
|
16
16
|
parentFd: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-operations.d.ts","sourceRoot":"","sources":["../src/native-operations.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAG/D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"native-operations.d.ts","sourceRoot":"","sources":["../src/native-operations.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAG/D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAI1D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,WAAW,EAAE,oBAAoB,CAAC;IAC3C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC;IACvB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,QAAQ,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5E,CAAC;AAEF,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOrD;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAS5D;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,gBAAgB,EACvB,QAAQ,CAAC,EAAE,MAAM,EACjB,oBAAoB,CAAC,EAAE,MAAM,IAAI,GAChC,OAAO,CAAC,IAAI,CAAC,CAoBf;AAsBD,wBAAgB,oCAAoC,CAAC,MAAM,EAAE;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB,GAAG,IAAI,CAqBP;AAED,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAwDvC"}
|
|
@@ -4,6 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
6
6
|
import { getNativeBinding } from "./native.js";
|
|
7
|
+
import { writeAllToFile } from "./write-file-handle.js";
|
|
8
|
+
import { writeCopyFileToFd } from "./copy-file-input.js";
|
|
7
9
|
export function nativeOpenFlags(flags) {
|
|
8
10
|
const closeOnExec = fsSync.constants.O_CLOEXEC;
|
|
9
11
|
return (flags |
|
|
@@ -20,21 +22,25 @@ export function writeNativeFd(fd, data) {
|
|
|
20
22
|
offset += written;
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
|
-
export async function writeNativeInput(fd, input, maxBytes) {
|
|
25
|
+
export async function writeNativeInput(fd, input, maxBytes, assertBeforeMutation) {
|
|
26
|
+
if (input.kind === "file") {
|
|
27
|
+
await writeCopyFileToFd(fd, input, maxBytes, assertBeforeMutation);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
24
30
|
let bytes = 0;
|
|
25
|
-
const write = (data) => {
|
|
31
|
+
const write = async (data) => {
|
|
26
32
|
bytes += data.byteLength;
|
|
27
33
|
if (maxBytes !== undefined && bytes > maxBytes) {
|
|
28
34
|
throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
|
|
29
35
|
}
|
|
30
|
-
|
|
36
|
+
await writeAllToFile(fd, data, { assertBeforeMutation });
|
|
31
37
|
};
|
|
32
38
|
if (input.kind === "buffer") {
|
|
33
|
-
write(typeof input.data === "string" ? Buffer.from(input.data, input.encoding ?? "utf8") : input.data);
|
|
39
|
+
await write(typeof input.data === "string" ? Buffer.from(input.data, input.encoding ?? "utf8") : input.data);
|
|
34
40
|
}
|
|
35
41
|
else {
|
|
36
42
|
for await (const chunk of input.stream) {
|
|
37
|
-
write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
43
|
+
await write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
46
|
}
|