@openclaw/fs-safe 0.18.2 → 0.20.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 +47 -0
- package/README.md +15 -5
- package/dist/advanced.d.ts +2 -0
- package/dist/advanced.js +1 -0
- package/dist/archive-durability.js +1 -1
- package/dist/archive-merge.js +1 -1
- package/dist/archive-plan.d.ts +2 -7
- package/dist/archive-read.js +9 -18
- package/dist/archive-staging.js +3 -1
- package/dist/archive-zip-entry.d.ts +11 -11
- package/dist/archive-zip-entry.js +3 -35
- package/dist/archive-zip-integrity.d.ts +2 -2
- package/dist/archive-zip-integrity.js +2 -12
- package/dist/archive-zip-loader.d.ts +7 -3
- package/dist/archive-zip-loader.js +10 -9
- package/dist/archive-zip-preflight.d.ts +2 -1
- package/dist/archive-zip-preflight.js +16 -7
- package/dist/archive.js +17 -16
- package/dist/directory-receipt.js +5 -7
- package/dist/effective-uid.js +1 -4
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +3 -2
- package/dist/file-lock-sync-root-held.d.ts +4 -11
- package/dist/file-lock-sync-root-held.js +1 -4
- package/dist/file-lock-sync-root-io.d.ts +1 -4
- package/dist/file-lock-sync-root.d.ts +2 -4
- package/dist/file-store-boundary.d.ts +3 -7
- package/dist/file-store-boundary.js +7 -10
- package/dist/file-store-prune.js +3 -2
- package/dist/file-store.d.ts +4 -7
- package/dist/file-store.js +19 -21
- package/dist/guest-native-python.js +23 -31
- package/dist/guest.js +21 -12
- package/dist/json-document-store.d.ts +4 -9
- package/dist/json-durable-queue.js +2 -6
- package/dist/local-file-access.js +2 -5
- package/dist/local-file-descriptor.d.ts +2 -5
- package/dist/local-roots.d.ts +2 -7
- package/dist/move-path-cleanup.js +4 -4
- package/dist/native-binding.d.ts +18 -14
- package/dist/native-staged-symlink.d.ts +13 -0
- package/dist/native-staged-symlink.js +303 -0
- package/dist/owner-dacl-batch-worker.d.ts +1 -0
- package/dist/owner-dacl-batch-worker.js +54 -0
- package/dist/owner-dacl-batch.d.ts +5 -0
- package/dist/owner-dacl-batch.js +64 -0
- package/dist/owner-dacl.d.ts +2 -0
- package/dist/owner-dacl.js +3 -0
- package/dist/path.js +17 -1
- package/dist/permission-exec.js +3 -6
- package/dist/permissions-public.d.ts +1 -0
- package/dist/permissions-public.js +1 -0
- package/dist/pinned-mutation-admission.d.ts +0 -1
- package/dist/pinned-mutation-shared-route.d.ts +2 -8
- package/dist/pinned-open.d.ts +0 -1
- package/dist/pinned-open.js +1 -2
- package/dist/publish-copy-stage.js +4 -0
- package/dist/read-opened-file.d.ts +2 -5
- package/dist/regular-file.js +3 -3
- package/dist/replace-file-copy-fallback.d.ts +1 -2
- package/dist/root-context.js +3 -2
- package/dist/root-impl.js +0 -3
- package/dist/root-move-noreplace.d.ts +2 -7
- package/dist/root-observed-path.d.ts +0 -1
- package/dist/root-observed-path.js +0 -3
- package/dist/root-path-observation.d.ts +4 -11
- package/dist/root-path.js +7 -10
- package/dist/root-paths.d.ts +2 -6
- package/dist/root-remove-identity.d.ts +1 -3
- package/dist/root-walk.js +8 -3
- package/dist/root-write-admission.js +1 -6
- package/dist/root-write-complete-parent.d.ts +2 -0
- package/dist/root-write-complete-parent.js +1 -1
- package/dist/safe-path-segment.d.ts +1 -0
- package/dist/safe-path-segment.js +8 -2
- package/dist/secret-file.d.ts +6 -2
- package/dist/secret-file.js +1 -0
- package/dist/secure-file-windows.js +1 -5
- package/dist/secure-file.js +3 -2
- package/dist/sidecar-lock-admission-parser.d.ts +1 -2
- package/dist/sidecar-lock-handle.d.ts +2 -8
- package/dist/sidecar-lock-policy.d.ts +2 -7
- package/dist/sidecar-lock-stale-admission.d.ts +1 -5
- package/dist/sidecar-lock.js +5 -3
- package/dist/staged-symlink-types.d.ts +49 -0
- package/dist/staged-symlink-types.js +1 -0
- package/dist/symlink-parents.js +58 -7
- package/dist/temp-target.js +4 -2
- package/dist/temp-workspace-owner.js +4 -9
- package/dist/test-hooks.d.ts +1 -1
- package/dist/text-atomic.d.ts +2 -1
- package/dist/text-atomic.js +2 -0
- package/dist/trash.js +27 -1
- package/dist/walk.d.ts +2 -5
- package/dist/windows-owner.d.ts +0 -1
- package/dist/windows-owner.js +0 -1
- package/dist/windows-security-bridge.cs +6 -4
- package/dist/windows-security-bridge.ps1 +78 -3
- package/dist/windows-security-command.d.ts +8 -0
- package/dist/windows-security-command.js +66 -15
- package/dist/windows-security-facts.d.ts +4 -0
- package/dist/windows-security-facts.js +6 -2
- package/docs/advanced.md +3 -2
- package/docs/archive.md +10 -0
- package/docs/atomic.md +11 -3
- package/docs/contributing.md +30 -0
- package/docs/copy.md +2 -0
- package/docs/file-store.md +5 -0
- package/docs/guest.md +7 -1
- package/docs/install.md +28 -0
- package/docs/native-helper.md +11 -4
- package/docs/native.md +45 -1
- package/docs/permissions.md +66 -0
- package/docs/public-api.md +7 -1
- package/docs/root.md +10 -1
- package/docs/secret-file.md +10 -0
- package/docs/security-model.md +4 -1
- package/docs/sidecar-lock.md +2 -0
- package/docs/staged-symlink.md +123 -0
- package/docs/store.md +3 -1
- package/docs/testing.md +28 -0
- package/docs/walk.md +12 -0
- package/docs/writing.md +21 -0
- package/package.json +9 -9
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type SyncHeldLock } from "./file-lock-sync-admission.js";
|
|
2
|
+
import type { SidecarLockOptionFields } from "./sidecar-lock-types.js";
|
|
1
3
|
import type { FileLockSyncHandle } from "./file-lock-sync.js";
|
|
2
|
-
import { type SidecarLockSnapshot } from "./sidecar-lock-reclaim.js";
|
|
3
4
|
import { type FileLockSyncRootAuthority, type FileLockSyncRootPath } from "./file-lock-sync-root.js";
|
|
4
5
|
import { type FileLockSyncRootFileReceipt, type FileLockSyncRootSnapshot } from "./file-lock-sync-root-io.js";
|
|
5
6
|
type RootSyncHeldLockReleaseState = "active" | "releasing" | "exit-cleaning" | "released";
|
|
@@ -7,22 +8,14 @@ type RootSyncHeldLockHandleDisposition = {
|
|
|
7
8
|
held: RootSyncHeldLock;
|
|
8
9
|
released: boolean;
|
|
9
10
|
};
|
|
10
|
-
export type RootSyncHeldLock = {
|
|
11
|
+
export type RootSyncHeldLock = SidecarLockOptionFields<SyncHeldLock & {
|
|
11
12
|
deferredExitReleases?: Set<RootSyncHeldLockHandleDisposition>;
|
|
12
|
-
fd: number | undefined;
|
|
13
|
-
lockPath: string;
|
|
14
|
-
normalizedTargetPath: string;
|
|
15
|
-
parsePayload?: (raw: string) => unknown;
|
|
16
|
-
refCount: number;
|
|
17
|
-
reentrantOwner?: string;
|
|
18
13
|
releaseState: RootSyncHeldLockReleaseState;
|
|
19
14
|
revision: number;
|
|
20
15
|
rootAuthority: FileLockSyncRootAuthority;
|
|
21
16
|
rootPath: FileLockSyncRootPath;
|
|
22
17
|
rootReceipt: FileLockSyncRootFileReceipt;
|
|
23
|
-
|
|
24
|
-
timer?: NodeJS.Timeout;
|
|
25
|
-
};
|
|
18
|
+
}>;
|
|
26
19
|
export declare function readRootSidecarSnapshotSync(rootPath: FileLockSyncRootPath, parsePayload?: (raw: string) => unknown, onOpenFailure?: (error: unknown) => void, expectedReceipt?: FileLockSyncRootFileReceipt): FileLockSyncRootSnapshot | null;
|
|
27
20
|
export declare function getRootSyncHeldLocks(): Map<string, RootSyncHeldLock>;
|
|
28
21
|
export declare function ensureRootSyncExitCleanupRegistered(): void;
|
|
@@ -53,9 +53,6 @@ export function getRootSyncHeldLocks() {
|
|
|
53
53
|
}
|
|
54
54
|
return globalWithState[ROOT_SYNC_HELD_LOCKS_KEY];
|
|
55
55
|
}
|
|
56
|
-
function existingRootSyncHeldLocks() {
|
|
57
|
-
return globalThis[ROOT_SYNC_HELD_LOCKS_KEY];
|
|
58
|
-
}
|
|
59
56
|
function reactivateExactHeldLock(heldLocks, held, restoreConsumedReference) {
|
|
60
57
|
if (heldLocks.get(held.normalizedTargetPath) !== held || held.releaseState === "released") {
|
|
61
58
|
return false;
|
|
@@ -123,7 +120,7 @@ function releaseAllRootSyncHeldLocks() {
|
|
|
123
120
|
try {
|
|
124
121
|
// A newListener callback can invoke the candidate handler before
|
|
125
122
|
// registration succeeds. Do not create acquisition state in that window.
|
|
126
|
-
const heldLocks =
|
|
123
|
+
const heldLocks = globalThis[ROOT_SYNC_HELD_LOCKS_KEY];
|
|
127
124
|
if (!heldLocks)
|
|
128
125
|
return;
|
|
129
126
|
getSyncLockAdmissions().clear();
|
|
@@ -14,10 +14,7 @@ export type FileLockSyncRootFileReceipt = Readonly<{
|
|
|
14
14
|
identity: ExactIdentity;
|
|
15
15
|
parent: DirectoryReceipt;
|
|
16
16
|
}>;
|
|
17
|
-
export type FileLockSyncRootDirectoryReceipt =
|
|
18
|
-
identity: ExactIdentity;
|
|
19
|
-
parent: DirectoryReceipt;
|
|
20
|
-
}>;
|
|
17
|
+
export type FileLockSyncRootDirectoryReceipt = FileLockSyncRootFileReceipt;
|
|
21
18
|
export type FileLockSyncRootDiskSnapshot = {
|
|
22
19
|
ownershipToken?: never;
|
|
23
20
|
payload: unknown;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type DenyMutationPolicy } from "./deny-mutations.js";
|
|
1
2
|
import { type RootContext } from "./root-context.js";
|
|
2
3
|
import type { Root } from "./root-impl.js";
|
|
3
4
|
import type { RootDefaults } from "./root-options.js";
|
|
@@ -7,10 +8,7 @@ export type FileLockSyncRootAuthority = Readonly<{
|
|
|
7
8
|
adapter: object;
|
|
8
9
|
context: RootContext;
|
|
9
10
|
assertBeforeMutation?: () => void;
|
|
10
|
-
denyMutations?: Readonly<
|
|
11
|
-
paths?: readonly string[];
|
|
12
|
-
prefixes?: readonly string[];
|
|
13
|
-
}>;
|
|
11
|
+
denyMutations?: Readonly<DenyMutationPolicy>;
|
|
14
12
|
hardlinks?: RootDefaults["hardlinks"];
|
|
15
13
|
mutationPolicy: RootPathResolutionPolicy;
|
|
16
14
|
readPolicy: RootPathResolutionPolicy;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Readable } from "node:stream";
|
|
2
|
-
import { type SyncStoreDirectoryReceipt } from "./file-store-sync-directory.js";
|
|
2
|
+
import { ensureSyncStoreDirectory, type SyncStoreDirectoryReceipt } from "./file-store-sync-directory.js";
|
|
3
3
|
import { type Root } from "./root.js";
|
|
4
4
|
import { prepareSecretFileWrite } from "./secret-file.js";
|
|
5
5
|
export type SyncParentGuard = SyncStoreDirectoryReceipt;
|
|
@@ -23,12 +23,8 @@ export declare function ensureParentSync(params: {
|
|
|
23
23
|
filePath: string;
|
|
24
24
|
mode: number;
|
|
25
25
|
}): SyncParentGuard;
|
|
26
|
-
export declare function ensureStoreDirectorySync(params:
|
|
27
|
-
|
|
28
|
-
targetDir: string;
|
|
29
|
-
mode: number;
|
|
30
|
-
messagePrefix: "private store" | "store";
|
|
31
|
-
}): SyncParentGuard;
|
|
26
|
+
export declare function ensureStoreDirectorySync(params: Parameters<typeof ensureSyncStoreDirectory>[0]): SyncParentGuard;
|
|
27
|
+
export declare function literalStoreRootPath(relativePath: string): string;
|
|
32
28
|
export declare function assertRelativePath(relativePath: string): string;
|
|
33
29
|
export declare function resolveStorePath(rootDir: string, relativePath: string): string;
|
|
34
30
|
export declare function assertFileStoreMaxBytes(size: number, limit: number | undefined): void;
|
|
@@ -7,8 +7,7 @@ import { FsSafeError } from "./errors.js";
|
|
|
7
7
|
import { assertSyncStoreDirectoryReceipt, ensureSyncStoreDirectory, } from "./file-store-sync-directory.js";
|
|
8
8
|
import { isPathInside, splitSafeRelativePath } from "./path.js";
|
|
9
9
|
import { resolveOpenedFileRealPathForHandle, root } from "./root.js";
|
|
10
|
-
import {
|
|
11
|
-
import { RootHandle } from "./root-impl.js";
|
|
10
|
+
import { rootFromDirectoryGuard } from "./root-impl.js";
|
|
12
11
|
import { prepareSecretFileWrite } from "./secret-file.js";
|
|
13
12
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
14
13
|
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
@@ -16,7 +15,7 @@ import { readRegularFile } from "./regular-file.js";
|
|
|
16
15
|
import { errorCauseOptions } from "./root-errors.js";
|
|
17
16
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
18
17
|
export async function ensureParentInRoot(scopedRoot, relativePath, mode) {
|
|
19
|
-
const parent = path.posix.dirname(relativePath);
|
|
18
|
+
const parent = literalStoreRootPath(path.posix.dirname(relativePath));
|
|
20
19
|
if (parent === ".") {
|
|
21
20
|
return;
|
|
22
21
|
}
|
|
@@ -32,13 +31,7 @@ export async function openWritableStoreRoot(params) {
|
|
|
32
31
|
export async function openPrivateStoreLockRoot(params) {
|
|
33
32
|
const { parentGuard } = await prepareSecretFileWrite(params);
|
|
34
33
|
// Bind to the admitted parent, never resolve a replacement into a fresh capability.
|
|
35
|
-
return
|
|
36
|
-
rootDir: parentGuard.dir,
|
|
37
|
-
rootGuard: { dir: parentGuard.realPath, realPath: parentGuard.realPath, stat: parentGuard.stat },
|
|
38
|
-
rootReal: parentGuard.realPath,
|
|
39
|
-
rootWithSep: ensureTrailingSep(parentGuard.realPath),
|
|
40
|
-
rootIdentity: { dev: parentGuard.stat.dev, ino: parentGuard.stat.ino },
|
|
41
|
-
}, { hardlinks: "reject" });
|
|
34
|
+
return rootFromDirectoryGuard(parentGuard, { hardlinks: "reject" });
|
|
42
35
|
}
|
|
43
36
|
async function chmodDirectoryInRootBestEffort(scopedRoot, relativePath, mode) {
|
|
44
37
|
const dirPath = await scopedRoot.resolve(relativePath);
|
|
@@ -120,6 +113,10 @@ export function ensureStoreDirectorySync(params) {
|
|
|
120
113
|
assertSyncStoreDirectoryReceipt(guard);
|
|
121
114
|
return guard;
|
|
122
115
|
}
|
|
116
|
+
// Store keys and directory-entry names are literal, unlike Root's home syntax.
|
|
117
|
+
export function literalStoreRootPath(relativePath) {
|
|
118
|
+
return relativePath === "~" || relativePath.startsWith("~/") ? `./${relativePath}` : relativePath;
|
|
119
|
+
}
|
|
123
120
|
export function assertRelativePath(relativePath) {
|
|
124
121
|
const raw = relativePath.trim();
|
|
125
122
|
if (!raw || raw !== relativePath) {
|
package/dist/file-store-prune.js
CHANGED
|
@@ -2,6 +2,7 @@ import fsSync, {} from "node:fs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { literalStoreRootPath } from "./file-store-boundary.js";
|
|
5
6
|
import { isPathInside } from "./path.js";
|
|
6
7
|
import { realpathSync } from "./realpath.js";
|
|
7
8
|
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
@@ -74,13 +75,13 @@ export async function pruneExpiredStoreEntries(params) {
|
|
|
74
75
|
await assertRootGuard();
|
|
75
76
|
// Keep empty-dir pruning on the same root-bounded remove path as files;
|
|
76
77
|
// the Root fallback handles empty directories without recursive delete.
|
|
77
|
-
await scopedRoot.remove(relativePath, REMOVE_EMPTY_DIRECTORY_OPTIONS).catch(() => undefined);
|
|
78
|
+
await scopedRoot.remove(literalStoreRootPath(relativePath), REMOVE_EMPTY_DIRECTORY_OPTIONS).catch(() => undefined);
|
|
78
79
|
}
|
|
79
80
|
continue;
|
|
80
81
|
}
|
|
81
82
|
if (stat.isFile() && now - stat.mtimeMs > params.options.ttlMs) {
|
|
82
83
|
await assertRootGuard();
|
|
83
|
-
await scopedRoot.remove(relativePath, {
|
|
84
|
+
await scopedRoot.remove(literalStoreRootPath(relativePath), {
|
|
84
85
|
assertBeforeMutation: () => {
|
|
85
86
|
// Removal preparation can outlive the expiry observation above.
|
|
86
87
|
const current = fsSync.lstatSync(fullPath);
|
package/dist/file-store.d.ts
CHANGED
|
@@ -12,14 +12,11 @@ export type FileStoreOptions = {
|
|
|
12
12
|
mode?: number;
|
|
13
13
|
maxBytes?: number;
|
|
14
14
|
};
|
|
15
|
-
export type FileStoreWriteOptions = {
|
|
15
|
+
export type FileStoreWriteOptions = Partial<Pick<FileStoreOptions, "dirMode" | "mode" | "maxBytes"> & {
|
|
16
16
|
/** Override store durability; defaults to the store option, then true. */
|
|
17
|
-
durable
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
maxBytes?: number;
|
|
21
|
-
tempPrefix?: string;
|
|
22
|
-
};
|
|
17
|
+
durable: boolean;
|
|
18
|
+
tempPrefix: string;
|
|
19
|
+
}>;
|
|
23
20
|
export type FileStoreReadOptions = RootReadOptions & {
|
|
24
21
|
encoding?: BufferEncoding;
|
|
25
22
|
};
|
package/dist/file-store.js
CHANGED
|
@@ -3,7 +3,7 @@ import { normalizeMaxBytes } from "./byte-budget.js";
|
|
|
3
3
|
import { readFileDescriptorBoundedSync } from "./bounded-read.js";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { pruneExpiredStoreEntries } from "./file-store-prune.js";
|
|
6
|
-
import { assertFileStoreMaxBytes, assertRelativePath, ensureParentInRoot, openPrivateStoreLockRoot, openWritableStoreRoot, readFileStoreCopySource, resolveStorePath, writeStreamToTempSource, } from "./file-store-boundary.js";
|
|
6
|
+
import { assertFileStoreMaxBytes, assertRelativePath, ensureParentInRoot, literalStoreRootPath, openPrivateStoreLockRoot, openWritableStoreRoot, readFileStoreCopySource, resolveStorePath, writeStreamToTempSource, } from "./file-store-boundary.js";
|
|
7
7
|
import { writeFileSyncAtomic } from "./file-store-sync-write.js";
|
|
8
8
|
import { createJsonStore } from "./json-document-store.js";
|
|
9
9
|
import { stringifyJsonDocument } from "./json-stringify.js";
|
|
@@ -61,7 +61,7 @@ function handleSyncStoreReadOpenFailure(opened) {
|
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
async function copyIntoRoot(params) {
|
|
64
|
-
const relativePath =
|
|
64
|
+
const relativePath = params.relativePath;
|
|
65
65
|
const destination = resolveStorePath(params.rootDir, relativePath);
|
|
66
66
|
assertNoWindowsPathAlias(params.sourcePath, "filesystem", "source path uses a Windows filesystem namespace alias");
|
|
67
67
|
const sourceStat = syncFs.lstatSync(params.sourcePath);
|
|
@@ -75,7 +75,7 @@ async function copyIntoRoot(params) {
|
|
|
75
75
|
maxBytes: params.maxBytes,
|
|
76
76
|
});
|
|
77
77
|
await ensureParentInRoot(scopedRoot, relativePath, params.dirMode);
|
|
78
|
-
await scopedRoot.copyIn(relativePath, params.sourcePath, {
|
|
78
|
+
await scopedRoot.copyIn(literalStoreRootPath(relativePath), params.sourcePath, {
|
|
79
79
|
durable: params.durable,
|
|
80
80
|
maxBytes: params.maxBytes,
|
|
81
81
|
mkdir: false,
|
|
@@ -97,8 +97,7 @@ export function fileStore(options) {
|
|
|
97
97
|
return await root(rootDir, { hardlinks: "reject", maxBytes });
|
|
98
98
|
}
|
|
99
99
|
async function write(relativePath, data, writeOptions) {
|
|
100
|
-
const
|
|
101
|
-
const destination = resolveStorePath(rootDir, safeRelativePath);
|
|
100
|
+
const destination = resolveStorePath(rootDir, relativePath);
|
|
102
101
|
const content = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
103
102
|
const writeMaxBytes = normalizeMaxBytes(writeOptions?.maxBytes, { defaultValue: maxBytes });
|
|
104
103
|
assertFileStoreMaxBytes(content.byteLength, writeMaxBytes);
|
|
@@ -121,8 +120,8 @@ export function fileStore(options) {
|
|
|
121
120
|
dirMode: writeDirMode,
|
|
122
121
|
maxBytes: writeMaxBytes,
|
|
123
122
|
});
|
|
124
|
-
await ensureParentInRoot(scopedRoot,
|
|
125
|
-
await scopedRoot.write(
|
|
123
|
+
await ensureParentInRoot(scopedRoot, relativePath, writeDirMode);
|
|
124
|
+
await scopedRoot.write(literalStoreRootPath(relativePath), content, {
|
|
126
125
|
mkdir: false,
|
|
127
126
|
mode: writeMode,
|
|
128
127
|
durable: writeDurable,
|
|
@@ -135,8 +134,7 @@ export function fileStore(options) {
|
|
|
135
134
|
root: openRoot,
|
|
136
135
|
write,
|
|
137
136
|
writeStream: async (relativePath, stream, writeOptions) => {
|
|
138
|
-
const
|
|
139
|
-
const destination = resolveStorePath(rootDir, safeRelativePath);
|
|
137
|
+
const destination = resolveStorePath(rootDir, relativePath);
|
|
140
138
|
const configuredLimit = normalizeMaxBytes(writeOptions?.maxBytes, { defaultValue: maxBytes });
|
|
141
139
|
const limit = configuredLimit ?? (privateMode ? DEFAULT_ROOT_MAX_BYTES : undefined);
|
|
142
140
|
if (privateMode) {
|
|
@@ -172,7 +170,7 @@ export function fileStore(options) {
|
|
|
172
170
|
try {
|
|
173
171
|
await copyIntoRoot({
|
|
174
172
|
rootDir,
|
|
175
|
-
relativePath
|
|
173
|
+
relativePath,
|
|
176
174
|
sourcePath: staged.path,
|
|
177
175
|
durable: writeDurable,
|
|
178
176
|
maxBytes: limit,
|
|
@@ -214,17 +212,17 @@ export function fileStore(options) {
|
|
|
214
212
|
tempPrefix: writeOptions?.tempPrefix,
|
|
215
213
|
});
|
|
216
214
|
},
|
|
217
|
-
open: async (relativePath, readOptions) => await (await openRoot()).open(assertRelativePath(relativePath), readOptions),
|
|
218
|
-
read: async (relativePath, readOptions) => await (await openRoot()).read(assertRelativePath(relativePath), readOptions),
|
|
219
|
-
readBytes: async (relativePath, readOptions) => await (await openRoot()).readBytes(assertRelativePath(relativePath), readOptions),
|
|
215
|
+
open: async (relativePath, readOptions) => await (await openRoot()).open(literalStoreRootPath(assertRelativePath(relativePath)), readOptions),
|
|
216
|
+
read: async (relativePath, readOptions) => await (await openRoot()).read(literalStoreRootPath(assertRelativePath(relativePath)), readOptions),
|
|
217
|
+
readBytes: async (relativePath, readOptions) => await (await openRoot()).readBytes(literalStoreRootPath(assertRelativePath(relativePath)), readOptions),
|
|
220
218
|
readText: async (relativePath, readOptions) => {
|
|
221
219
|
const { encoding = "utf8", ...options } = readOptions ?? {};
|
|
222
|
-
return (await (await openRoot()).read(assertRelativePath(relativePath), options)).buffer
|
|
220
|
+
return (await (await openRoot()).read(literalStoreRootPath(assertRelativePath(relativePath)), options)).buffer
|
|
223
221
|
.toString(encoding);
|
|
224
222
|
},
|
|
225
223
|
readTextIfExists: async (relativePath, readOptions) => {
|
|
226
224
|
try {
|
|
227
|
-
return await (await openRoot()).readText(assertRelativePath(relativePath), readOptions);
|
|
225
|
+
return await (await openRoot()).readText(literalStoreRootPath(assertRelativePath(relativePath)), readOptions);
|
|
228
226
|
}
|
|
229
227
|
catch (error) {
|
|
230
228
|
if (isNotFound(error)) {
|
|
@@ -235,12 +233,12 @@ export function fileStore(options) {
|
|
|
235
233
|
},
|
|
236
234
|
readJson: async (relativePath, readOptions) => {
|
|
237
235
|
const { encoding = "utf8", ...options } = readOptions ?? {};
|
|
238
|
-
return JSON.parse((await (await openRoot()).read(assertRelativePath(relativePath), options)).buffer
|
|
236
|
+
return JSON.parse((await (await openRoot()).read(literalStoreRootPath(assertRelativePath(relativePath)), options)).buffer
|
|
239
237
|
.toString(encoding));
|
|
240
238
|
},
|
|
241
239
|
readJsonIfExists: async (relativePath, readOptions) => {
|
|
242
240
|
try {
|
|
243
|
-
return await (await openRoot()).readJson(assertRelativePath(relativePath), readOptions);
|
|
241
|
+
return await (await openRoot()).readJson(literalStoreRootPath(assertRelativePath(relativePath)), readOptions);
|
|
244
242
|
}
|
|
245
243
|
catch (error) {
|
|
246
244
|
if (isNotFound(error)) {
|
|
@@ -250,9 +248,9 @@ export function fileStore(options) {
|
|
|
250
248
|
}
|
|
251
249
|
},
|
|
252
250
|
remove: async (relativePath) => {
|
|
253
|
-
await (await openRoot()).remove(assertRelativePath(relativePath));
|
|
251
|
+
await (await openRoot()).remove(literalStoreRootPath(assertRelativePath(relativePath)));
|
|
254
252
|
},
|
|
255
|
-
exists: async (relativePath) => await (await openRoot()).exists(assertRelativePath(relativePath)),
|
|
253
|
+
exists: async (relativePath) => await (await openRoot()).exists(literalStoreRootPath(assertRelativePath(relativePath))),
|
|
256
254
|
writeText: async (relativePath, data, writeOptions) => await write(relativePath, data, writeOptions),
|
|
257
255
|
writeJson: async (relativePath, data, writeOptions) => {
|
|
258
256
|
const trailingNewline = writeOptions?.trailingNewline;
|
|
@@ -269,7 +267,7 @@ export function fileStore(options) {
|
|
|
269
267
|
} : {}),
|
|
270
268
|
readIfExists: async () => {
|
|
271
269
|
try {
|
|
272
|
-
return await (await openRoot()).readJson(
|
|
270
|
+
return await (await openRoot()).readJson(literalStoreRootPath(relativePath));
|
|
273
271
|
}
|
|
274
272
|
catch (error) {
|
|
275
273
|
if (isNotFound(error)) {
|
|
@@ -278,7 +276,7 @@ export function fileStore(options) {
|
|
|
278
276
|
throw error;
|
|
279
277
|
}
|
|
280
278
|
},
|
|
281
|
-
readRequired: async () => await (await openRoot()).readJson(
|
|
279
|
+
readRequired: async () => await (await openRoot()).readJson(literalStoreRootPath(relativePath)),
|
|
282
280
|
write: async (value, options) => {
|
|
283
281
|
const json = stringifyJsonDocument(value, null, 2);
|
|
284
282
|
await write(relativePath, options?.trailingNewline === false ? json : `${json}\n`, { durable: options?.durable });
|
|
@@ -36,15 +36,6 @@ export const GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON = [
|
|
|
36
36
|
" is_linux = sys.platform.startswith('linux')",
|
|
37
37
|
" if is_linux:",
|
|
38
38
|
" rename_fn = getattr(libc, 'renameat2', None)",
|
|
39
|
-
" if rename_fn is None:",
|
|
40
|
-
" os.link(",
|
|
41
|
-
" src_basename,",
|
|
42
|
-
" dst_basename,",
|
|
43
|
-
" src_dir_fd=src_parent_fd,",
|
|
44
|
-
" dst_dir_fd=dst_parent_fd,",
|
|
45
|
-
" follow_symlinks=False,",
|
|
46
|
-
" )",
|
|
47
|
-
" return",
|
|
48
39
|
" flags = 1 # RENAME_NOREPLACE",
|
|
49
40
|
" elif sys.platform == 'darwin':",
|
|
50
41
|
" rename_fn = getattr(libc, 'renameatx_np', None)",
|
|
@@ -52,32 +43,33 @@ export const GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON = [
|
|
|
52
43
|
" else:",
|
|
53
44
|
" rename_fn = None",
|
|
54
45
|
" flags = 0",
|
|
55
|
-
" if rename_fn is None:",
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
46
|
+
" if rename_fn is not None:",
|
|
47
|
+
" rename_fn.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]",
|
|
48
|
+
" rename_fn.restype = ctypes.c_int",
|
|
49
|
+
" result = rename_fn(",
|
|
50
|
+
" src_parent_fd,",
|
|
51
|
+
" os.fsencode(src_basename),",
|
|
52
|
+
" dst_parent_fd,",
|
|
53
|
+
" os.fsencode(dst_basename),",
|
|
54
|
+
" flags,",
|
|
55
|
+
" )",
|
|
56
|
+
" if result == 0:",
|
|
57
|
+
" return",
|
|
67
58
|
" error_code = ctypes.get_errno()",
|
|
68
59
|
" unsupported_codes = {errno.ENOSYS, errno.EINVAL, errno.ENOTSUP}",
|
|
69
60
|
" if hasattr(errno, 'EOPNOTSUPP'):",
|
|
70
61
|
" unsupported_codes.add(errno.EOPNOTSUPP)",
|
|
71
|
-
" if is_linux
|
|
72
|
-
" os.
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
62
|
+
" if not is_linux or error_code not in unsupported_codes:",
|
|
63
|
+
" raise OSError(error_code, os.strerror(error_code), dst_basename)",
|
|
64
|
+
" elif not is_linux:",
|
|
65
|
+
" raise OSError(errno.ENOSYS, 'atomic no-replace rename is unavailable')",
|
|
66
|
+
" os.link(",
|
|
67
|
+
" src_basename,",
|
|
68
|
+
" dst_basename,",
|
|
69
|
+
" src_dir_fd=src_parent_fd,",
|
|
70
|
+
" dst_dir_fd=dst_parent_fd,",
|
|
71
|
+
" follow_symlinks=False,",
|
|
72
|
+
" )",
|
|
81
73
|
].join("\n");
|
|
82
74
|
export const GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON = [
|
|
83
75
|
"def create_exclusive(parent_fd, basename, stdin_buffer):",
|
package/dist/guest.js
CHANGED
|
@@ -270,13 +270,15 @@ export const GUEST_FILESYSTEM_PYTHON = [
|
|
|
270
270
|
"def copy_entry(src_parent_fd, src_basename, dst_parent_fd, dst_basename):",
|
|
271
271
|
" src_stat = os.lstat(src_basename, dir_fd=src_parent_fd)",
|
|
272
272
|
" if stat.S_ISDIR(src_stat.st_mode) and not stat.S_ISLNK(src_stat.st_mode):",
|
|
273
|
-
" os.mkdir(dst_basename, stat.S_IMODE(src_stat.st_mode) or 0o755, dir_fd=dst_parent_fd)",
|
|
274
273
|
" copied_children = []",
|
|
275
274
|
" src_dir_fd = None",
|
|
276
275
|
" dst_dir_fd = None",
|
|
276
|
+
" destination_created = False",
|
|
277
277
|
" try:",
|
|
278
278
|
" src_dir_fd = open_dir(src_basename, dir_fd=src_parent_fd)",
|
|
279
279
|
" src_stat = os.fstat(src_dir_fd)",
|
|
280
|
+
" os.mkdir(dst_basename, stat.S_IMODE(src_stat.st_mode), dir_fd=dst_parent_fd)",
|
|
281
|
+
" destination_created = True",
|
|
280
282
|
" dst_dir_fd = open_dir(dst_basename, dir_fd=dst_parent_fd)",
|
|
281
283
|
" for child in os.listdir(src_dir_fd):",
|
|
282
284
|
" copied_children.append((child, copy_entry(src_dir_fd, child, dst_dir_fd, child)))",
|
|
@@ -284,10 +286,11 @@ export const GUEST_FILESYSTEM_PYTHON = [
|
|
|
284
286
|
" if dst_dir_fd is not None:",
|
|
285
287
|
" os.close(dst_dir_fd)",
|
|
286
288
|
" dst_dir_fd = None",
|
|
287
|
-
"
|
|
288
|
-
"
|
|
289
|
-
"
|
|
290
|
-
"
|
|
289
|
+
" if destination_created:",
|
|
290
|
+
" try:",
|
|
291
|
+
" remove_tree(dst_parent_fd, dst_basename)",
|
|
292
|
+
" except FileNotFoundError:",
|
|
293
|
+
" pass",
|
|
291
294
|
" raise",
|
|
292
295
|
" finally:",
|
|
293
296
|
" if src_dir_fd is not None:",
|
|
@@ -328,30 +331,36 @@ export const GUEST_FILESYSTEM_PYTHON = [
|
|
|
328
331
|
" raise",
|
|
329
332
|
" src_stat = os.lstat(src_basename, dir_fd=src_parent_fd)",
|
|
330
333
|
" if stat.S_ISDIR(src_stat.st_mode) and not stat.S_ISLNK(src_stat.st_mode):",
|
|
331
|
-
" temp_dir_name =
|
|
334
|
+
" temp_dir_name = None",
|
|
332
335
|
" copied_children = []",
|
|
333
336
|
" temp_dir_fd = None",
|
|
334
337
|
" src_dir_fd = None",
|
|
335
338
|
" try:",
|
|
336
|
-
" temp_dir_fd = open_dir(temp_dir_name, dir_fd=dst_parent_fd)",
|
|
337
339
|
" src_dir_fd = open_dir(src_basename, dir_fd=src_parent_fd)",
|
|
338
340
|
" src_stat = os.fstat(src_dir_fd)",
|
|
341
|
+
" source_mode = stat.S_IMODE(src_stat.st_mode)",
|
|
342
|
+
" temp_dir_name = create_temp_dir(dst_parent_fd, source_mode or 0o700)",
|
|
343
|
+
" temp_dir_fd = open_dir(temp_dir_name, dir_fd=dst_parent_fd)",
|
|
339
344
|
" for child in os.listdir(src_dir_fd):",
|
|
340
345
|
" copied_children.append((child, copy_entry(src_dir_fd, child, temp_dir_fd, child)))",
|
|
341
346
|
" os.close(src_dir_fd)",
|
|
342
347
|
" src_dir_fd = None",
|
|
348
|
+
" os.rename(temp_dir_name, dst_basename, src_dir_fd=dst_parent_fd, dst_dir_fd=dst_parent_fd)",
|
|
349
|
+
" temp_dir_name = None",
|
|
350
|
+
" if source_mode == 0:",
|
|
351
|
+
" os.fchmod(temp_dir_fd, 0)",
|
|
343
352
|
" os.close(temp_dir_fd)",
|
|
344
353
|
" temp_dir_fd = None",
|
|
345
|
-
" os.rename(temp_dir_name, dst_basename, src_dir_fd=dst_parent_fd, dst_dir_fd=dst_parent_fd)",
|
|
346
354
|
" except Exception:",
|
|
347
355
|
" if src_dir_fd is not None:",
|
|
348
356
|
" os.close(src_dir_fd)",
|
|
349
357
|
" if temp_dir_fd is not None:",
|
|
350
358
|
" os.close(temp_dir_fd)",
|
|
351
|
-
"
|
|
352
|
-
"
|
|
353
|
-
"
|
|
354
|
-
"
|
|
359
|
+
" if temp_dir_name is not None:",
|
|
360
|
+
" try:",
|
|
361
|
+
" remove_tree(dst_parent_fd, temp_dir_name)",
|
|
362
|
+
" except FileNotFoundError:",
|
|
363
|
+
" pass",
|
|
355
364
|
" raise",
|
|
356
365
|
" remove_copied_entry(src_parent_fd, src_basename, ('dir', entry_identity(src_stat), copied_children))",
|
|
357
366
|
" os.fsync(dst_parent_fd)",
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type FsSafeLockConfig } from "./lock-config.js";
|
|
2
2
|
import type { Root } from "./root.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
timeoutMs?: number;
|
|
7
|
-
retry?: FileLockRetryOptions;
|
|
8
|
-
staleRecovery?: SidecarLockStaleRecovery;
|
|
9
|
-
managerKey?: string;
|
|
10
|
-
};
|
|
3
|
+
export type JsonStoreLockOptions = Partial<FsSafeLockConfig & {
|
|
4
|
+
managerKey: string;
|
|
5
|
+
}>;
|
|
11
6
|
export type JsonFileStoreOptions = {
|
|
12
7
|
/** Overrides the parent store's durability for every JSON mutation. */
|
|
13
8
|
durable?: boolean;
|
|
@@ -145,8 +145,7 @@ export async function loadPendingJsonDurableQueueEntries(options) {
|
|
|
145
145
|
await unlinkStaleTmpBestEffort(path.join(queueDir, file), now, options.cleanupTmpMaxAgeMs);
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
-
const ids =
|
|
149
|
-
const seenIds = new Set();
|
|
148
|
+
const ids = new Set();
|
|
150
149
|
for (const file of files) {
|
|
151
150
|
const suffix = file.endsWith(".processing")
|
|
152
151
|
? ".processing"
|
|
@@ -162,10 +161,7 @@ export async function loadPendingJsonDurableQueueEntries(options) {
|
|
|
162
161
|
catch {
|
|
163
162
|
continue;
|
|
164
163
|
}
|
|
165
|
-
|
|
166
|
-
continue;
|
|
167
|
-
seenIds.add(id);
|
|
168
|
-
ids.push(id);
|
|
164
|
+
ids.add(id);
|
|
169
165
|
}
|
|
170
166
|
const entries = [];
|
|
171
167
|
for (const id of ids) {
|
|
@@ -8,10 +8,6 @@ const FILE_URL_PREFIX_RE = /^file:\/\//i;
|
|
|
8
8
|
export function isFileUrl(input) {
|
|
9
9
|
return FILE_URL_PREFIX_RE.test(input);
|
|
10
10
|
}
|
|
11
|
-
function isLocalFileUrlHost(hostname) {
|
|
12
|
-
const normalized = normalizeLowercaseStringOrEmpty(hostname);
|
|
13
|
-
return normalized === "" || normalized === "localhost";
|
|
14
|
-
}
|
|
15
11
|
export function hasEncodedFileUrlSeparator(pathname) {
|
|
16
12
|
return ENCODED_FILE_URL_SEPARATOR_RE.test(pathname);
|
|
17
13
|
}
|
|
@@ -41,7 +37,8 @@ export function safeFileURLToPath(fileUrl, platform = process.platform) {
|
|
|
41
37
|
if (parsed.protocol !== "file:") {
|
|
42
38
|
throw new Error(`Invalid file:// URL: ${fileUrl}`);
|
|
43
39
|
}
|
|
44
|
-
|
|
40
|
+
const normalizedHost = normalizeLowercaseStringOrEmpty(parsed.hostname);
|
|
41
|
+
if (!(normalizedHost === "" || normalizedHost === "localhost")) {
|
|
45
42
|
throw new Error(`file:// URLs with remote hosts are not allowed: ${fileUrl}`);
|
|
46
43
|
}
|
|
47
44
|
if (hasEncodedFileUrlSeparator(parsed.pathname)) {
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import type { BigIntStats, Stats } from "node:fs";
|
|
2
2
|
import type { FileHandle } from "node:fs/promises";
|
|
3
|
-
import type {
|
|
4
|
-
import type { SymlinkPolicy } from "./root-symlink-policy.js";
|
|
3
|
+
import type { RootReadOptions } from "./root-options.js";
|
|
5
4
|
type OwnedLocalFile = {
|
|
6
5
|
handle: FileHandle;
|
|
7
6
|
stat: Stats;
|
|
8
7
|
identity: BigIntStats;
|
|
9
8
|
preOpenStat: BigIntStats | undefined;
|
|
10
9
|
};
|
|
11
|
-
export declare function openLocalFileDescriptor(filePath: string, options?: {
|
|
12
|
-
hardlinks?: HardlinkPolicy;
|
|
13
|
-
symlinks?: SymlinkPolicy;
|
|
10
|
+
export declare function openLocalFileDescriptor(filePath: string, options?: Pick<RootReadOptions, "hardlinks" | "symlinks"> & {
|
|
14
11
|
readWrite?: true;
|
|
15
12
|
}): Promise<OwnedLocalFile>;
|
|
16
13
|
export {};
|
package/dist/local-roots.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ReadResult, type RootReadOptions } from "./root.js";
|
|
2
2
|
export type LocalRootsPathResult = {
|
|
3
3
|
path: string;
|
|
4
4
|
root: string;
|
|
@@ -15,11 +15,6 @@ export type ResolveLocalPathFromRootsSyncOptions = LocalRootsInputOptions & {
|
|
|
15
15
|
allowMissing?: boolean;
|
|
16
16
|
requireFile?: boolean;
|
|
17
17
|
};
|
|
18
|
-
export type ReadLocalFileFromRootsOptions = LocalRootsInputOptions &
|
|
19
|
-
hardlinks?: HardlinkPolicy;
|
|
20
|
-
maxBytes?: number;
|
|
21
|
-
nonBlockingRead?: boolean;
|
|
22
|
-
symlinks?: SymlinkPolicy;
|
|
23
|
-
};
|
|
18
|
+
export type ReadLocalFileFromRootsOptions = LocalRootsInputOptions & RootReadOptions;
|
|
24
19
|
export declare function resolveLocalPathFromRootsSync(options: ResolveLocalPathFromRootsSyncOptions): LocalRootsPathResult | null;
|
|
25
20
|
export declare function readLocalFileFromRoots(options: ReadLocalFileFromRootsOptions): Promise<LocalRootsReadResult | null>;
|
|
@@ -135,9 +135,6 @@ async function observeOwnedAliasUnlink(sourcePath, group) {
|
|
|
135
135
|
group.expected = entryIdentity(observed);
|
|
136
136
|
return "removed";
|
|
137
137
|
}
|
|
138
|
-
function mergeCleanupResults(a, b) {
|
|
139
|
-
return a === "stale" || b === "stale" ? "stale" : "removed";
|
|
140
|
-
}
|
|
141
138
|
export async function cleanupCopiedEntry(sourcePath, manifest, state, assertBeforeMutation) {
|
|
142
139
|
if (manifest.kind === "directory") {
|
|
143
140
|
let currentStat;
|
|
@@ -162,7 +159,10 @@ export async function cleanupCopiedEntry(sourcePath, manifest, state, assertBefo
|
|
|
162
159
|
inspectSourceDirectory(sourcePath, manifest.directoryIdentity);
|
|
163
160
|
} : undefined;
|
|
164
161
|
for (const child of manifest.children) {
|
|
165
|
-
|
|
162
|
+
const childResult = await cleanupCopiedEntry(path.join(sourcePath, child.name), child.manifest, state, assertBeforeChildMutation);
|
|
163
|
+
if (childResult === "stale") {
|
|
164
|
+
result = "stale";
|
|
165
|
+
}
|
|
166
166
|
}
|
|
167
167
|
// Child cleanup and the caller's authority check can replace the directory.
|
|
168
168
|
// Keep the final exact observation after both, immediately before removal.
|