@openclaw/openshell-sandbox 2026.10.1-beta.2 → 2026.10.5-beta.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/dist/index.js +69 -111
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +91 -0
- package/node_modules/@openclaw/fs-safe/README.md +1 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-entry.js +7 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-read.js +5 -4
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +22 -38
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.d.ts +17 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +117 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +38 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +195 -0
- package/node_modules/@openclaw/fs-safe/dist/create.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/create.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/errors.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.js +57 -11
- package/node_modules/@openclaw/fs-safe/dist/guarded-mutation.js +5 -5
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +52 -5
- package/node_modules/@openclaw/fs-safe/dist/guest.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +54 -0
- package/node_modules/@openclaw/fs-safe/dist/native-noreplace.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/native-noreplace.js +55 -0
- package/node_modules/@openclaw/fs-safe/dist/native-operations.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/native-operations.js +22 -26
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +12 -2
- package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +52 -6
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +2 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +17 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +6 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +35 -2
- package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +6 -5
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +6 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-input.d.ts +15 -1
- package/node_modules/@openclaw/fs-safe/dist/root-create-input.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +6 -12
- package/node_modules/@openclaw/fs-safe/dist/root-errors.js +25 -4
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +18 -18
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +4 -2
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +13 -17
- package/node_modules/@openclaw/fs-safe/dist/root-paths.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/root-write-verification.d.ts +10 -1
- package/node_modules/@openclaw/fs-safe/dist/root-write-verification.js +20 -0
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +19 -7
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +18 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +25 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.d.ts +14 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +187 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +15 -1
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.d.ts +10 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-target.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +6 -6
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +4 -4
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +17 -16
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +20 -28
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +27 -4
- package/node_modules/@openclaw/fs-safe/dist/walk.js +2 -13
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +3 -9
- package/node_modules/@openclaw/fs-safe/dist/watch.js +2 -3
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +1 -0
- package/node_modules/@openclaw/fs-safe/docs/archive.md +5 -1
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +11 -3
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/copy.md +109 -1
- package/node_modules/@openclaw/fs-safe/docs/durability.md +50 -5
- package/node_modules/@openclaw/fs-safe/docs/errors.md +37 -0
- package/node_modules/@openclaw/fs-safe/docs/guest.md +21 -4
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +5 -3
- package/node_modules/@openclaw/fs-safe/docs/native.md +39 -6
- package/node_modules/@openclaw/fs-safe/docs/path-scope.md +4 -0
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +3 -1
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +46 -8
- package/node_modules/@openclaw/fs-safe/docs/staged-file.md +10 -3
- package/node_modules/@openclaw/fs-safe/docs/temp.md +12 -0
- package/node_modules/@openclaw/fs-safe/docs/watch.md +6 -3
- package/node_modules/@openclaw/fs-safe/docs/writing.md +40 -11
- package/node_modules/@openclaw/fs-safe/package.json +16 -16
- package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- package/package.json +5 -5
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.d.ts +0 -17
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.js +0 -66
- package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.d.ts +0 -7
- package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.js +0 -16
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { assertSyncDirectoryGuard, captureDirectoryGuard, inspectDirectoryIdentitySync } from "./directory-guard.js";
|
|
4
|
+
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
6
|
+
import { realpathSync } from "./realpath.js";
|
|
7
|
+
import { resolveRootPathSync, ROOT_PATH_ALIAS_POLICIES } from "./root-path.js";
|
|
8
|
+
function admitUncachedCopyPath(input, create = false, admittedRoot) {
|
|
9
|
+
if (!path.isAbsolute(input.rootPath) || !path.isAbsolute(input.absolutePath)) {
|
|
10
|
+
throw new FsSafeError("invalid-path", "copy roots and paths must be absolute");
|
|
11
|
+
}
|
|
12
|
+
const root = admittedRoot ?? captureDirectoryGuard(realpathSync(input.rootPath), "native", { bigint: true });
|
|
13
|
+
// Canonical POSIX callers already own a freshly checked root. Keep the
|
|
14
|
+
// resolver's lexical/symlink walk, but do not rediscover that root each time.
|
|
15
|
+
const rootCanonicalPath = admittedRoot?.realPath;
|
|
16
|
+
const resolve = () => {
|
|
17
|
+
const parent = create ? resolveRootPathSync({
|
|
18
|
+
...input, rootCanonicalPath, absolutePath: path.dirname(input.absolutePath), boundaryLabel: "copy parent",
|
|
19
|
+
rejectSymlinks: true, rejectFinalSymlink: true,
|
|
20
|
+
}) : undefined;
|
|
21
|
+
const selected = resolveRootPathSync({
|
|
22
|
+
...input, rootCanonicalPath, boundaryLabel: "copy", rejectSymlinks: !create, rejectFinalSymlink: !create,
|
|
23
|
+
policy: create ? ROOT_PATH_ALIAS_POLICIES.unlinkTarget : undefined,
|
|
24
|
+
});
|
|
25
|
+
if (parent && path.dirname(selected.canonicalPath) !== parent.canonicalPath) {
|
|
26
|
+
throw new FsSafeError("path-mismatch", "copy destination parent changed");
|
|
27
|
+
}
|
|
28
|
+
return selected;
|
|
29
|
+
};
|
|
30
|
+
const selected = resolve();
|
|
31
|
+
const parent = captureDirectoryGuard(path.dirname(selected.canonicalPath), "native", { bigint: true });
|
|
32
|
+
const assertCurrent = () => {
|
|
33
|
+
assertSyncDirectoryGuard(root);
|
|
34
|
+
assertSyncDirectoryGuard(parent);
|
|
35
|
+
const current = resolve();
|
|
36
|
+
if (current.rootCanonicalPath !== root.realPath || current.canonicalPath !== selected.canonicalPath) {
|
|
37
|
+
throw new FsSafeError("path-mismatch", "copy path changed during operation");
|
|
38
|
+
}
|
|
39
|
+
assertSyncDirectoryGuard(root);
|
|
40
|
+
assertSyncDirectoryGuard(parent);
|
|
41
|
+
};
|
|
42
|
+
assertCurrent();
|
|
43
|
+
return { path: selected.canonicalPath, parent, assertCurrent, rootCanonicalPath: undefined };
|
|
44
|
+
}
|
|
45
|
+
/** Metadata custody lasts only for the owning copy or explicit batch. */
|
|
46
|
+
export function createCopyPathAdmissionCache() {
|
|
47
|
+
const roots = new Map();
|
|
48
|
+
const parents = new Map();
|
|
49
|
+
const directories = new Map();
|
|
50
|
+
const assertParent = ({ root, parent, chain }) => {
|
|
51
|
+
assertSyncDirectoryGuard(root);
|
|
52
|
+
for (const guard of chain) {
|
|
53
|
+
inspectDirectoryIdentitySync(guard.dir, { dev: BigInt(guard.stat.dev), ino: BigInt(guard.stat.ino) });
|
|
54
|
+
}
|
|
55
|
+
assertSyncDirectoryGuard(parent);
|
|
56
|
+
assertSyncDirectoryGuard(root);
|
|
57
|
+
};
|
|
58
|
+
const admit = (input, create = false) => {
|
|
59
|
+
// Keep the public resolver for Windows aliases and noncanonical spellings,
|
|
60
|
+
// including raw dot segments whose traversal must not be normalized away.
|
|
61
|
+
if (process.platform === "win32" || !path.isAbsolute(input.rootPath) ||
|
|
62
|
+
!path.isAbsolute(input.absolutePath) || input.rootPath.includes("\0") || input.absolutePath.includes("\0") ||
|
|
63
|
+
path.resolve(input.rootPath) !== input.rootPath || path.resolve(input.absolutePath) !== input.absolutePath ||
|
|
64
|
+
!input.absolutePath.startsWith(input.rootPath.endsWith(path.sep) ? input.rootPath : `${input.rootPath}${path.sep}`)) {
|
|
65
|
+
return admitUncachedCopyPath(input, create);
|
|
66
|
+
}
|
|
67
|
+
let root = roots.get(input.rootPath);
|
|
68
|
+
if (!root) {
|
|
69
|
+
const canonical = realpathSync(input.rootPath);
|
|
70
|
+
if (canonical !== input.rootPath)
|
|
71
|
+
return admitUncachedCopyPath(input, create);
|
|
72
|
+
root = captureDirectoryGuard(canonical, "native", { bigint: true });
|
|
73
|
+
roots.set(input.rootPath, root);
|
|
74
|
+
}
|
|
75
|
+
assertSyncDirectoryGuard(root);
|
|
76
|
+
const parentPath = path.dirname(input.absolutePath);
|
|
77
|
+
let byParent = parents.get(input.rootPath);
|
|
78
|
+
let admitted = byParent?.get(parentPath);
|
|
79
|
+
if (!admitted) {
|
|
80
|
+
// The first file in each parent retains the existing lexical admission.
|
|
81
|
+
const selected = admitUncachedCopyPath(input, create, root);
|
|
82
|
+
const chain = [];
|
|
83
|
+
let cursor = input.rootPath;
|
|
84
|
+
const relative = path.relative(input.rootPath, parentPath);
|
|
85
|
+
for (const segment of relative ? relative.split(path.sep) : []) {
|
|
86
|
+
cursor = path.join(cursor, segment);
|
|
87
|
+
let guard = directories.get(cursor);
|
|
88
|
+
if (!guard) {
|
|
89
|
+
guard = captureDirectoryGuard(cursor, "native", { bigint: true });
|
|
90
|
+
directories.set(cursor, guard);
|
|
91
|
+
}
|
|
92
|
+
chain.push(guard);
|
|
93
|
+
}
|
|
94
|
+
admitted = { root, parent: chain.at(-1) ?? root, chain };
|
|
95
|
+
assertParent(admitted);
|
|
96
|
+
if (selected.path !== input.absolutePath || selected.parent.realPath !== parentPath ||
|
|
97
|
+
!sameFileIdentity(selected.parent.stat, admitted.parent.stat)) {
|
|
98
|
+
throw new FsSafeError("path-mismatch", "copy path changed during admission");
|
|
99
|
+
}
|
|
100
|
+
byParent ??= new Map();
|
|
101
|
+
byParent.set(parentPath, admitted);
|
|
102
|
+
parents.set(input.rootPath, byParent);
|
|
103
|
+
}
|
|
104
|
+
const selected = admitted;
|
|
105
|
+
const assertCurrent = () => {
|
|
106
|
+
assertParent(selected);
|
|
107
|
+
// No file observation is cached. Preserve the resolver's symlink errors.
|
|
108
|
+
if (!create && fs.lstatSync(input.absolutePath, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
109
|
+
admitUncachedCopyPath(input, create);
|
|
110
|
+
throw new FsSafeError("symlink", "symlink not allowed");
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
assertCurrent();
|
|
114
|
+
return { path: input.absolutePath, parent: selected.parent, assertCurrent, rootCanonicalPath: selected.root.realPath };
|
|
115
|
+
};
|
|
116
|
+
return { admit, clear() { roots.clear(); parents.clear(); directories.clear(); } };
|
|
117
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type CopyPath } from "./copy-path-admission.js";
|
|
2
|
+
import { type CopyCloneMode } from "./copy-policy.js";
|
|
3
|
+
import { type OwnedFileDescriptorSync } from "./create-owned-file.js";
|
|
4
|
+
export type CopyRootFileSyncOptions = {
|
|
5
|
+
source: CopyPath;
|
|
6
|
+
destination: CopyPath;
|
|
7
|
+
expectedSourceIdentity?: {
|
|
8
|
+
dev: bigint;
|
|
9
|
+
ino: bigint;
|
|
10
|
+
};
|
|
11
|
+
clone?: CopyCloneMode;
|
|
12
|
+
maxBytes?: number;
|
|
13
|
+
mode?: number;
|
|
14
|
+
preserveSourceMode?: boolean;
|
|
15
|
+
sourceHardlinks?: "reject" | "allow";
|
|
16
|
+
};
|
|
17
|
+
export type CopiedRootFileSync = OwnedFileDescriptorSync & Readonly<{
|
|
18
|
+
path: string;
|
|
19
|
+
bytes: number;
|
|
20
|
+
method: "clone" | "copy-file-range" | "copy";
|
|
21
|
+
identity: Readonly<{
|
|
22
|
+
dev: bigint;
|
|
23
|
+
ino: bigint;
|
|
24
|
+
}>;
|
|
25
|
+
sourceIdentity: Readonly<{
|
|
26
|
+
dev: bigint;
|
|
27
|
+
ino: bigint;
|
|
28
|
+
}>;
|
|
29
|
+
}>;
|
|
30
|
+
/** Exclusively copies an admitted source and transfers the owned destination fd. */
|
|
31
|
+
export declare function copyRootFileSync(options: CopyRootFileSyncOptions): CopiedRootFileSync;
|
|
32
|
+
export type RootFileCopyBatchSync = {
|
|
33
|
+
copyFile(options: CopyRootFileSyncOptions): CopiedRootFileSync;
|
|
34
|
+
close(): void;
|
|
35
|
+
[Symbol.dispose](): void;
|
|
36
|
+
};
|
|
37
|
+
/** Reuses directory admissions while retaining fresh checks for every copy. */
|
|
38
|
+
export declare function createRootFileCopyBatchSync(): RootFileCopyBatchSync;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import fs, {} from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
4
|
+
import { createCopyPathAdmissionCache } from "./copy-path-admission.js";
|
|
5
|
+
import { resolveCopyCloneMode } from "./copy-policy.js";
|
|
6
|
+
import { createFileWithAdmissionSync } from "./create.js";
|
|
7
|
+
import { ownFileDescriptorSync } from "./create-owned-file.js";
|
|
8
|
+
import { creationAdmissionFromParent, removeRecordedCreationFileSync } from "./creation-boundary.js";
|
|
9
|
+
import { FsSafeError } from "./errors.js";
|
|
10
|
+
import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
|
|
11
|
+
import { copyFileDescriptorSync } from "./file-handle-transfer.js";
|
|
12
|
+
import { captureNativeFdClose } from "./native-binding.js";
|
|
13
|
+
import { getNativeBinding } from "./native.js";
|
|
14
|
+
import { openRootFileSync } from "./root-file.js";
|
|
15
|
+
import { inspectOpenedPathIdentitySync } from "./root-read-admission.js";
|
|
16
|
+
import { openStagedDirectory } from "./staged-directory.js";
|
|
17
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
18
|
+
function copyError(error) {
|
|
19
|
+
if (error instanceof FsSafeError)
|
|
20
|
+
return error;
|
|
21
|
+
const code = error?.code;
|
|
22
|
+
return new FsSafeError(code === "EEXIST" ? "already-exists" : code === "ENOENT" ? "not-found" :
|
|
23
|
+
code === "too-large" ? "too-large" :
|
|
24
|
+
["ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EXDEV", "EINVAL"].includes(code ?? "")
|
|
25
|
+
? "unsupported-platform" : "helper-failed", "guarded synchronous file copy failed", { cause: error });
|
|
26
|
+
}
|
|
27
|
+
/** Exclusively copies an admitted source and transfers the owned destination fd. */
|
|
28
|
+
export function copyRootFileSync(options) {
|
|
29
|
+
return copyRootFileWithAdmissionSync(options, createCopyPathAdmissionCache());
|
|
30
|
+
}
|
|
31
|
+
/** Reuses directory admissions while retaining fresh checks for every copy. */
|
|
32
|
+
export function createRootFileCopyBatchSync() {
|
|
33
|
+
const cache = createCopyPathAdmissionCache();
|
|
34
|
+
let closed = false;
|
|
35
|
+
const close = () => { closed = true; cache.clear(); };
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
copyFile(options) {
|
|
38
|
+
if (closed)
|
|
39
|
+
throw new FsSafeError("invalid-path", "copy batch is closed");
|
|
40
|
+
return copyRootFileWithAdmissionSync(options, cache);
|
|
41
|
+
},
|
|
42
|
+
close,
|
|
43
|
+
[Symbol.dispose]: close,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function copyRootFileWithAdmissionSync(options, cache) {
|
|
47
|
+
// Snapshot caller options before the first filesystem observation.
|
|
48
|
+
const sourceInput = options.source;
|
|
49
|
+
const sourcePath = { rootPath: sourceInput.rootPath, absolutePath: sourceInput.absolutePath };
|
|
50
|
+
const targetInput = options.destination;
|
|
51
|
+
const targetPath = { rootPath: targetInput.rootPath, absolutePath: targetInput.absolutePath };
|
|
52
|
+
const expectedInput = options.expectedSourceIdentity;
|
|
53
|
+
const expectedSourceIdentity = expectedInput === undefined ? undefined : { dev: expectedInput.dev, ino: expectedInput.ino };
|
|
54
|
+
const clone = resolveCopyCloneMode(options.clone, "never");
|
|
55
|
+
const maxBytes = normalizeMaxBytes(options.maxBytes);
|
|
56
|
+
const mode = options.mode;
|
|
57
|
+
const preserveSourceMode = options.preserveSourceMode;
|
|
58
|
+
const sourceHardlinks = options.sourceHardlinks ?? "reject";
|
|
59
|
+
if (mode !== undefined && (!Number.isInteger(mode) || mode < 0 || mode > 0o777)) {
|
|
60
|
+
throw new FsSafeError("invalid-path", "copy mode must be an integer from 0 to 0o777");
|
|
61
|
+
}
|
|
62
|
+
if (sourceHardlinks !== "allow" && sourceHardlinks !== "reject") {
|
|
63
|
+
throw new FsSafeError("invalid-path", "invalid copy source hardlink policy");
|
|
64
|
+
}
|
|
65
|
+
let sourceOwner;
|
|
66
|
+
let targetOwner;
|
|
67
|
+
let parentOwner;
|
|
68
|
+
let parentOpen = false;
|
|
69
|
+
let target;
|
|
70
|
+
let identity;
|
|
71
|
+
const native = getNativeBinding();
|
|
72
|
+
const nativeCopy = process.platform !== "win32" && native?.copyFileExclusiveSync && native.removeStagedFile
|
|
73
|
+
? native.copyFileExclusiveSync.bind(native) : undefined;
|
|
74
|
+
try {
|
|
75
|
+
const source = cache.admit(sourcePath);
|
|
76
|
+
target = cache.admit(targetPath, true);
|
|
77
|
+
// Preserve exclusive-create collision semantics for dangling Windows leaves.
|
|
78
|
+
assertExclusiveCreateLeaf(targetPath.absolutePath);
|
|
79
|
+
if (fs.lstatSync(target.path, { throwIfNoEntry: false })) {
|
|
80
|
+
throw new FsSafeError("already-exists", "copy destination already exists");
|
|
81
|
+
}
|
|
82
|
+
const opened = openRootFileSync({
|
|
83
|
+
...sourcePath, rootRealPath: source.rootCanonicalPath, boundaryLabel: "copy source", maxBytes,
|
|
84
|
+
rejectHardlinks: sourceHardlinks !== "allow",
|
|
85
|
+
});
|
|
86
|
+
if (!opened.ok)
|
|
87
|
+
throw opened.error ?? new FsSafeError("helper-failed", "copy source admission failed");
|
|
88
|
+
sourceOwner = ownFileDescriptorSync(opened.fd);
|
|
89
|
+
const sourceIdentity = inspectFileIdentitySync(() => fs.fstatSync(opened.fd, { bigint: true }), expectedSourceIdentity);
|
|
90
|
+
const verifySource = () => {
|
|
91
|
+
source.assertCurrent();
|
|
92
|
+
for (const inspect of [
|
|
93
|
+
() => fs.fstatSync(opened.fd, { bigint: true }),
|
|
94
|
+
() => inspectOpenedPathIdentitySync(source.path, undefined),
|
|
95
|
+
]) {
|
|
96
|
+
const current = inspectFileIdentitySync(inspect, sourceIdentity);
|
|
97
|
+
if (!current.isFile())
|
|
98
|
+
throw new FsSafeError("not-file", "copy source must be a regular file");
|
|
99
|
+
if (sourceHardlinks !== "allow" && current.nlink > 1n) {
|
|
100
|
+
throw new FsSafeError("hardlink", "copy source must not be hardlinked");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const selectedMode = mode ?? (preserveSourceMode ? Number(sourceIdentity.mode & 511n) : 0o600 & ~process.umask());
|
|
105
|
+
let copied;
|
|
106
|
+
verifySource();
|
|
107
|
+
target.assertCurrent();
|
|
108
|
+
if (nativeCopy) {
|
|
109
|
+
const closeNative = captureNativeFdClose(native);
|
|
110
|
+
const parent = openStagedDirectory(target.parent.realPath);
|
|
111
|
+
parentOwner = ownFileDescriptorSync(parent.fd);
|
|
112
|
+
parentOpen = true;
|
|
113
|
+
inspectFileIdentitySync(() => fs.fstatSync(parent.fd, { bigint: true }), { dev: BigInt(target.parent.stat.dev), ino: BigInt(target.parent.stat.ino) });
|
|
114
|
+
target.assertCurrent();
|
|
115
|
+
copied = nativeCopy(opened.fd, parent.fd, path.basename(target.path), clone, maxBytes !== undefined && Number.isFinite(maxBytes) ? maxBytes : undefined);
|
|
116
|
+
targetOwner = ownFileDescriptorSync(copied.fd, closeNative);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
if (clone === "always")
|
|
120
|
+
throw new FsSafeError("unsupported-platform", "synchronous native file cloning is unavailable");
|
|
121
|
+
targetOwner = createFileWithAdmissionSync(target.path, { mode: 0o600, assertBeforeMutation: target.assertCurrent }, creationAdmissionFromParent(target.parent));
|
|
122
|
+
}
|
|
123
|
+
identity = inspectFileIdentitySync(() => fs.fstatSync(targetOwner.fd, { bigint: true }));
|
|
124
|
+
const verifyTarget = () => {
|
|
125
|
+
target.assertCurrent();
|
|
126
|
+
const current = inspectFileIdentitySync(() => fs.fstatSync(targetOwner.fd, { bigint: true }), identity);
|
|
127
|
+
const named = inspectFileIdentitySync(() => inspectOpenedPathIdentitySync(target.path, undefined), identity);
|
|
128
|
+
if (!current.isFile() || current.nlink !== 1n || !named.isFile() || named.nlink !== 1n) {
|
|
129
|
+
throw new FsSafeError("path-mismatch", "copy destination changed");
|
|
130
|
+
}
|
|
131
|
+
return current;
|
|
132
|
+
};
|
|
133
|
+
verifyTarget();
|
|
134
|
+
if (copied?.errorCode) {
|
|
135
|
+
throw copyError(Object.assign(new Error(copied.errorMessage), { code: copied.errorCode }));
|
|
136
|
+
}
|
|
137
|
+
const method = copied ? copied.method : "copy";
|
|
138
|
+
if (method !== "clone" && method !== "copy-file-range" && method !== "copy") {
|
|
139
|
+
throw new FsSafeError("helper-failed", "native copy returned an unknown method");
|
|
140
|
+
}
|
|
141
|
+
if (!copied) {
|
|
142
|
+
copyFileDescriptorSync(opened.fd, targetOwner.fd, { maxBytes });
|
|
143
|
+
}
|
|
144
|
+
verifySource();
|
|
145
|
+
const completed = verifyTarget();
|
|
146
|
+
if (maxBytes !== undefined && Number.isFinite(maxBytes) && completed.size > BigInt(maxBytes)) {
|
|
147
|
+
throw new FsSafeError("too-large", "copy exceeds maxBytes");
|
|
148
|
+
}
|
|
149
|
+
fs.fchmodSync(targetOwner.fd, selectedMode);
|
|
150
|
+
verifySource();
|
|
151
|
+
verifyTarget();
|
|
152
|
+
sourceOwner.close();
|
|
153
|
+
parentOpen = false;
|
|
154
|
+
parentOwner?.close();
|
|
155
|
+
return Object.freeze({
|
|
156
|
+
...targetOwner, path: target.path, bytes: Number(completed.size), method,
|
|
157
|
+
identity: Object.freeze({ dev: identity.dev, ino: identity.ino }),
|
|
158
|
+
sourceIdentity: Object.freeze({ dev: sourceIdentity.dev, ino: sourceIdentity.ino }),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
const primary = copyError(error);
|
|
163
|
+
const cleanup = [];
|
|
164
|
+
if (targetOwner && target) {
|
|
165
|
+
try {
|
|
166
|
+
if (parentOpen) {
|
|
167
|
+
const removed = native.removeStagedFile(parentOwner.fd, path.basename(target.path), targetOwner.fd);
|
|
168
|
+
if (removed === "preserved")
|
|
169
|
+
throw new FsSafeError("path-mismatch", "copy cleanup preserved a replacement");
|
|
170
|
+
}
|
|
171
|
+
else if (identity) {
|
|
172
|
+
removeRecordedCreationFileSync(target.path, identity, target.assertCurrent);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (failure) {
|
|
176
|
+
cleanup.push(failure);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const owner of [targetOwner, sourceOwner, parentOwner]) {
|
|
180
|
+
try {
|
|
181
|
+
owner?.close();
|
|
182
|
+
}
|
|
183
|
+
catch (failure) {
|
|
184
|
+
cleanup.push(failure);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (cleanup.length) {
|
|
188
|
+
throw new FsSafeError(primary.code, primary.message, {
|
|
189
|
+
cause: new AggregateError([primary, ...cleanup], "copy and cleanup failed"),
|
|
190
|
+
details: { path: target?.path, cleanup: "failed" },
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
throw primary;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -4,6 +4,7 @@ import { type CreationPermissions, type CreationParentIdentity } from "./creatio
|
|
|
4
4
|
export type { OwnedFileDescriptorSync } from "./create-owned-file.js";
|
|
5
5
|
export type CreateFileOptions = CreateDirectoryOptions;
|
|
6
6
|
export declare function createFileSync(targetPath: string, options?: CreateFileOptions): OwnedFileDescriptorSync;
|
|
7
|
+
export declare function createFileWithAdmissionSync(targetPath: string, options?: CreateFileOptions, admission?: CreationAdmission): OwnedFileDescriptorSync;
|
|
7
8
|
/** Internal async writer; Windows preparation uses the nonblocking stage owner. */
|
|
8
9
|
export declare function createFileHandle(targetPath: string, options?: CreateFileOptions, admission?: CreationAdmission): Promise<FileHandle>;
|
|
9
10
|
export type CreateDirectoryOptions = CreationPermissions & {
|
|
@@ -108,9 +108,12 @@ function createPrivateWindowsFile(selected, options, mode) {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
export function createFileSync(targetPath, options = {}) {
|
|
111
|
+
return createFileWithAdmissionSync(targetPath, options);
|
|
112
|
+
}
|
|
113
|
+
export function createFileWithAdmissionSync(targetPath, options = {}, admission = {}) {
|
|
111
114
|
const permissions = resolveCreationPermissions(options, false);
|
|
112
115
|
const assertion = options.assertBeforeMutation;
|
|
113
|
-
const selected = prepareCreationPath(targetPath);
|
|
116
|
+
const selected = prepareCreationPath(targetPath, admission.expectedParentIdentity);
|
|
114
117
|
if (permissions.private && process.platform === "win32") {
|
|
115
118
|
assertPrivateFileCreationAvailable();
|
|
116
119
|
return createPrivateWindowsFile(selected, { ...permissions, assertBeforeMutation: assertion }, permissions.mode);
|
|
@@ -2,6 +2,7 @@ import fs, {} from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { assertDirectoryIdentitySync } from "./directory-guard.js";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { noReplaceUnavailable } from "./native-noreplace.js";
|
|
5
6
|
import { assertSynchronousCallbackResult } from "./mutation-authority.js";
|
|
6
7
|
import { captureNativeFdClose } from "./native-binding.js";
|
|
7
8
|
import { requireNativeBinding } from "./native.js";
|
|
@@ -120,7 +121,8 @@ class Publication {
|
|
|
120
121
|
}
|
|
121
122
|
transition = native.outcome; // Capture BEFORE diagnostics, observations or close.
|
|
122
123
|
if (native.errorCode || transition !== "committed") {
|
|
123
|
-
|
|
124
|
+
const error = Object.assign(new Error(native.errorMessage ?? "entry publication did not commit"), { code: native.errorCode ?? "helper-failed" });
|
|
125
|
+
throw noReplaceUnavailable(error, "retained entry publication") ?? error;
|
|
124
126
|
}
|
|
125
127
|
phase = "postcheck";
|
|
126
128
|
verification = "failed";
|
|
@@ -12,4 +12,6 @@ export declare class FsSafeError extends Error {
|
|
|
12
12
|
details?: FsSafeErrorDetails;
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
+
/** A rejected no-replace rename capability, not a generic missing native helper. */
|
|
16
|
+
export declare function isNoReplaceUnsupported(error: unknown): error is FsSafeError;
|
|
15
17
|
export {};
|
|
@@ -25,3 +25,8 @@ export class FsSafeError extends Error {
|
|
|
25
25
|
this.details = options.details;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
/** A rejected no-replace rename capability, not a generic missing native helper. */
|
|
29
|
+
export function isNoReplaceUnsupported(error) {
|
|
30
|
+
return error instanceof FsSafeError && error.code === "helper-unavailable" &&
|
|
31
|
+
error.details?.capability === "rename-noreplace";
|
|
32
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import type { FileLockSyncAcquireOptions, FileLockSyncHandle } from "./file-lock-sync.js";
|
|
2
2
|
import type { Root } from "./root-impl.js";
|
|
3
|
+
import { type FileLockSyncRootPath } from "./file-lock-sync-root.js";
|
|
4
|
+
import { type FileLockSyncRootFileReceipt } from "./file-lock-sync-root-io.js";
|
|
5
|
+
export declare function cleanupCreatedRootSyncLock(lockRootPath: FileLockSyncRootPath, fd: number, receipt: FileLockSyncRootFileReceipt, timer?: NodeJS.Timeout): void;
|
|
3
6
|
export declare function acquireFileLockSyncWithRoot<TPayload extends Record<string, unknown>>(targetPath: string, inputOptions: FileLockSyncAcquireOptions<TPayload>, lockRoot: Root): FileLockSyncHandle;
|
|
@@ -10,10 +10,40 @@ import { SyncLockAcquisition } from "./file-lock-sync-acquisition.js";
|
|
|
10
10
|
import { assertSynchronousCallbackResult } from "./mutation-authority.js";
|
|
11
11
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
12
12
|
import { admitFileLockSyncRootPath, assertFileLockSyncRootPathsCurrent, captureFileLockSyncRootAuthority, normalizeFileLockSyncTargetWithRoot, } from "./file-lock-sync-root.js";
|
|
13
|
-
import { cleanupCreatedRootSyncLock, FileLockSyncRootArbitrationCollision, tryReuseCurrentRootSyncHeldLock, } from "./file-lock-sync-root-arbitration.js";
|
|
14
13
|
import { fileLockSyncRootSnapshotStillCurrent, } from "./file-lock-sync-root-io.js";
|
|
15
14
|
import { createFileLockSyncRootDirectory, createFileLockSyncRootFile, fileLockSyncRootGuardExists, fileLockSyncRootDirectoryReceiptStillCurrent, refreshFileLockSyncRootFileReceipt, removeFileLockSyncRootDirectory, removeFileLockSyncRootFile, } from "./file-lock-sync-root-mutation.js";
|
|
16
|
-
import { createRootSyncHeldLockHandle, ensureRootSyncExitCleanupRegistered, getRootSyncHeldLocks, readRootSidecarSnapshotSync, } from "./file-lock-sync-root-held.js";
|
|
15
|
+
import { createRootSyncHeldLockHandle, ensureRootSyncExitCleanupRegistered, getRootSyncHeldLocks, readRootSidecarSnapshotSync, verifyRootSyncHeldLock, } from "./file-lock-sync-root-held.js";
|
|
16
|
+
class FileLockSyncRootArbitrationCollision extends FsSafeError {
|
|
17
|
+
constructor() {
|
|
18
|
+
super("path-mismatch", "file lock arbitration changed during local creation");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function cleanupCreatedRootSyncLock(lockRootPath, fd, receipt, timer) {
|
|
22
|
+
let timerCleanupFailed = false;
|
|
23
|
+
let timerCleanupError;
|
|
24
|
+
try {
|
|
25
|
+
if (timer)
|
|
26
|
+
clearInterval(timer);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
timerCleanupFailed = true;
|
|
30
|
+
timerCleanupError = error;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
fs.closeSync(fd);
|
|
34
|
+
if (!removeFileLockSyncRootFile(lockRootPath, receipt)) {
|
|
35
|
+
throw new FsSafeError("path-mismatch", "created sidecar lock changed before cleanup");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (fileCleanupError) {
|
|
39
|
+
if (timerCleanupFailed) {
|
|
40
|
+
throw createSuppressedError(timerCleanupError, fileCleanupError, "unpublished lock timer and file cleanup both failed");
|
|
41
|
+
}
|
|
42
|
+
throw fileCleanupError;
|
|
43
|
+
}
|
|
44
|
+
if (timerCleanupFailed)
|
|
45
|
+
throw timerCleanupError;
|
|
46
|
+
}
|
|
17
47
|
export function acquireFileLockSyncWithRoot(targetPath, inputOptions, lockRoot) {
|
|
18
48
|
// The public dispatcher already read lockRoot exactly once. Validate and
|
|
19
49
|
// snapshot that genuine Root before any remaining option or retry getter can
|
|
@@ -37,23 +67,39 @@ export function acquireFileLockSyncWithRoot(targetPath, inputOptions, lockRoot)
|
|
|
37
67
|
const heldLocks = getRootSyncHeldLocks();
|
|
38
68
|
const currentTargetHolder = () => heldLocks.get(normalizedTargetPath) ??
|
|
39
69
|
foreignSyncHeldLock("root", normalizedTargetPath);
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
70
|
+
const tryReuseCurrentHeld = () => {
|
|
71
|
+
const held = heldLocks.get(normalizedTargetPath);
|
|
72
|
+
const reusable = held && !foreignSyncHeldLock("root", normalizedTargetPath) &&
|
|
73
|
+
options.reentrantOwner !== undefined && held.reentrantOwner !== undefined &&
|
|
74
|
+
options.reentrantOwner === held.reentrantOwner &&
|
|
75
|
+
authority.adapter === held.rootAuthority.adapter &&
|
|
76
|
+
lockRootPath.relativePath === held.rootPath.relativePath &&
|
|
77
|
+
path.relative(lockRootPath.path, held.rootPath.path) === "" &&
|
|
78
|
+
held.releaseState !== "released";
|
|
79
|
+
if (!reusable)
|
|
80
|
+
return undefined;
|
|
81
|
+
if (!verifyRootSyncHeldLock(held)) {
|
|
82
|
+
throw new FsSafeError("path-mismatch", "held sidecar lock changed before reentrant reuse");
|
|
83
|
+
}
|
|
84
|
+
// The parser can reenter acquisition; recheck the exact held entry before reuse.
|
|
85
|
+
if (heldLocks.get(normalizedTargetPath) !== held || held.releaseState === "released" ||
|
|
86
|
+
foreignSyncHeldLock("root", normalizedTargetPath)) {
|
|
87
|
+
throw new FsSafeError("path-mismatch", "held sidecar lock changed during reentrant reuse");
|
|
88
|
+
}
|
|
89
|
+
held.refCount += 1;
|
|
90
|
+
held.revision += 1;
|
|
91
|
+
return createRootSyncHeldLockHandle(held);
|
|
92
|
+
};
|
|
47
93
|
if (heldLocks.has(normalizedTargetPath) && !foreignSyncHeldLock("root", normalizedTargetPath)) {
|
|
48
94
|
assertFileLockSyncRootPathsCurrent(guardedPaths);
|
|
49
|
-
const initiallyReusable =
|
|
95
|
+
const initiallyReusable = tryReuseCurrentHeld();
|
|
50
96
|
if (initiallyReusable)
|
|
51
97
|
return initiallyReusable;
|
|
52
98
|
}
|
|
53
99
|
const acquisition = new SyncLockAcquisition(lockPath, normalizedTargetPath, options.retry, options.timeoutMs);
|
|
54
100
|
let ownedReclaimGuard;
|
|
55
101
|
let reclaimCleanupAttempted = false;
|
|
56
|
-
const reuseCurrentHeld = () => ownedReclaimGuard ? undefined :
|
|
102
|
+
const reuseCurrentHeld = () => ownedReclaimGuard ? undefined : tryReuseCurrentHeld();
|
|
57
103
|
const releaseReclaimGuard = () => {
|
|
58
104
|
const receipt = ownedReclaimGuard;
|
|
59
105
|
if (!receipt)
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import fsSync, {} from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { assertDirectoryGuard, assertSyncDirectoryGuard, createAsyncDirectoryGuard, createSyncDirectoryGuard, } from "./directory-guard.js";
|
|
5
5
|
export async function withAsyncDirectoryGuards(guards, mutate, options = {}) {
|
|
6
6
|
for (const guard of guards) {
|
|
7
|
-
|
|
7
|
+
assertDirectoryGuard(guard, "native");
|
|
8
8
|
}
|
|
9
9
|
const result = await mutate();
|
|
10
10
|
if (options.verifyAfter !== false) {
|
|
11
11
|
try {
|
|
12
12
|
for (const guard of guards) {
|
|
13
|
-
|
|
13
|
+
assertDirectoryGuard(guard, "native");
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
catch (error) {
|
|
@@ -46,11 +46,11 @@ export async function guardedRename(params) {
|
|
|
46
46
|
const sourceGuard = await createAsyncDirectoryGuard(path.dirname(params.from));
|
|
47
47
|
const targetGuard = await createAsyncDirectoryGuard(path.dirname(params.to));
|
|
48
48
|
await withAsyncDirectoryGuards([sourceGuard, targetGuard], async () => {
|
|
49
|
+
// Authority must survive all awaited guards; do not yield before rename dispatch.
|
|
50
|
+
params.assertBeforeRename?.();
|
|
49
51
|
if (params.onSourceInspected) {
|
|
50
52
|
params.onSourceInspected(fsSync.lstatSync(params.from, { bigint: true }));
|
|
51
53
|
}
|
|
52
|
-
// Authority must survive all awaited guards; do not yield before rename dispatch.
|
|
53
|
-
params.assertBeforeRename?.();
|
|
54
54
|
await fs.rename(params.from, params.to);
|
|
55
55
|
params.onRenamed?.();
|
|
56
56
|
});
|