@openclaw/fs-safe 0.7.2 → 0.8.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 +71 -0
- package/README.md +5 -1
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +9 -10
- package/dist/archive-merge.d.ts +18 -0
- package/dist/archive-merge.d.ts.map +1 -0
- package/dist/archive-merge.js +157 -0
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +25 -5
- package/dist/archive-policy.d.ts +1 -1
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +1 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +3 -2
- package/dist/archive-staging.d.ts +13 -8
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +44 -192
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +9 -5
- package/dist/archive-tar-runtime.d.ts +1 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-zip-admission.d.ts +2 -1
- package/dist/archive-zip-admission.d.ts.map +1 -1
- package/dist/archive-zip-admission.js +2 -2
- package/dist/archive-zip-directory.d.ts +8 -1
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +6 -2
- package/dist/archive-zip-entry.d.ts +1 -1
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-names.d.ts +1 -1
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +1 -0
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +17 -25
- package/dist/directory-guard.d.ts +17 -5
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +8 -6
- package/dist/directory-mode-node.d.ts +8 -0
- package/dist/directory-mode-node.d.ts.map +1 -0
- package/dist/directory-mode-node.js +93 -0
- package/dist/directory-mode-owner.d.ts +21 -0
- package/dist/directory-mode-owner.d.ts.map +1 -0
- package/dist/directory-mode-owner.js +73 -0
- package/dist/file-observation.d.ts +1 -1
- package/dist/file-observation.d.ts.map +1 -1
- package/dist/file-store-boundary.d.ts +2 -0
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +13 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +8 -11
- package/dist/filename.d.ts +6 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +36 -0
- package/dist/guarded-mkdir.d.ts +2 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +2 -2
- package/dist/guarded-mutation.d.ts +2 -2
- package/dist/guarded-mutation.d.ts.map +1 -1
- 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 +2 -0
- package/dist/json-durable-queue-ownership.d.ts +3 -1
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +7 -4
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +25 -28
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +18 -56
- package/dist/move-path-cleanup.d.ts +43 -0
- package/dist/move-path-cleanup.d.ts.map +1 -0
- package/dist/move-path-cleanup.js +159 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +30 -84
- package/dist/native-operations.d.ts +2 -4
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +7 -9
- package/dist/native-pinned-write-windows.d.ts +2 -2
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +5 -5
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +14 -4
- package/dist/native-staged-file.d.ts +2 -2
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/opened-file-failure.d.ts +1 -0
- package/dist/opened-file-failure.d.ts.map +1 -1
- package/dist/opened-file-failure.js +4 -0
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +9 -3
- package/dist/pinned-write.d.ts +2 -2
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +32 -13
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +7 -3
- package/dist/regular-file.d.ts +1 -1
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +35 -6
- package/dist/replace-file-descriptor.d.ts +9 -1
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +58 -10
- package/dist/replace-file-temp-owner.d.ts +7 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +29 -0
- package/dist/replace-file.d.ts +1 -0
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +12 -38
- package/dist/root-context.d.ts +4 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +19 -0
- package/dist/root-impl.d.ts +43 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +13 -11
- package/dist/root-path-symlink.d.ts +6 -2
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +7 -6
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +6 -2
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +4 -0
- package/dist/root-write-verification.d.ts +3 -2
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +6 -0
- package/dist/secret-file.d.ts +9 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +136 -74
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +9 -3
- package/dist/sidecar-lock-acquire.d.ts +1 -0
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +24 -5
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +6 -4
- package/dist/sidecar-lock-types.d.ts +7 -0
- package/dist/sidecar-lock-types.d.ts.map +1 -1
- package/dist/sidecar-lock.d.ts +2 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +44 -3
- package/dist/write-open-flags.d.ts +5 -0
- package/dist/write-open-flags.d.ts.map +1 -0
- package/dist/write-open-flags.js +35 -0
- package/docs/advanced.md +1 -1
- package/docs/archive.md +56 -7
- package/docs/atomic.md +8 -2
- package/docs/contributing.md +13 -0
- package/docs/durability.md +3 -2
- package/docs/errors.md +3 -1
- package/docs/file-store.md +38 -8
- package/docs/local-roots.md +4 -3
- package/docs/output.md +4 -1
- package/docs/private-file-store.md +11 -2
- package/docs/reading.md +3 -1
- package/docs/regular-file.md +16 -2
- package/docs/root.md +4 -2
- package/docs/secret-file.md +37 -2
- package/docs/sidecar-lock.md +33 -5
- package/docs/store.md +5 -1
- package/docs/temp.md +19 -1
- package/docs/writing.md +24 -3
- package/package.json +14 -10
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import fs, {} from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
4
|
+
import { syncDirectory } from "./directory-durability.js";
|
|
4
5
|
import { syncQueueDirectoryCreation } from "./json-durable-queue-directory.js";
|
|
5
6
|
import { acknowledgeDurableQueueEntry, claimDurableQueueEntry, completeDeliveredQueueEntry, moveDurableQueueEntryToFailed, } from "./json-durable-queue-ownership.js";
|
|
6
7
|
import { stringifyJsonDocument } from "./json-stringify.js";
|
|
7
8
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
8
|
-
import {
|
|
9
|
+
import { replaceFileAtomicWithDirectorySync } from "./replace-file.js";
|
|
9
10
|
import { assertSafePathSegment } from "./safe-path-segment.js";
|
|
10
11
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
11
12
|
export const DEFAULT_JSON_DURABLE_QUEUE_ENTRY_MAX_BYTES = 16 * 1024 * 1024;
|
|
@@ -209,24 +210,13 @@ async function chmodQueueDirectory(dir) {
|
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
212
|
export async function writeJsonDurableQueueEntry(params) {
|
|
212
|
-
await
|
|
213
|
+
await replaceFileAtomicWithDirectorySync({
|
|
213
214
|
filePath: params.filePath,
|
|
214
215
|
content: stringifyJsonDocument(params.entry, null, 2),
|
|
215
216
|
mode: 0o600,
|
|
216
217
|
tempPrefix: params.tempPrefix,
|
|
217
218
|
syncTempFile: true,
|
|
218
|
-
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
async function replaceJsonDurableQueueEntry(params) {
|
|
222
|
-
await replaceFileAtomic({
|
|
223
|
-
filePath: params.filePath,
|
|
224
|
-
content: stringifyJsonDocument(params.entry, null, 2),
|
|
225
|
-
mode: 0o600,
|
|
226
|
-
tempPrefix: params.tempPrefix,
|
|
227
|
-
syncTempFile: true,
|
|
228
|
-
syncParentDir: true,
|
|
229
|
-
});
|
|
219
|
+
}, syncDirectory);
|
|
230
220
|
}
|
|
231
221
|
async function inspectQueueEntry(inspect, maxBytes, expected) {
|
|
232
222
|
let inspectionFailed = false;
|
|
@@ -303,7 +293,7 @@ export async function loadJsonDurableQueueEntry(params) {
|
|
|
303
293
|
});
|
|
304
294
|
const result = params.read ? await params.read(raw, params.paths.jsonPath) : { entry: raw };
|
|
305
295
|
if (result.migrated) {
|
|
306
|
-
await
|
|
296
|
+
await writeJsonDurableQueueEntry({
|
|
307
297
|
filePath: claimedPath,
|
|
308
298
|
entry: result.entry,
|
|
309
299
|
tempPrefix: params.tempPrefix,
|
|
@@ -350,6 +340,12 @@ export async function loadPendingJsonDurableQueueEntries(options) {
|
|
|
350
340
|
if (!suffix)
|
|
351
341
|
continue;
|
|
352
342
|
const id = file.slice(0, -suffix.length);
|
|
343
|
+
try {
|
|
344
|
+
assertSafeQueueEntryId(id);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
353
349
|
if (seenIds.has(id))
|
|
354
350
|
continue;
|
|
355
351
|
seenIds.add(id);
|
|
@@ -357,25 +353,26 @@ export async function loadPendingJsonDurableQueueEntries(options) {
|
|
|
357
353
|
}
|
|
358
354
|
const entries = [];
|
|
359
355
|
for (const id of ids) {
|
|
356
|
+
const paths = resolveJsonDurableQueueEntryPaths(options.queueDir, id);
|
|
357
|
+
const claimedPath = await claimDurableQueueEntry(paths, { skipUnowned: true });
|
|
358
|
+
if (!claimedPath)
|
|
359
|
+
continue;
|
|
360
|
+
let result;
|
|
360
361
|
try {
|
|
361
|
-
const paths = resolveJsonDurableQueueEntryPaths(options.queueDir, id);
|
|
362
|
-
const claimedPath = await claimDurableQueueEntry(paths);
|
|
363
|
-
if (!claimedPath)
|
|
364
|
-
continue;
|
|
365
362
|
const raw = await readJsonDurableQueueEntry(claimedPath, { maxBytes: options.maxBytes });
|
|
366
|
-
|
|
367
|
-
if (result.migrated) {
|
|
368
|
-
await replaceJsonDurableQueueEntry({
|
|
369
|
-
filePath: claimedPath,
|
|
370
|
-
entry: result.entry,
|
|
371
|
-
tempPrefix: options.tempPrefix,
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
entries.push(result.entry);
|
|
363
|
+
result = options.read ? await options.read(raw, paths.jsonPath) : { entry: raw };
|
|
375
364
|
}
|
|
376
365
|
catch {
|
|
377
366
|
continue;
|
|
378
367
|
}
|
|
368
|
+
if (result.migrated) {
|
|
369
|
+
await writeJsonDurableQueueEntry({
|
|
370
|
+
filePath: claimedPath,
|
|
371
|
+
entry: result.entry,
|
|
372
|
+
tempPrefix: options.tempPrefix,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
entries.push(result.entry);
|
|
379
376
|
}
|
|
380
377
|
return entries;
|
|
381
378
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-roots.d.ts","sourceRoot":"","sources":["../src/local-roots.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"local-roots.d.ts","sourceRoot":"","sources":["../src/local-roots.ts"],"names":[],"mappings":"AAQA,OAAO,EAAQ,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAE3F,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,UAAU,GAAG;IAC9C,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,oCAAoC,GAAG,sBAAsB,GAAG;IAC1E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG,sBAAsB,GAAG;IACnE,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B,CAAC;AAgDF,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,oCAAoC,GAC5C,oBAAoB,GAAG,IAAI,CAuC7B;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CA4CtC"}
|
package/dist/local-roots.js
CHANGED
|
@@ -5,6 +5,7 @@ import { FsSafeError } from "./errors.js";
|
|
|
5
5
|
import { expandHomePrefix, resolveUserPath } from "./home-dir.js";
|
|
6
6
|
import { isFileUrl, safeFileURLToPath } from "./local-file-access.js";
|
|
7
7
|
import { isPathInside } from "./path.js";
|
|
8
|
+
import { resolveRootPathSync } from "./root-path.js";
|
|
8
9
|
import { root } from "./root.js";
|
|
9
10
|
function resolveLocalPathInput(input, label) {
|
|
10
11
|
if (isFileUrl(input)) {
|
|
@@ -37,9 +38,6 @@ function resolveLocalRootInput(input, label) {
|
|
|
37
38
|
}
|
|
38
39
|
return path.resolve(resolved);
|
|
39
40
|
}
|
|
40
|
-
function isPathInsideRoot(candidate, rootDir) {
|
|
41
|
-
return isPathInside(rootDir, candidate);
|
|
42
|
-
}
|
|
43
41
|
function resolveRootRealSync(rootDir) {
|
|
44
42
|
try {
|
|
45
43
|
// Configured roots may themselves be symlinks. Follow only this trusted
|
|
@@ -54,53 +52,6 @@ function resolveRootRealSync(rootDir) {
|
|
|
54
52
|
return null;
|
|
55
53
|
}
|
|
56
54
|
}
|
|
57
|
-
function resolveCandidateCanonicalSync(filePath) {
|
|
58
|
-
let sawExistingLeaf = false;
|
|
59
|
-
try {
|
|
60
|
-
const stat = fsSync.lstatSync(filePath);
|
|
61
|
-
sawExistingLeaf = true;
|
|
62
|
-
return {
|
|
63
|
-
exists: true,
|
|
64
|
-
canonicalPath: fsSync.realpathSync(filePath),
|
|
65
|
-
isFile: stat.isFile(),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
catch (err) {
|
|
69
|
-
if (err.code !== "ENOENT") {
|
|
70
|
-
throw err;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (sawExistingLeaf) {
|
|
74
|
-
// lstat succeeded but realpath failed: this is an existing dangling
|
|
75
|
-
// symlink, not a missing path callers may safely create through.
|
|
76
|
-
throw new FsSafeError("symlink", "local roots candidate is a dangling symlink");
|
|
77
|
-
}
|
|
78
|
-
let cursor = filePath;
|
|
79
|
-
const missingSegments = [];
|
|
80
|
-
while (true) {
|
|
81
|
-
const parent = path.dirname(cursor);
|
|
82
|
-
if (parent === cursor) {
|
|
83
|
-
return { exists: false, canonicalPath: filePath };
|
|
84
|
-
}
|
|
85
|
-
missingSegments.unshift(path.basename(cursor));
|
|
86
|
-
cursor = parent;
|
|
87
|
-
try {
|
|
88
|
-
fsSync.lstatSync(cursor);
|
|
89
|
-
const ancestorReal = fsSync.realpathSync(cursor);
|
|
90
|
-
return {
|
|
91
|
-
exists: false,
|
|
92
|
-
canonicalPath: path.join(ancestorReal, ...missingSegments),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
catch (err) {
|
|
96
|
-
if (err.code !== "ENOENT") {
|
|
97
|
-
// Existing ancestors that cannot be canonicalized are symlink/error
|
|
98
|
-
// terrain; do not reconstruct a trusted missing path through them.
|
|
99
|
-
throw err;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
55
|
export function resolveLocalPathFromRootsSync(options) {
|
|
105
56
|
const label = options.label ?? "local roots";
|
|
106
57
|
const requestedPath = path.resolve(resolveLocalPathInput(options.filePath, "file path"));
|
|
@@ -112,7 +63,13 @@ export function resolveLocalPathFromRootsSync(options) {
|
|
|
112
63
|
}
|
|
113
64
|
let candidate;
|
|
114
65
|
try {
|
|
115
|
-
candidate =
|
|
66
|
+
candidate = resolveRootPathSync({
|
|
67
|
+
absolutePath: requestedPath,
|
|
68
|
+
rootPath: rootDir,
|
|
69
|
+
rootCanonicalPath: rootReal,
|
|
70
|
+
boundaryLabel: label,
|
|
71
|
+
rejectUnresolvedSymlinks: true,
|
|
72
|
+
});
|
|
116
73
|
}
|
|
117
74
|
catch {
|
|
118
75
|
continue;
|
|
@@ -120,12 +77,17 @@ export function resolveLocalPathFromRootsSync(options) {
|
|
|
120
77
|
if (!candidate.exists && options.allowMissing !== true) {
|
|
121
78
|
continue;
|
|
122
79
|
}
|
|
123
|
-
if (candidate.exists && options.requireFile === true
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
80
|
+
if (candidate.exists && options.requireFile === true) {
|
|
81
|
+
try {
|
|
82
|
+
if (!fsSync.lstatSync(requestedPath).isFile()) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
128
89
|
}
|
|
90
|
+
return { path: candidate.canonicalPath, root: rootReal };
|
|
129
91
|
}
|
|
130
92
|
return null;
|
|
131
93
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type EntryIdentity = {
|
|
2
|
+
ctimeMs: number;
|
|
3
|
+
dev: number;
|
|
4
|
+
ino: number;
|
|
5
|
+
mode: number;
|
|
6
|
+
mtimeMs: number;
|
|
7
|
+
nlink: number;
|
|
8
|
+
size: number;
|
|
9
|
+
};
|
|
10
|
+
export type CopiedEntryManifest = (EntryIdentity & {
|
|
11
|
+
children: Array<{
|
|
12
|
+
name: string;
|
|
13
|
+
manifest: CopiedEntryManifest;
|
|
14
|
+
}>;
|
|
15
|
+
kind: "directory";
|
|
16
|
+
}) | (EntryIdentity & {
|
|
17
|
+
kind: "leaf";
|
|
18
|
+
});
|
|
19
|
+
type CleanupCopiedEntryResult = "removed" | "stale";
|
|
20
|
+
type CleanupAliasGroup = {
|
|
21
|
+
expected: EntryIdentity;
|
|
22
|
+
remainingPaths: Set<string>;
|
|
23
|
+
stale: boolean;
|
|
24
|
+
};
|
|
25
|
+
export type CleanupCopiedEntryState = {
|
|
26
|
+
aliasGroups: Map<string, CleanupAliasGroup>;
|
|
27
|
+
};
|
|
28
|
+
export declare function entryIdentity(stat: {
|
|
29
|
+
ctimeMs: number;
|
|
30
|
+
dev: number;
|
|
31
|
+
ino: number;
|
|
32
|
+
mode: number;
|
|
33
|
+
mtimeMs: number;
|
|
34
|
+
nlink: number;
|
|
35
|
+
size: number;
|
|
36
|
+
}): EntryIdentity;
|
|
37
|
+
export declare function sameIdentity(a: EntryIdentity, b: EntryIdentity): boolean;
|
|
38
|
+
export declare function sourceChangedError(sourcePath: string): Error;
|
|
39
|
+
export declare function assertSourceStillMatches(sourcePath: string, identity: EntryIdentity): Promise<void>;
|
|
40
|
+
export declare function createCleanupCopiedEntryState(sourcePath: string, manifest: CopiedEntryManifest): CleanupCopiedEntryState;
|
|
41
|
+
export declare function cleanupCopiedEntry(sourcePath: string, manifest: CopiedEntryManifest, state: CleanupCopiedEntryState): Promise<CleanupCopiedEntryResult>;
|
|
42
|
+
export {};
|
|
43
|
+
//# sourceMappingURL=move-path-cleanup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"move-path-cleanup.d.ts","sourceRoot":"","sources":["../src/move-path-cleanup.ts"],"names":[],"mappings":"AAGA,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,GAC7B,OAAO,CAAC,wBAAwB,CAAC,CAkDnC"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function entryIdentity(stat) {
|
|
4
|
+
return {
|
|
5
|
+
ctimeMs: stat.ctimeMs,
|
|
6
|
+
dev: stat.dev,
|
|
7
|
+
ino: stat.ino,
|
|
8
|
+
mode: stat.mode,
|
|
9
|
+
mtimeMs: stat.mtimeMs,
|
|
10
|
+
nlink: stat.nlink,
|
|
11
|
+
size: stat.size,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function sameIdentity(a, b) {
|
|
15
|
+
return (a.dev === b.dev &&
|
|
16
|
+
a.ino === b.ino &&
|
|
17
|
+
a.mode === b.mode &&
|
|
18
|
+
a.nlink === b.nlink &&
|
|
19
|
+
a.size === b.size &&
|
|
20
|
+
a.mtimeMs === b.mtimeMs &&
|
|
21
|
+
a.ctimeMs === b.ctimeMs);
|
|
22
|
+
}
|
|
23
|
+
function sameDirectoryNode(a, b) {
|
|
24
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
25
|
+
}
|
|
26
|
+
export function sourceChangedError(sourcePath) {
|
|
27
|
+
return Object.assign(new Error(`Source changed during move fallback: ${sourcePath}`), {
|
|
28
|
+
code: "ESTALE",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export async function assertSourceStillMatches(sourcePath, identity) {
|
|
32
|
+
if (!sameIdentity(identity, entryIdentity(await fs.lstat(sourcePath)))) {
|
|
33
|
+
throw sourceChangedError(sourcePath);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function identityKey(identity) {
|
|
37
|
+
return `${identity.dev}:${identity.ino}`;
|
|
38
|
+
}
|
|
39
|
+
function collectAliasCandidates(sourcePath, manifest, candidates) {
|
|
40
|
+
if (manifest.kind === "directory") {
|
|
41
|
+
for (const child of manifest.children) {
|
|
42
|
+
collectAliasCandidates(path.join(sourcePath, child.name), child.manifest, candidates);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (manifest.nlink <= 1) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const key = identityKey(manifest);
|
|
50
|
+
const entries = candidates.get(key) ?? [];
|
|
51
|
+
entries.push({ manifest, path: sourcePath });
|
|
52
|
+
candidates.set(key, entries);
|
|
53
|
+
}
|
|
54
|
+
export function createCleanupCopiedEntryState(sourcePath, manifest) {
|
|
55
|
+
const candidates = new Map();
|
|
56
|
+
collectAliasCandidates(sourcePath, manifest, candidates);
|
|
57
|
+
const aliasGroups = new Map();
|
|
58
|
+
for (const [key, entries] of candidates) {
|
|
59
|
+
if (entries.length < 2) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const first = entries[0];
|
|
63
|
+
if (!first || entries.some((entry) => !sameIdentity(first.manifest, entry.manifest))) {
|
|
64
|
+
throw sourceChangedError(sourcePath);
|
|
65
|
+
}
|
|
66
|
+
aliasGroups.set(key, {
|
|
67
|
+
expected: first.manifest,
|
|
68
|
+
remainingPaths: new Set(entries.map((entry) => entry.path)),
|
|
69
|
+
stale: false,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return { aliasGroups };
|
|
73
|
+
}
|
|
74
|
+
function sameOwnedUnlinkTransition(before, after) {
|
|
75
|
+
return (before.dev === after.dev &&
|
|
76
|
+
before.ino === after.ino &&
|
|
77
|
+
before.mode === after.mode &&
|
|
78
|
+
before.size === after.size &&
|
|
79
|
+
before.mtimeMs === after.mtimeMs &&
|
|
80
|
+
before.nlink > 0 &&
|
|
81
|
+
after.nlink === before.nlink - 1 &&
|
|
82
|
+
after.ctimeMs >= before.ctimeMs);
|
|
83
|
+
}
|
|
84
|
+
function poisonAliasGroup(group) {
|
|
85
|
+
group.stale = true;
|
|
86
|
+
return "stale";
|
|
87
|
+
}
|
|
88
|
+
async function observeOwnedAliasUnlink(sourcePath, group) {
|
|
89
|
+
group.remainingPaths.delete(sourcePath);
|
|
90
|
+
const remainingPath = group.remainingPaths.values().next().value;
|
|
91
|
+
if (!remainingPath) {
|
|
92
|
+
return "removed";
|
|
93
|
+
}
|
|
94
|
+
let observed;
|
|
95
|
+
try {
|
|
96
|
+
observed = await fs.lstat(remainingPath);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (error?.code === "ENOENT") {
|
|
100
|
+
return poisonAliasGroup(group);
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
const observedIdentity = entryIdentity(observed);
|
|
105
|
+
if (!sameOwnedUnlinkTransition(group.expected, observedIdentity)) {
|
|
106
|
+
return poisonAliasGroup(group);
|
|
107
|
+
}
|
|
108
|
+
group.expected = observedIdentity;
|
|
109
|
+
return "removed";
|
|
110
|
+
}
|
|
111
|
+
function mergeCleanupResults(a, b) {
|
|
112
|
+
return a === "stale" || b === "stale" ? "stale" : "removed";
|
|
113
|
+
}
|
|
114
|
+
export async function cleanupCopiedEntry(sourcePath, manifest, state) {
|
|
115
|
+
const aliasGroup = manifest.kind === "leaf" ? state.aliasGroups.get(identityKey(manifest)) : undefined;
|
|
116
|
+
if (aliasGroup?.stale) {
|
|
117
|
+
return "stale";
|
|
118
|
+
}
|
|
119
|
+
let currentStat;
|
|
120
|
+
try {
|
|
121
|
+
currentStat = await fs.lstat(sourcePath);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error?.code === "ENOENT") {
|
|
125
|
+
return aliasGroup ? poisonAliasGroup(aliasGroup) : "removed";
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
if (manifest.kind === "directory") {
|
|
130
|
+
if (!currentStat.isDirectory() || !sameDirectoryNode(manifest, entryIdentity(currentStat))) {
|
|
131
|
+
return "stale";
|
|
132
|
+
}
|
|
133
|
+
// A same-inode directory can gain unrelated children after commit. Still
|
|
134
|
+
// clean manifest children so the fallback does not duplicate copied files.
|
|
135
|
+
let result = "removed";
|
|
136
|
+
for (const child of manifest.children) {
|
|
137
|
+
result = mergeCleanupResults(result, await cleanupCopiedEntry(path.join(sourcePath, child.name), child.manifest, state));
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
await fs.rmdir(sourcePath);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const code = error?.code;
|
|
144
|
+
if (code === "ENOTEMPTY" || code === "EEXIST") {
|
|
145
|
+
return "stale";
|
|
146
|
+
}
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
const expected = aliasGroup?.expected ?? manifest;
|
|
152
|
+
if (!sameIdentity(expected, entryIdentity(currentStat))) {
|
|
153
|
+
return aliasGroup ? poisonAliasGroup(aliasGroup) : "stale";
|
|
154
|
+
}
|
|
155
|
+
await fs.unlink(sourcePath);
|
|
156
|
+
return aliasGroup
|
|
157
|
+
? await observeOwnedAliasUnlink(sourcePath, aliasGroup)
|
|
158
|
+
: "removed";
|
|
159
|
+
}
|
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":"AAoBA,MAAM,MAAM,+BAA+B,GAAG;IAC5C,uFAAuF;IACvF,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,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;AA8OD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CAkFf"}
|
package/dist/move-path.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { FsSafeError } from "./errors.js";
|
|
6
6
|
import { guardedRename } from "./guarded-mutation.js";
|
|
7
|
+
import { assertSourceStillMatches, cleanupCopiedEntry, createCleanupCopiedEntryState, entryIdentity, sameIdentity, sourceChangedError, } from "./move-path-cleanup.js";
|
|
7
8
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
8
9
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
9
10
|
export function moveCopyFallbackReasonForRenameError(error, platform = process.platform) {
|
|
@@ -50,51 +51,34 @@ function isSameOrDescendant(parentPath, candidatePath) {
|
|
|
50
51
|
return (relative === "" ||
|
|
51
52
|
(!path.isAbsolute(relative) && !relative.startsWith(`..${path.sep}`) && relative !== ".."));
|
|
52
53
|
}
|
|
53
|
-
async function assertCopyDestinationOutsideSource(sourcePath, targetPath) {
|
|
54
|
-
const
|
|
54
|
+
async function assertCopyDestinationOutsideSource(sourcePath, targetPath, expectedIdentity) {
|
|
55
|
+
const sourceStat = await fs.lstat(sourcePath);
|
|
56
|
+
const sourceIdentity = entryIdentity(sourceStat);
|
|
57
|
+
if (expectedIdentity && !sameIdentity(expectedIdentity, sourceIdentity)) {
|
|
58
|
+
throw sourceChangedError(sourcePath);
|
|
59
|
+
}
|
|
60
|
+
const normalizedSource = path.resolve(sourcePath);
|
|
55
61
|
const normalizedTarget = path.resolve(targetPath);
|
|
56
|
-
const targetParentReal = await
|
|
62
|
+
const [sourceParentReal, targetParentReal] = await Promise.all([
|
|
63
|
+
fs.realpath(path.dirname(normalizedSource)),
|
|
64
|
+
fs.realpath(path.dirname(normalizedTarget)),
|
|
65
|
+
]);
|
|
66
|
+
const sourceCandidate = path.join(sourceParentReal, path.basename(normalizedSource));
|
|
57
67
|
const targetCandidate = path.join(targetParentReal, path.basename(normalizedTarget));
|
|
58
|
-
|
|
68
|
+
const sourceBoundary = sourceStat.isDirectory()
|
|
69
|
+
? await fs.realpath(sourcePath)
|
|
70
|
+
: sourceCandidate;
|
|
71
|
+
const unsafeTarget = sourceStat.isDirectory()
|
|
72
|
+
? isSameOrDescendant(sourceBoundary, targetCandidate)
|
|
73
|
+
: path.relative(sourceBoundary, targetCandidate) === "";
|
|
74
|
+
if (unsafeTarget) {
|
|
59
75
|
throw new FsSafeError("invalid-path", "Move destination must not be inside the source");
|
|
60
76
|
}
|
|
61
|
-
|
|
62
|
-
function entryIdentity(stat) {
|
|
63
|
-
return {
|
|
64
|
-
ctimeMs: stat.ctimeMs,
|
|
65
|
-
dev: stat.dev,
|
|
66
|
-
ino: stat.ino,
|
|
67
|
-
mode: stat.mode,
|
|
68
|
-
mtimeMs: stat.mtimeMs,
|
|
69
|
-
nlink: stat.nlink,
|
|
70
|
-
size: stat.size,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
function sameIdentity(a, b) {
|
|
74
|
-
return (a.dev === b.dev &&
|
|
75
|
-
a.ino === b.ino &&
|
|
76
|
-
a.mode === b.mode &&
|
|
77
|
-
a.nlink === b.nlink &&
|
|
78
|
-
a.size === b.size &&
|
|
79
|
-
a.mtimeMs === b.mtimeMs &&
|
|
80
|
-
a.ctimeMs === b.ctimeMs);
|
|
81
|
-
}
|
|
82
|
-
function sameDirectoryNode(a, b) {
|
|
83
|
-
return a.dev === b.dev && a.ino === b.ino;
|
|
77
|
+
return sourceIdentity;
|
|
84
78
|
}
|
|
85
79
|
function modeBits(mode) {
|
|
86
80
|
return mode & 0o777;
|
|
87
81
|
}
|
|
88
|
-
function sourceChangedError(sourcePath) {
|
|
89
|
-
return Object.assign(new Error(`Source changed during move fallback: ${sourcePath}`), {
|
|
90
|
-
code: "ESTALE",
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
async function assertSourceStillMatches(sourcePath, identity) {
|
|
94
|
-
if (!sameIdentity(identity, entryIdentity(await fs.lstat(sourcePath)))) {
|
|
95
|
-
throw sourceChangedError(sourcePath);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
82
|
async function chmodDirectoryPinned(directoryPath, mode) {
|
|
99
83
|
if (process.platform === "win32") {
|
|
100
84
|
// Node cannot portably open a Windows directory for descriptor-bound
|
|
@@ -174,9 +158,12 @@ async function copyRegularFilePinned(params) {
|
|
|
174
158
|
await sourceHandle.close();
|
|
175
159
|
}
|
|
176
160
|
}
|
|
177
|
-
async function copyEntryWithManifest(from, to, options) {
|
|
161
|
+
async function copyEntryWithManifest(from, to, options, expectedIdentity) {
|
|
178
162
|
const sourceStat = await fs.lstat(from);
|
|
179
163
|
const identity = entryIdentity(sourceStat);
|
|
164
|
+
if (expectedIdentity && !sameIdentity(expectedIdentity, identity)) {
|
|
165
|
+
throw sourceChangedError(from);
|
|
166
|
+
}
|
|
180
167
|
if (sourceStat.isSymbolicLink()) {
|
|
181
168
|
await fs.symlink(await fs.readlink(from), to);
|
|
182
169
|
// readlink() is path-based; verify the symlink we copied is still the one
|
|
@@ -224,48 +211,6 @@ async function copyEntryWithManifest(from, to, options) {
|
|
|
224
211
|
});
|
|
225
212
|
return { ...identity, kind: "leaf" };
|
|
226
213
|
}
|
|
227
|
-
function mergeCleanupResults(a, b) {
|
|
228
|
-
return a === "stale" || b === "stale" ? "stale" : "removed";
|
|
229
|
-
}
|
|
230
|
-
async function cleanupCopiedEntry(sourcePath, manifest) {
|
|
231
|
-
let currentStat;
|
|
232
|
-
try {
|
|
233
|
-
currentStat = await fs.lstat(sourcePath);
|
|
234
|
-
}
|
|
235
|
-
catch (error) {
|
|
236
|
-
if (error?.code === "ENOENT") {
|
|
237
|
-
return "removed";
|
|
238
|
-
}
|
|
239
|
-
throw error;
|
|
240
|
-
}
|
|
241
|
-
if (manifest.kind === "directory") {
|
|
242
|
-
if (!currentStat.isDirectory() || !sameDirectoryNode(manifest, entryIdentity(currentStat))) {
|
|
243
|
-
return "stale";
|
|
244
|
-
}
|
|
245
|
-
// A same-inode directory can gain unrelated children after commit. Still
|
|
246
|
-
// clean manifest children so the fallback does not duplicate copied files.
|
|
247
|
-
let result = "removed";
|
|
248
|
-
for (const child of manifest.children) {
|
|
249
|
-
result = mergeCleanupResults(result, await cleanupCopiedEntry(path.join(sourcePath, child.name), child.manifest));
|
|
250
|
-
}
|
|
251
|
-
try {
|
|
252
|
-
await fs.rmdir(sourcePath);
|
|
253
|
-
}
|
|
254
|
-
catch (error) {
|
|
255
|
-
const code = error?.code;
|
|
256
|
-
if (code === "ENOTEMPTY" || code === "EEXIST") {
|
|
257
|
-
return "stale";
|
|
258
|
-
}
|
|
259
|
-
throw error;
|
|
260
|
-
}
|
|
261
|
-
return result;
|
|
262
|
-
}
|
|
263
|
-
if (!sameIdentity(manifest, entryIdentity(currentStat))) {
|
|
264
|
-
return "stale";
|
|
265
|
-
}
|
|
266
|
-
await fs.unlink(sourcePath);
|
|
267
|
-
return "removed";
|
|
268
|
-
}
|
|
269
214
|
export async function movePathWithCopyFallback(options) {
|
|
270
215
|
// Keep the initiating owner's callback across preparation and copy fallback.
|
|
271
216
|
const callerAssert = options.assertBeforeRename;
|
|
@@ -307,7 +252,7 @@ export async function movePathWithCopyFallback(options) {
|
|
|
307
252
|
// Commit a fresh inode through the copy path so a post-scan hardlink can
|
|
308
253
|
// never become the published target; the copy loop fences nlink again.
|
|
309
254
|
}
|
|
310
|
-
await assertCopyDestinationOutsideSource(sourcePath, targetPath);
|
|
255
|
+
const sourceIdentity = await assertCopyDestinationOutsideSource(sourcePath, targetPath);
|
|
311
256
|
const targetDir = path.dirname(targetPath);
|
|
312
257
|
const staged = path.join(targetDir, `.fs-safe-move-${process.pid}-${randomUUID()}.tmp`);
|
|
313
258
|
const unregisterStaged = registerTempPathForExit(staged, { recursive: true });
|
|
@@ -316,13 +261,14 @@ export async function movePathWithCopyFallback(options) {
|
|
|
316
261
|
const manifest = await copyEntryWithManifest(sourcePath, staged, {
|
|
317
262
|
sourceHardlinks: options.sourceHardlinks ?? "allow",
|
|
318
263
|
...(rejectHardlinks ? { budget: { discovered: 1 } } : {}),
|
|
319
|
-
});
|
|
264
|
+
}, sourceIdentity);
|
|
265
|
+
const cleanupState = createCleanupCopiedEntryState(sourcePath, manifest);
|
|
320
266
|
unregisterStaged.setIdentity(await fs.lstat(staged, { bigint: true }));
|
|
321
|
-
await assertCopyDestinationOutsideSource(sourcePath, targetPath);
|
|
267
|
+
await assertCopyDestinationOutsideSource(sourcePath, targetPath, manifest);
|
|
322
268
|
await guardedRename({ from: staged, to: targetPath, assertBeforeRename });
|
|
323
269
|
stagedCommitted = true;
|
|
324
270
|
unregisterStaged();
|
|
325
|
-
const cleanupResult = await cleanupCopiedEntry(sourcePath, manifest);
|
|
271
|
+
const cleanupResult = await cleanupCopiedEntry(sourcePath, manifest, cleanupState);
|
|
326
272
|
if (cleanupResult === "stale") {
|
|
327
273
|
throw sourceChangedError(sourcePath);
|
|
328
274
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { type Stats } from "node:fs";
|
|
1
|
+
import { type BigIntStats, type Stats } from "node:fs";
|
|
2
2
|
import type { ContainmentGuarantee } from "./containment.js";
|
|
3
|
-
import { type NativeBinding } from "./native.js";
|
|
4
3
|
import type { PinnedWriteInput } from "./pinned-write.js";
|
|
5
4
|
export type NativeFileHandle = {
|
|
6
5
|
readonly fd: number;
|
|
@@ -13,11 +12,10 @@ export declare function nativeOpenFlags(flags: number): number;
|
|
|
13
12
|
export declare function writeNativeFd(fd: number, data: Buffer): void;
|
|
14
13
|
export declare function writeNativeInput(fd: number, input: PinnedWriteInput, maxBytes?: number): Promise<void>;
|
|
15
14
|
export declare function removeNativeCreatedFileIfStillPinned(params: {
|
|
16
|
-
binding: NativeBinding;
|
|
17
15
|
parentPath: string;
|
|
18
16
|
parentFd: number;
|
|
19
17
|
basename: string;
|
|
20
|
-
created?:
|
|
18
|
+
created?: BigIntStats;
|
|
21
19
|
}): void;
|
|
22
20
|
export declare function createNativeExclusiveFile(targetPath: string, mode: number): Promise<NativeFileHandle | undefined>;
|
|
23
21
|
export declare function syncNativeFileBestEffort(fd: number): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-operations.d.ts","sourceRoot":"","sources":["../src/native-operations.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,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;AAE1D,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,GAChB,OAAO,CAAC,IAAI,CAAC,CAgBf;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;AAED,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAQzD"}
|
|
@@ -62,14 +62,13 @@ export function removeNativeCreatedFileIfStillPinned(params) {
|
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
64
|
try {
|
|
65
|
-
const parentPathStat = fsSync.lstatSync(params.parentPath);
|
|
66
|
-
const parentFdStat =
|
|
65
|
+
const parentPathStat = fsSync.lstatSync(params.parentPath, { bigint: true });
|
|
66
|
+
const parentFdStat = fsSync.fstatSync(params.parentFd, { bigint: true });
|
|
67
67
|
const targetPath = path.join(params.parentPath, params.basename);
|
|
68
|
-
const target = fsSync.lstatSync(targetPath);
|
|
69
|
-
if (!parentPathStat.isSymbolicLink() &&
|
|
70
|
-
parentPathStat
|
|
71
|
-
|
|
72
|
-
!target.isSymbolicLink() &&
|
|
68
|
+
const target = fsSync.lstatSync(targetPath, { bigint: true });
|
|
69
|
+
if (!parentPathStat.isSymbolicLink() && parentPathStat.isDirectory() &&
|
|
70
|
+
sameFileIdentityForCleanup(parentPathStat, parentFdStat) &&
|
|
71
|
+
!target.isSymbolicLink() && target.isFile() &&
|
|
73
72
|
sameFileIdentityForCleanup(target, params.created)) {
|
|
74
73
|
fsSync.rmSync(targetPath);
|
|
75
74
|
}
|
|
@@ -107,7 +106,7 @@ export async function createNativeExclusiveFile(targetPath, mode) {
|
|
|
107
106
|
}
|
|
108
107
|
fd = opened.fd;
|
|
109
108
|
fsSync.fchmodSync(fd, mode);
|
|
110
|
-
created = fsSync.fstatSync(fd);
|
|
109
|
+
created = fsSync.fstatSync(fd, { bigint: true });
|
|
111
110
|
return wrapNativeFd(fd, opened.containment);
|
|
112
111
|
}
|
|
113
112
|
catch (error) {
|
|
@@ -119,7 +118,6 @@ export async function createNativeExclusiveFile(targetPath, mode) {
|
|
|
119
118
|
// Preserve the original error.
|
|
120
119
|
}
|
|
121
120
|
removeNativeCreatedFileIfStillPinned({
|
|
122
|
-
binding,
|
|
123
121
|
parentPath,
|
|
124
122
|
parentFd: parent.fd,
|
|
125
123
|
basename,
|