@anthropic-ai/sdk 0.118.0 → 0.120.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 +21 -0
- package/client.d.mts +8 -2
- package/client.d.mts.map +1 -1
- package/client.d.ts +8 -2
- package/client.d.ts.map +1 -1
- package/client.js +6 -0
- package/client.js.map +1 -1
- package/client.mjs +6 -0
- package/client.mjs.map +1 -1
- package/helpers/beta/environments.d.mts +5 -4
- package/helpers/beta/environments.d.mts.map +1 -1
- package/helpers/beta/environments.d.ts +5 -4
- package/helpers/beta/environments.d.ts.map +1 -1
- package/helpers/beta/environments.js +5 -4
- package/helpers/beta/environments.js.map +1 -1
- package/helpers/beta/environments.mjs +5 -4
- package/helpers/beta/environments.mjs.map +1 -1
- package/internal/file-store.d.mts +136 -0
- package/internal/file-store.d.mts.map +1 -0
- package/internal/file-store.d.ts +136 -0
- package/internal/file-store.d.ts.map +1 -0
- package/internal/file-store.js +531 -0
- package/internal/file-store.js.map +1 -0
- package/internal/file-store.mjs +523 -0
- package/internal/file-store.mjs.map +1 -0
- package/lib/environments/poller.d.mts +6 -0
- package/lib/environments/poller.d.mts.map +1 -1
- package/lib/environments/poller.d.ts +6 -0
- package/lib/environments/poller.d.ts.map +1 -1
- package/lib/environments/poller.js +46 -1
- package/lib/environments/poller.js.map +1 -1
- package/lib/environments/poller.mjs +46 -1
- package/lib/environments/poller.mjs.map +1 -1
- package/lib/environments/worker.d.mts +89 -12
- package/lib/environments/worker.d.mts.map +1 -1
- package/lib/environments/worker.d.ts +89 -12
- package/lib/environments/worker.d.ts.map +1 -1
- package/lib/environments/worker.js +336 -67
- package/lib/environments/worker.js.map +1 -1
- package/lib/environments/worker.mjs +336 -68
- package/lib/environments/worker.mjs.map +1 -1
- package/lib/tools/BetaRunnableTool.d.mts +4 -1
- package/lib/tools/BetaRunnableTool.d.mts.map +1 -1
- package/lib/tools/BetaRunnableTool.d.ts +4 -1
- package/lib/tools/BetaRunnableTool.d.ts.map +1 -1
- package/lib/tools/BetaRunnableTool.js +7 -2
- package/lib/tools/BetaRunnableTool.js.map +1 -1
- package/lib/tools/BetaRunnableTool.mjs +7 -2
- package/lib/tools/BetaRunnableTool.mjs.map +1 -1
- package/lib/tools/BetaToolRunner.d.mts.map +1 -1
- package/lib/tools/BetaToolRunner.d.ts.map +1 -1
- package/lib/tools/BetaToolRunner.js +3 -1
- package/lib/tools/BetaToolRunner.js.map +1 -1
- package/lib/tools/BetaToolRunner.mjs +3 -1
- package/lib/tools/BetaToolRunner.mjs.map +1 -1
- package/package.json +1 -1
- package/resources/beta/agents/agents.d.mts +322 -27
- package/resources/beta/agents/agents.d.mts.map +1 -1
- package/resources/beta/agents/agents.d.ts +322 -27
- package/resources/beta/agents/agents.d.ts.map +1 -1
- package/resources/beta/agents/agents.js.map +1 -1
- package/resources/beta/agents/agents.mjs.map +1 -1
- package/resources/beta/agents/index.d.mts +1 -1
- package/resources/beta/agents/index.d.mts.map +1 -1
- package/resources/beta/agents/index.d.ts +1 -1
- package/resources/beta/agents/index.d.ts.map +1 -1
- package/resources/beta/agents/index.js.map +1 -1
- package/resources/beta/agents/index.mjs.map +1 -1
- package/resources/beta/beta.d.mts +6 -6
- package/resources/beta/beta.d.mts.map +1 -1
- package/resources/beta/beta.d.ts +6 -6
- package/resources/beta/beta.d.ts.map +1 -1
- package/resources/beta/beta.js.map +1 -1
- package/resources/beta/beta.mjs.map +1 -1
- package/resources/beta/environments/work.d.mts +5 -1
- package/resources/beta/environments/work.d.mts.map +1 -1
- package/resources/beta/environments/work.d.ts +5 -1
- package/resources/beta/environments/work.d.ts.map +1 -1
- package/resources/beta/environments/work.js +0 -17
- package/resources/beta/environments/work.js.map +1 -1
- package/resources/beta/environments/work.mjs +0 -17
- package/resources/beta/environments/work.mjs.map +1 -1
- package/resources/beta/files.d.mts +22 -22
- package/resources/beta/files.d.mts.map +1 -1
- package/resources/beta/files.d.ts +22 -22
- package/resources/beta/files.d.ts.map +1 -1
- package/resources/beta/files.js +4 -4
- package/resources/beta/files.js.map +1 -1
- package/resources/beta/files.mjs +4 -4
- package/resources/beta/files.mjs.map +1 -1
- package/resources/beta/index.d.mts +3 -3
- package/resources/beta/index.d.mts.map +1 -1
- package/resources/beta/index.d.ts +3 -3
- package/resources/beta/index.d.ts.map +1 -1
- package/resources/beta/index.js.map +1 -1
- package/resources/beta/index.mjs.map +1 -1
- package/resources/beta/messages/index.d.mts +1 -1
- package/resources/beta/messages/index.d.mts.map +1 -1
- package/resources/beta/messages/index.d.ts +1 -1
- package/resources/beta/messages/index.d.ts.map +1 -1
- package/resources/beta/messages/index.js.map +1 -1
- package/resources/beta/messages/index.mjs.map +1 -1
- package/resources/beta/messages/messages.d.mts +1202 -5
- package/resources/beta/messages/messages.d.mts.map +1 -1
- package/resources/beta/messages/messages.d.ts +1202 -5
- package/resources/beta/messages/messages.d.ts.map +1 -1
- package/resources/beta/messages/messages.js.map +1 -1
- package/resources/beta/messages/messages.mjs.map +1 -1
- package/resources/files.d.mts +105 -0
- package/resources/files.d.mts.map +1 -0
- package/resources/files.d.ts +105 -0
- package/resources/files.d.ts.map +1 -0
- package/resources/files.js +52 -0
- package/resources/files.js.map +1 -0
- package/resources/files.mjs +48 -0
- package/resources/files.mjs.map +1 -0
- package/resources/index.d.mts +3 -1
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +3 -1
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js +5 -1
- package/resources/index.js.map +1 -1
- package/resources/index.mjs +2 -0
- package/resources/index.mjs.map +1 -1
- package/resources/messages/index.d.mts +1 -1
- package/resources/messages/index.d.mts.map +1 -1
- package/resources/messages/index.d.ts +1 -1
- package/resources/messages/index.d.ts.map +1 -1
- package/resources/messages/index.js.map +1 -1
- package/resources/messages/index.mjs.map +1 -1
- package/resources/messages/messages.d.mts +1267 -7
- package/resources/messages/messages.d.mts.map +1 -1
- package/resources/messages/messages.d.ts +1267 -7
- package/resources/messages/messages.d.ts.map +1 -1
- package/resources/messages/messages.js.map +1 -1
- package/resources/messages/messages.mjs.map +1 -1
- package/resources/skills/index.d.mts +3 -0
- package/resources/skills/index.d.mts.map +1 -0
- package/resources/skills/index.d.ts +3 -0
- package/resources/skills/index.d.ts.map +1 -0
- package/resources/skills/index.js +9 -0
- package/resources/skills/index.js.map +1 -0
- package/resources/skills/index.mjs +4 -0
- package/resources/skills/index.mjs.map +1 -0
- package/resources/skills/skills.d.mts +129 -0
- package/resources/skills/skills.d.mts.map +1 -0
- package/resources/skills/skills.d.ts +129 -0
- package/resources/skills/skills.d.ts.map +1 -0
- package/resources/skills/skills.js +44 -0
- package/resources/skills/skills.js.map +1 -0
- package/resources/skills/skills.mjs +39 -0
- package/resources/skills/skills.mjs.map +1 -0
- package/resources/skills/versions.d.mts +104 -0
- package/resources/skills/versions.d.mts.map +1 -0
- package/resources/skills/versions.d.ts +104 -0
- package/resources/skills/versions.d.ts.map +1 -0
- package/resources/skills/versions.js +41 -0
- package/resources/skills/versions.js.map +1 -0
- package/resources/skills/versions.mjs +37 -0
- package/resources/skills/versions.mjs.map +1 -0
- package/resources/skills.d.mts +2 -0
- package/resources/skills.d.mts.map +1 -0
- package/resources/skills.d.ts +2 -0
- package/resources/skills.d.ts.map +1 -0
- package/resources/skills.js +6 -0
- package/resources/skills.js.map +1 -0
- package/resources/skills.mjs +3 -0
- package/resources/skills.mjs.map +1 -0
- package/src/client.ts +172 -0
- package/src/helpers/beta/environments.ts +5 -4
- package/src/internal/file-store.ts +550 -0
- package/src/lib/environments/poller.ts +47 -1
- package/src/lib/environments/worker.ts +419 -79
- package/src/lib/tools/BetaRunnableTool.ts +9 -2
- package/src/lib/tools/BetaToolRunner.ts +6 -1
- package/src/resources/beta/agents/agents.ts +435 -30
- package/src/resources/beta/agents/index.ts +17 -0
- package/src/resources/beta/beta.ts +160 -6
- package/src/resources/beta/environments/work.ts +7 -0
- package/src/resources/beta/files.ts +27 -27
- package/src/resources/beta/index.ts +80 -3
- package/src/resources/beta/messages/index.ts +60 -0
- package/src/resources/beta/messages/messages.ts +1865 -415
- package/src/resources/files.ts +163 -0
- package/src/resources/index.ts +83 -0
- package/src/resources/messages/index.ts +66 -0
- package/src/resources/messages/messages.ts +1648 -115
- package/src/resources/skills/index.ts +21 -0
- package/src/resources/skills/skills.ts +194 -0
- package/src/resources/skills/versions.ts +160 -0
- package/src/resources/skills.ts +3 -0
- package/src/tools/agent-toolset/fs-util.ts +40 -24
- package/src/tools/agent-toolset/memories.ts +1017 -0
- package/src/tools/agent-toolset/node.browser.ts +68 -0
- package/src/tools/agent-toolset/node.ts +139 -53
- package/src/tools/agent-toolset/skills.ts +29 -9
- package/src/tools/agent-toolset/sync-interval.ts +30 -0
- package/src/version.ts +1 -1
- package/tools/agent-toolset/fs-util.d.mts +23 -10
- package/tools/agent-toolset/fs-util.d.mts.map +1 -1
- package/tools/agent-toolset/fs-util.d.ts +23 -10
- package/tools/agent-toolset/fs-util.d.ts.map +1 -1
- package/tools/agent-toolset/fs-util.js +40 -25
- package/tools/agent-toolset/fs-util.js.map +1 -1
- package/tools/agent-toolset/fs-util.mjs +38 -25
- package/tools/agent-toolset/fs-util.mjs.map +1 -1
- package/tools/agent-toolset/memories.d.mts +173 -0
- package/tools/agent-toolset/memories.d.mts.map +1 -0
- package/tools/agent-toolset/memories.d.ts +173 -0
- package/tools/agent-toolset/memories.d.ts.map +1 -0
- package/tools/agent-toolset/memories.js +902 -0
- package/tools/agent-toolset/memories.js.map +1 -0
- package/tools/agent-toolset/memories.mjs +895 -0
- package/tools/agent-toolset/memories.mjs.map +1 -0
- package/tools/agent-toolset/node.browser.d.mts +31 -0
- package/tools/agent-toolset/node.browser.d.mts.map +1 -1
- package/tools/agent-toolset/node.browser.d.ts +31 -0
- package/tools/agent-toolset/node.browser.d.ts.map +1 -1
- package/tools/agent-toolset/node.browser.js +59 -1
- package/tools/agent-toolset/node.browser.js.map +1 -1
- package/tools/agent-toolset/node.browser.mjs +54 -0
- package/tools/agent-toolset/node.browser.mjs.map +1 -1
- package/tools/agent-toolset/node.d.mts +54 -22
- package/tools/agent-toolset/node.d.mts.map +1 -1
- package/tools/agent-toolset/node.d.ts +54 -22
- package/tools/agent-toolset/node.d.ts.map +1 -1
- package/tools/agent-toolset/node.js +83 -44
- package/tools/agent-toolset/node.js.map +1 -1
- package/tools/agent-toolset/node.mjs +77 -44
- package/tools/agent-toolset/node.mjs.map +1 -1
- package/tools/agent-toolset/skills.d.mts +16 -7
- package/tools/agent-toolset/skills.d.mts.map +1 -1
- package/tools/agent-toolset/skills.d.ts +16 -7
- package/tools/agent-toolset/skills.d.ts.map +1 -1
- package/tools/agent-toolset/skills.js +27 -9
- package/tools/agent-toolset/skills.js.map +1 -1
- package/tools/agent-toolset/skills.mjs +27 -9
- package/tools/agent-toolset/skills.mjs.map +1 -1
- package/tools/agent-toolset/sync-interval.d.mts +19 -0
- package/tools/agent-toolset/sync-interval.d.mts.map +1 -0
- package/tools/agent-toolset/sync-interval.d.ts +19 -0
- package/tools/agent-toolset/sync-interval.d.ts.map +1 -0
- package/tools/agent-toolset/sync-interval.js +29 -0
- package/tools/agent-toolset/sync-interval.js.map +1 -0
- package/tools/agent-toolset/sync-interval.mjs +25 -0
- package/tools/agent-toolset/sync-interval.mjs.map +1 -0
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `FileStore` — one confined folder; a relative path cannot escape it.
|
|
3
|
+
*
|
|
4
|
+
* Beta scope: symlinks are refused or skipped wherever the store meets them,
|
|
5
|
+
* but there is no hardening against a process racing the store's own
|
|
6
|
+
* syscalls; fsync durability, non-POSIX hosts, and read-size caps are out of
|
|
7
|
+
* scope.
|
|
8
|
+
*/
|
|
9
|
+
/** A refused operation — input the store will not act on. OS errors propagate with their `.code`. */
|
|
10
|
+
export declare class FileStoreError extends Error {
|
|
11
|
+
static readonly ESCAPES_ROOT = "escapes the store root";
|
|
12
|
+
static readonly IS_A_SYMLINK = "is a symlink";
|
|
13
|
+
static readonly NOT_A_FILE = "is not a regular file";
|
|
14
|
+
static readonly NOT_A_DIRECTORY = "is not a directory";
|
|
15
|
+
static readonly NOT_UTF8 = "is not valid utf-8";
|
|
16
|
+
static readonly MOVE_DESTINATION_EXISTS = "already exists";
|
|
17
|
+
readonly reason: string;
|
|
18
|
+
readonly relPath: string;
|
|
19
|
+
constructor(reason: string, relPath: string);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The store's resolved root, and what {@link FileStore.dispose} will do to it.
|
|
23
|
+
*
|
|
24
|
+
* `removedOnDispose` is true when `open` found no root, so `dispose` removes
|
|
25
|
+
* it. A root that was already there is someone else's — a pre-seeded mount, a
|
|
26
|
+
* caller's workdir — and is kept.
|
|
27
|
+
*/
|
|
28
|
+
export type Root = {
|
|
29
|
+
path: string;
|
|
30
|
+
removedOnDispose: boolean;
|
|
31
|
+
};
|
|
32
|
+
/** Options for {@link FileStore.open} / {@link openFileStore}. */
|
|
33
|
+
export interface OpenFileStoreOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Restrict the store to valid UTF-8 (default `false`): a `put` of binary
|
|
36
|
+
* bytes and a `get` of a binary file are refused with
|
|
37
|
+
* {@link FileStoreError}, so a caller that decodes what `get` returns can
|
|
38
|
+
* never hit a decode error.
|
|
39
|
+
*/
|
|
40
|
+
utf8?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** Resolve `root`; creates nothing — only {@link FileStore.createRoot} makes the folder. */
|
|
43
|
+
export declare function openFileStore(root: string, opts?: OpenFileStoreOptions): Promise<FileStore>;
|
|
44
|
+
/**
|
|
45
|
+
* True for a path usable verbatim as a store location: absolute, with no `..`
|
|
46
|
+
* components. Paths are judged in POSIX terms — they are wire values naming
|
|
47
|
+
* locations inside a POSIX container, not host-native paths.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isPathLegal(p: string): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* One confined folder of regular files.
|
|
52
|
+
*
|
|
53
|
+
* Every `relPath` is relative to the root (a leading `/` also means the root)
|
|
54
|
+
* and refused with {@link FileStoreError} when it escapes. The store holds
|
|
55
|
+
* regular files only: symlinks are refused on read and skipped by listings —
|
|
56
|
+
* {@link findSymlinks} reports them. A `relPath` resolving to the root itself
|
|
57
|
+
* is banned by this interface: `put` and `get` refuse it, `move` and `remove`
|
|
58
|
+
* do nothing. A store opened with `utf8: true` refuses binary content the
|
|
59
|
+
* same way — on `put` of such bytes and on `get` of such a file. Only
|
|
60
|
+
* {@link createRoot} makes the root: writes create directories below it,
|
|
61
|
+
* never the root itself, so a root removed while the store is open stays
|
|
62
|
+
* removed and the write fails with `ENOENT`.
|
|
63
|
+
*/
|
|
64
|
+
export declare class FileStore {
|
|
65
|
+
private readonly rootPath;
|
|
66
|
+
/** True ⇒ this open created the root, so {@link dispose} removes it. */
|
|
67
|
+
private readonly removedOnDispose;
|
|
68
|
+
/** Set iff the store refuses non-UTF-8 content; {@link requireUtf8} decodes against it, on both put and get. */
|
|
69
|
+
private readonly decoder;
|
|
70
|
+
/** `hashtree`'s advisory cache; every hit re-validates against a fresh stat. */
|
|
71
|
+
private readonly hashes;
|
|
72
|
+
static isPathLegal: typeof isPathLegal;
|
|
73
|
+
/** @internal — use {@link FileStore.open} / {@link openFileStore}. */
|
|
74
|
+
constructor(root: string, removedOnDispose: boolean, utf8Only?: boolean);
|
|
75
|
+
/** Resolve `root`; creates nothing — only {@link createRoot} makes the folder. */
|
|
76
|
+
static open(root: string, opts?: OpenFileStoreOptions): Promise<FileStore>;
|
|
77
|
+
/** Create the root directory and any missing ancestors; already existing is fine. */
|
|
78
|
+
createRoot(): Promise<void>;
|
|
79
|
+
/** The resolved root, and what {@link dispose} will do to it. */
|
|
80
|
+
root(): Root;
|
|
81
|
+
/**
|
|
82
|
+
* Remove the root iff `open` created it; pre-existing roots are kept.
|
|
83
|
+
*
|
|
84
|
+
* Wired to `Symbol.asyncDispose` at runtime when the host provides it, so
|
|
85
|
+
* `await using` works on engines with explicit resource management.
|
|
86
|
+
*/
|
|
87
|
+
dispose(): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Write `data` (`string` UTF-8 or bytes) atomically to the file at `relPath`.
|
|
90
|
+
*
|
|
91
|
+
* Missing directories below the root are created; a missing root is not —
|
|
92
|
+
* the write fails with `ENOENT`.
|
|
93
|
+
*/
|
|
94
|
+
put(relPath: string, data: string | Uint8Array, opts?: {
|
|
95
|
+
executable?: boolean;
|
|
96
|
+
}): Promise<void>;
|
|
97
|
+
/** The file's bytes; `null` when absent. */
|
|
98
|
+
get(relPath: string): Promise<Uint8Array | null>;
|
|
99
|
+
/** The relative path of every file under the directory `under`. */
|
|
100
|
+
ls(under?: string): Promise<Set<string>>;
|
|
101
|
+
/**
|
|
102
|
+
* Every symlink under `under` — listings skip them and reads refuse them,
|
|
103
|
+
* so a caller that must know they exist asks here.
|
|
104
|
+
*/
|
|
105
|
+
findSymlinks(under?: string): Promise<Set<string>>;
|
|
106
|
+
/**
|
|
107
|
+
* `{relPath: sha256Hex}` of every file under the directory `under`.
|
|
108
|
+
*
|
|
109
|
+
* Unchanged files — same size, mtime, and ctime since the last call —
|
|
110
|
+
* reuse their recorded hash instead of being re-read.
|
|
111
|
+
*/
|
|
112
|
+
hashtree(under?: string): Promise<Record<string, string>>;
|
|
113
|
+
/** One file's sha256; `null` when absent. Shares {@link hashtree}'s cache. */
|
|
114
|
+
hashFile(relPath: string): Promise<string | null>;
|
|
115
|
+
/**
|
|
116
|
+
* Rename `src` to `dst`; an existing `dst` is refused. The banned store
|
|
117
|
+
* root as either end does nothing.
|
|
118
|
+
*/
|
|
119
|
+
move(src: string, dst: string): Promise<void>;
|
|
120
|
+
/** Delete a file or subtree; absent — and the banned store root — do nothing. */
|
|
121
|
+
remove(relPath: string): Promise<void>;
|
|
122
|
+
private resolveUnderRoot;
|
|
123
|
+
private requireUtf8;
|
|
124
|
+
private hashViaCache;
|
|
125
|
+
}
|
|
126
|
+
/** sha256 of a file's contents, streamed — constant memory on any file size. */
|
|
127
|
+
declare function hashFile(full: string): Promise<string>;
|
|
128
|
+
/** Test seam — the hasher, the trust margin, and the walk clock. @internal */
|
|
129
|
+
export declare const _internals: {
|
|
130
|
+
hashFile: typeof hashFile;
|
|
131
|
+
timestampTrustMarginNs: bigint;
|
|
132
|
+
nowNs: () => bigint;
|
|
133
|
+
};
|
|
134
|
+
export declare const LocalFileStore: typeof FileStore;
|
|
135
|
+
export {};
|
|
136
|
+
//# sourceMappingURL=file-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/internal/file-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `FileStore` — one confined folder; a relative path cannot escape it.
|
|
4
|
+
*
|
|
5
|
+
* Beta scope: symlinks are refused or skipped wherever the store meets them,
|
|
6
|
+
* but there is no hardening against a process racing the store's own
|
|
7
|
+
* syscalls; fsync durability, non-POSIX hosts, and read-size caps are out of
|
|
8
|
+
* scope.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.LocalFileStore = exports._internals = exports.FileStore = exports.FileStoreError = void 0;
|
|
12
|
+
exports.openFileStore = openFileStore;
|
|
13
|
+
exports.isPathLegal = isPathLegal;
|
|
14
|
+
const tslib_1 = require("./tslib.js");
|
|
15
|
+
const fsp = tslib_1.__importStar(require("node:fs/promises"));
|
|
16
|
+
const path = tslib_1.__importStar(require("node:path"));
|
|
17
|
+
const node_fs_1 = require("node:fs");
|
|
18
|
+
const node_crypto_1 = require("node:crypto");
|
|
19
|
+
const bytes_1 = require("./utils/bytes.js");
|
|
20
|
+
// Owner-only regardless of umask: the store holds downloaded user/model content.
|
|
21
|
+
const OWNER_ONLY_DIR_MODE = 0o700;
|
|
22
|
+
const OWNER_ONLY_FILE_MODE = 0o600;
|
|
23
|
+
const OWNER_ONLY_EXEC_MODE = 0o700;
|
|
24
|
+
// 0 where the platform lacks them; `open` refuses such platforms.
|
|
25
|
+
const O_NOFOLLOW = node_fs_1.constants.O_NOFOLLOW ?? 0;
|
|
26
|
+
const O_NONBLOCK = node_fs_1.constants.O_NONBLOCK ?? 0;
|
|
27
|
+
/** A refused operation — input the store will not act on. OS errors propagate with their `.code`. */
|
|
28
|
+
class FileStoreError extends Error {
|
|
29
|
+
constructor(reason, relPath) {
|
|
30
|
+
super(`path ${JSON.stringify(relPath)} ${reason}`);
|
|
31
|
+
this.name = 'FileStoreError';
|
|
32
|
+
this.reason = reason;
|
|
33
|
+
this.relPath = relPath;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.FileStoreError = FileStoreError;
|
|
37
|
+
FileStoreError.ESCAPES_ROOT = 'escapes the store root';
|
|
38
|
+
FileStoreError.IS_A_SYMLINK = 'is a symlink';
|
|
39
|
+
FileStoreError.NOT_A_FILE = 'is not a regular file';
|
|
40
|
+
FileStoreError.NOT_A_DIRECTORY = 'is not a directory';
|
|
41
|
+
FileStoreError.NOT_UTF8 = 'is not valid utf-8';
|
|
42
|
+
FileStoreError.MOVE_DESTINATION_EXISTS = 'already exists';
|
|
43
|
+
/** Resolve `root`; creates nothing — only {@link FileStore.createRoot} makes the folder. */
|
|
44
|
+
async function openFileStore(root, opts) {
|
|
45
|
+
return FileStore.open(root, opts);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* True for a path usable verbatim as a store location: absolute, with no `..`
|
|
49
|
+
* components. Paths are judged in POSIX terms — they are wire values naming
|
|
50
|
+
* locations inside a POSIX container, not host-native paths.
|
|
51
|
+
*/
|
|
52
|
+
function isPathLegal(p) {
|
|
53
|
+
return p.startsWith('/') && !p.split('/').includes('..');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* One confined folder of regular files.
|
|
57
|
+
*
|
|
58
|
+
* Every `relPath` is relative to the root (a leading `/` also means the root)
|
|
59
|
+
* and refused with {@link FileStoreError} when it escapes. The store holds
|
|
60
|
+
* regular files only: symlinks are refused on read and skipped by listings —
|
|
61
|
+
* {@link findSymlinks} reports them. A `relPath` resolving to the root itself
|
|
62
|
+
* is banned by this interface: `put` and `get` refuse it, `move` and `remove`
|
|
63
|
+
* do nothing. A store opened with `utf8: true` refuses binary content the
|
|
64
|
+
* same way — on `put` of such bytes and on `get` of such a file. Only
|
|
65
|
+
* {@link createRoot} makes the root: writes create directories below it,
|
|
66
|
+
* never the root itself, so a root removed while the store is open stays
|
|
67
|
+
* removed and the write fails with `ENOENT`.
|
|
68
|
+
*/
|
|
69
|
+
class FileStore {
|
|
70
|
+
/** @internal — use {@link FileStore.open} / {@link openFileStore}. */
|
|
71
|
+
constructor(root, removedOnDispose, utf8Only = false) {
|
|
72
|
+
/** `hashtree`'s advisory cache; every hit re-validates against a fresh stat. */
|
|
73
|
+
this.hashes = new Map();
|
|
74
|
+
this.rootPath = root;
|
|
75
|
+
this.removedOnDispose = removedOnDispose;
|
|
76
|
+
this.decoder = utf8Only ? new TextDecoder('utf-8', { fatal: true }) : undefined;
|
|
77
|
+
}
|
|
78
|
+
/** Resolve `root`; creates nothing — only {@link createRoot} makes the folder. */
|
|
79
|
+
static async open(root, opts) {
|
|
80
|
+
// A deployment condition, not refused caller input — hence not FileStoreError.
|
|
81
|
+
if (!platformSupported()) {
|
|
82
|
+
throw new Error('FileStore requires O_NOFOLLOW support on this platform');
|
|
83
|
+
}
|
|
84
|
+
let removedOnDispose = false;
|
|
85
|
+
try {
|
|
86
|
+
// lstat, not a follow-and-swallow existence check: following symlinks or
|
|
87
|
+
// swallowing permission errors would mark a real directory ours to
|
|
88
|
+
// delete on dispose.
|
|
89
|
+
await fsp.lstat(root);
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
if (e.code !== 'ENOENT')
|
|
93
|
+
throw e;
|
|
94
|
+
removedOnDispose = true;
|
|
95
|
+
}
|
|
96
|
+
return new FileStore(path.resolve(root), removedOnDispose, opts?.utf8 ?? false);
|
|
97
|
+
}
|
|
98
|
+
/** Create the root directory and any missing ancestors; already existing is fine. */
|
|
99
|
+
async createRoot() {
|
|
100
|
+
await makeDirAndAncestors(this.rootPath);
|
|
101
|
+
}
|
|
102
|
+
/** The resolved root, and what {@link dispose} will do to it. */
|
|
103
|
+
root() {
|
|
104
|
+
return { path: this.rootPath, removedOnDispose: this.removedOnDispose };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Remove the root iff `open` created it; pre-existing roots are kept.
|
|
108
|
+
*
|
|
109
|
+
* Wired to `Symbol.asyncDispose` at runtime when the host provides it, so
|
|
110
|
+
* `await using` works on engines with explicit resource management.
|
|
111
|
+
*/
|
|
112
|
+
async dispose() {
|
|
113
|
+
if (!this.removedOnDispose)
|
|
114
|
+
return;
|
|
115
|
+
await fsp.rm(this.rootPath, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Write `data` (`string` UTF-8 or bytes) atomically to the file at `relPath`.
|
|
119
|
+
*
|
|
120
|
+
* Missing directories below the root are created; a missing root is not —
|
|
121
|
+
* the write fails with `ENOENT`.
|
|
122
|
+
*/
|
|
123
|
+
async put(relPath, data, opts) {
|
|
124
|
+
// "dir/." names a directory just like a trailing "/".
|
|
125
|
+
const tail = relPath.replace(/\\/g, '/');
|
|
126
|
+
if (tail.endsWith('/') || tail.endsWith('/.') || tail === '' || tail === '.') {
|
|
127
|
+
throw new FileStoreError(FileStoreError.NOT_A_FILE, relPath);
|
|
128
|
+
}
|
|
129
|
+
const dest = this.resolveUnderRoot(relPath);
|
|
130
|
+
const payload = typeof data === 'string' ? (0, bytes_1.encodeUTF8)(data) : data;
|
|
131
|
+
this.requireUtf8(relPath, payload);
|
|
132
|
+
await makeDirsBelowRoot(this.rootPath, path.dirname(dest));
|
|
133
|
+
await replaceViaTemp(dest, payload, opts?.executable ?? false);
|
|
134
|
+
}
|
|
135
|
+
/** The file's bytes; `null` when absent. */
|
|
136
|
+
async get(relPath) {
|
|
137
|
+
const dest = this.resolveUnderRoot(relPath);
|
|
138
|
+
let handle;
|
|
139
|
+
try {
|
|
140
|
+
handle = await openRegularFile(relPath, dest);
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
if (e.code === 'ENOENT')
|
|
144
|
+
return null;
|
|
145
|
+
throw e;
|
|
146
|
+
}
|
|
147
|
+
let data;
|
|
148
|
+
try {
|
|
149
|
+
const buf = await handle.readFile();
|
|
150
|
+
data = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
await handle.close();
|
|
154
|
+
}
|
|
155
|
+
this.requireUtf8(relPath, data);
|
|
156
|
+
return data;
|
|
157
|
+
}
|
|
158
|
+
/** The relative path of every file under the directory `under`. */
|
|
159
|
+
async ls(under = '/') {
|
|
160
|
+
const base = this.resolveUnderRoot(under);
|
|
161
|
+
return new Set((await filenamesInDir(this.rootPath, under, base)).map(([rel]) => rel));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Every symlink under `under` — listings skip them and reads refuse them,
|
|
165
|
+
* so a caller that must know they exist asks here.
|
|
166
|
+
*/
|
|
167
|
+
async findSymlinks(under = '/') {
|
|
168
|
+
const base = this.resolveUnderRoot(under);
|
|
169
|
+
return symlinksInDir(this.rootPath, under, base);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* `{relPath: sha256Hex}` of every file under the directory `under`.
|
|
173
|
+
*
|
|
174
|
+
* Unchanged files — same size, mtime, and ctime since the last call —
|
|
175
|
+
* reuse their recorded hash instead of being re-read.
|
|
176
|
+
*/
|
|
177
|
+
async hashtree(under = '/') {
|
|
178
|
+
const base = this.resolveUnderRoot(under);
|
|
179
|
+
const walkStartNs = exports._internals.nowNs();
|
|
180
|
+
// Null prototype so a file named `__proto__` (or `constructor`) is an
|
|
181
|
+
// ordinary own key instead of a silent prototype write / inherited read.
|
|
182
|
+
const out = Object.create(null);
|
|
183
|
+
for (const [rel, full] of await filenamesInDir(this.rootPath, under, base)) {
|
|
184
|
+
const sha = await this.hashViaCache(rel, full, walkStartNs);
|
|
185
|
+
if (sha !== null)
|
|
186
|
+
out[rel] = sha;
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
/** One file's sha256; `null` when absent. Shares {@link hashtree}'s cache. */
|
|
191
|
+
async hashFile(relPath) {
|
|
192
|
+
const dest = this.resolveUnderRoot(relPath);
|
|
193
|
+
let st;
|
|
194
|
+
try {
|
|
195
|
+
st = await fsp.lstat(dest, { bigint: true });
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
if (e.code === 'ENOENT')
|
|
199
|
+
return null;
|
|
200
|
+
throw e;
|
|
201
|
+
}
|
|
202
|
+
if (st.isSymbolicLink())
|
|
203
|
+
throw new FileStoreError(FileStoreError.IS_A_SYMLINK, relPath);
|
|
204
|
+
if (!st.isFile())
|
|
205
|
+
throw new FileStoreError(FileStoreError.NOT_A_FILE, relPath);
|
|
206
|
+
const rel = path.relative(this.rootPath, dest).split(path.sep).join('/');
|
|
207
|
+
return this.hashViaCache(rel, dest, exports._internals.nowNs());
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Rename `src` to `dst`; an existing `dst` is refused. The banned store
|
|
211
|
+
* root as either end does nothing.
|
|
212
|
+
*/
|
|
213
|
+
async move(src, dst) {
|
|
214
|
+
const s = this.resolveUnderRoot(src);
|
|
215
|
+
const d = this.resolveUnderRoot(dst);
|
|
216
|
+
if (s === this.rootPath || d === this.rootPath)
|
|
217
|
+
return;
|
|
218
|
+
// stat, not lstat: a dangling symlink at dst reads as absent and is
|
|
219
|
+
// atomically replaced by the rename, like any other rename target.
|
|
220
|
+
const dstExists = await fsp.stat(d).then(() => true, () => false);
|
|
221
|
+
if (dstExists)
|
|
222
|
+
throw new FileStoreError(FileStoreError.MOVE_DESTINATION_EXISTS, dst);
|
|
223
|
+
await makeDirsBelowRoot(this.rootPath, path.dirname(d));
|
|
224
|
+
await fsp.rename(s, d);
|
|
225
|
+
}
|
|
226
|
+
/** Delete a file or subtree; absent — and the banned store root — do nothing. */
|
|
227
|
+
async remove(relPath) {
|
|
228
|
+
const dest = this.resolveUnderRoot(relPath);
|
|
229
|
+
if (dest === this.rootPath)
|
|
230
|
+
return;
|
|
231
|
+
let st;
|
|
232
|
+
try {
|
|
233
|
+
// lstat: a dangling symlink must still be unlinked.
|
|
234
|
+
st = await fsp.lstat(dest, { bigint: true });
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
if (e.code === 'ENOENT')
|
|
238
|
+
return;
|
|
239
|
+
throw e;
|
|
240
|
+
}
|
|
241
|
+
if (st.isDirectory()) {
|
|
242
|
+
await fsp.rm(dest, { recursive: true, force: true });
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
try {
|
|
246
|
+
await fsp.unlink(dest);
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
if (e.code !== 'ENOENT')
|
|
250
|
+
throw e;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
resolveUnderRoot(relPath) {
|
|
255
|
+
const norm = relPath.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
256
|
+
const parts = norm.split('/').filter((p) => p !== '' && p !== '.');
|
|
257
|
+
if (path.posix.isAbsolute(norm) || parts.includes('..')) {
|
|
258
|
+
throw new FileStoreError(FileStoreError.ESCAPES_ROOT, relPath);
|
|
259
|
+
}
|
|
260
|
+
return parts.length === 0 ? this.rootPath : path.join(this.rootPath, ...parts);
|
|
261
|
+
}
|
|
262
|
+
requireUtf8(relPath, data) {
|
|
263
|
+
if (!this.decoder)
|
|
264
|
+
return;
|
|
265
|
+
try {
|
|
266
|
+
this.decoder.decode(data);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
throw new FileStoreError(FileStoreError.NOT_UTF8, relPath);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async hashViaCache(rel, full, walkStartNs) {
|
|
273
|
+
let st;
|
|
274
|
+
try {
|
|
275
|
+
st = await fsp.lstat(full, { bigint: true });
|
|
276
|
+
}
|
|
277
|
+
catch (e) {
|
|
278
|
+
if (e.code === 'ENOENT')
|
|
279
|
+
return null; // vanished since the walk: not in this snapshot
|
|
280
|
+
throw e;
|
|
281
|
+
}
|
|
282
|
+
if (!st.isFile())
|
|
283
|
+
return null;
|
|
284
|
+
const cached = this.hashes.get(rel);
|
|
285
|
+
let sha;
|
|
286
|
+
if (cached !== undefined && unchangedSinceHashed(cached, st)) {
|
|
287
|
+
sha = cached.sha;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
try {
|
|
291
|
+
sha = await exports._internals.hashFile(full);
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
294
|
+
const code = e.code;
|
|
295
|
+
if (code === 'ENOENT' || e instanceof FileStoreError)
|
|
296
|
+
return null;
|
|
297
|
+
// FreeBSD reports EMLINK rather than ELOOP for O_NOFOLLOW.
|
|
298
|
+
if (code === 'ELOOP' || code === 'EMLINK')
|
|
299
|
+
return null;
|
|
300
|
+
throw e;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (oldEnoughToCache(st, walkStartNs)) {
|
|
304
|
+
this.hashes.set(rel, { mtimeNs: st.mtimeNs, ctimeNs: st.ctimeNs, size: st.size, sha });
|
|
305
|
+
}
|
|
306
|
+
return sha;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
exports.FileStore = FileStore;
|
|
310
|
+
FileStore.isPathLegal = isPathLegal;
|
|
311
|
+
function platformSupported() {
|
|
312
|
+
return O_NOFOLLOW !== 0;
|
|
313
|
+
}
|
|
314
|
+
async function makeDirAndAncestors(dir) {
|
|
315
|
+
const missing = [];
|
|
316
|
+
let current = dir;
|
|
317
|
+
for (;;) {
|
|
318
|
+
try {
|
|
319
|
+
await fsp.stat(current);
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
catch (e) {
|
|
323
|
+
const code = e.code;
|
|
324
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR' && code !== 'ELOOP')
|
|
325
|
+
throw e;
|
|
326
|
+
}
|
|
327
|
+
missing.push(current);
|
|
328
|
+
const parent = path.dirname(current);
|
|
329
|
+
if (parent === current)
|
|
330
|
+
break;
|
|
331
|
+
current = parent;
|
|
332
|
+
}
|
|
333
|
+
for (const directory of missing.reverse()) {
|
|
334
|
+
try {
|
|
335
|
+
await fsp.mkdir(directory, { mode: OWNER_ONLY_DIR_MODE });
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
if (e.code !== 'EEXIST')
|
|
339
|
+
throw e;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function makeDirsBelowRoot(root, dir) {
|
|
344
|
+
// Never the root itself: only createRoot() makes it, so a write racing an
|
|
345
|
+
// rm -rf of the folder fails with ENOENT instead of re-creating it.
|
|
346
|
+
const below = path.relative(root, dir);
|
|
347
|
+
if (below === '')
|
|
348
|
+
return;
|
|
349
|
+
let current = root;
|
|
350
|
+
for (const part of below.split(path.sep)) {
|
|
351
|
+
current = path.join(current, part);
|
|
352
|
+
try {
|
|
353
|
+
await fsp.mkdir(current, { mode: OWNER_ONLY_DIR_MODE });
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
if (e.code !== 'EEXIST')
|
|
357
|
+
throw e;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function replaceViaTemp(dest, data, isExecutable) {
|
|
362
|
+
const mode = isExecutable ? OWNER_ONLY_EXEC_MODE : OWNER_ONLY_FILE_MODE;
|
|
363
|
+
const tmp = path.join(path.dirname(dest), `.fs-${(0, node_crypto_1.randomBytes)(8).toString('hex')}.tmp`);
|
|
364
|
+
let handle;
|
|
365
|
+
try {
|
|
366
|
+
handle = await fsp.open(tmp, node_fs_1.constants.O_WRONLY | node_fs_1.constants.O_CREAT | node_fs_1.constants.O_EXCL | O_NOFOLLOW, mode);
|
|
367
|
+
await handle.writeFile(data);
|
|
368
|
+
await handle.close();
|
|
369
|
+
handle = undefined;
|
|
370
|
+
await fsp.rename(tmp, dest);
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
// Best-effort temp cleanup; never mask the original error.
|
|
374
|
+
if (handle)
|
|
375
|
+
await handle.close().catch(() => { });
|
|
376
|
+
await fsp.unlink(tmp).catch(() => { });
|
|
377
|
+
throw err;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async function openRegularFile(relPath, dest) {
|
|
381
|
+
// O_NONBLOCK: a FIFO fails the fstat check below instead of blocking the open.
|
|
382
|
+
let handle;
|
|
383
|
+
try {
|
|
384
|
+
handle = await fsp.open(dest, node_fs_1.constants.O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
385
|
+
}
|
|
386
|
+
catch (e) {
|
|
387
|
+
// FreeBSD reports EMLINK rather than ELOOP for O_NOFOLLOW.
|
|
388
|
+
const code = e.code;
|
|
389
|
+
if (code === 'ELOOP' || code === 'EMLINK') {
|
|
390
|
+
throw new FileStoreError(FileStoreError.IS_A_SYMLINK, relPath);
|
|
391
|
+
}
|
|
392
|
+
throw e;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const st = await handle.stat();
|
|
396
|
+
if (!st.isFile())
|
|
397
|
+
throw new FileStoreError(FileStoreError.NOT_A_FILE, relPath);
|
|
398
|
+
}
|
|
399
|
+
catch (e) {
|
|
400
|
+
await handle.close().catch(() => { });
|
|
401
|
+
throw e;
|
|
402
|
+
}
|
|
403
|
+
return handle;
|
|
404
|
+
}
|
|
405
|
+
/** sha256 of a file's contents, streamed — constant memory on any file size. */
|
|
406
|
+
async function hashFile(full) {
|
|
407
|
+
const digest = (0, node_crypto_1.createHash)('sha256');
|
|
408
|
+
const handle = await openRegularFile(path.basename(full), full);
|
|
409
|
+
const buf = new Uint8Array(1024 * 1024);
|
|
410
|
+
try {
|
|
411
|
+
for (;;) {
|
|
412
|
+
const { bytesRead } = await handle.read(buf, 0, buf.length);
|
|
413
|
+
if (bytesRead === 0)
|
|
414
|
+
break;
|
|
415
|
+
digest.update(buf.subarray(0, bytesRead));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
await handle.close();
|
|
420
|
+
}
|
|
421
|
+
return digest.digest('hex');
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* `[rel, path]` for every regular file under the directory `base`; an absent
|
|
425
|
+
* `base` is empty, a present non-directory is refused. The walk never
|
|
426
|
+
* descends symlinked directories.
|
|
427
|
+
*/
|
|
428
|
+
async function filenamesInDir(root, under, base) {
|
|
429
|
+
if (!(await requireDir(under, base)))
|
|
430
|
+
return [];
|
|
431
|
+
const out = [];
|
|
432
|
+
await walk(base, (full, entry) => {
|
|
433
|
+
if (entry.isFile())
|
|
434
|
+
out.push([path.relative(root, full).split(path.sep).join('/'), full]);
|
|
435
|
+
});
|
|
436
|
+
out.sort();
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
async function symlinksInDir(root, under, base) {
|
|
440
|
+
const relOf = (full) => path.relative(root, full).split(path.sep).join('/');
|
|
441
|
+
let st;
|
|
442
|
+
try {
|
|
443
|
+
st = await fsp.lstat(base, { bigint: true });
|
|
444
|
+
}
|
|
445
|
+
catch (e) {
|
|
446
|
+
const code = e.code;
|
|
447
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
448
|
+
return new Set();
|
|
449
|
+
throw e;
|
|
450
|
+
}
|
|
451
|
+
if (st.isSymbolicLink())
|
|
452
|
+
return new Set([relOf(base)]);
|
|
453
|
+
if (!st.isDirectory())
|
|
454
|
+
throw new FileStoreError(FileStoreError.NOT_A_DIRECTORY, under);
|
|
455
|
+
const out = new Set();
|
|
456
|
+
// Symlinks to directories are reported, never descended, like the rest.
|
|
457
|
+
await walk(base, (full, entry) => {
|
|
458
|
+
if (entry.isSymbolicLink())
|
|
459
|
+
out.add(relOf(full));
|
|
460
|
+
});
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
/** `false` when `base` is absent, refused when present but not a directory. */
|
|
464
|
+
async function requireDir(under, base) {
|
|
465
|
+
let st;
|
|
466
|
+
try {
|
|
467
|
+
st = await fsp.lstat(base, { bigint: true });
|
|
468
|
+
}
|
|
469
|
+
catch (e) {
|
|
470
|
+
// Absent — including "under a file" (ENOTDIR) — is an empty listing.
|
|
471
|
+
const code = e.code;
|
|
472
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
473
|
+
return false;
|
|
474
|
+
throw e;
|
|
475
|
+
}
|
|
476
|
+
if (!st.isDirectory())
|
|
477
|
+
throw new FileStoreError(FileStoreError.NOT_A_DIRECTORY, under);
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
/** Visit every entry under `base` without descending symlinked directories. */
|
|
481
|
+
async function walk(base, visit) {
|
|
482
|
+
const stack = [base];
|
|
483
|
+
while (stack.length) {
|
|
484
|
+
const dir = stack.pop();
|
|
485
|
+
let entries;
|
|
486
|
+
try {
|
|
487
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
488
|
+
}
|
|
489
|
+
catch (e) {
|
|
490
|
+
if (e.code === 'ENOENT')
|
|
491
|
+
continue; // a listing is a snapshot, not a lock
|
|
492
|
+
throw e;
|
|
493
|
+
}
|
|
494
|
+
for (const entry of entries) {
|
|
495
|
+
const full = path.join(dir, entry.name);
|
|
496
|
+
visit(full, entry);
|
|
497
|
+
if (entry.isDirectory() && !entry.isSymbolicLink())
|
|
498
|
+
stack.push(full);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function unchangedSinceHashed(cached, st) {
|
|
503
|
+
return st.mtimeNs === cached.mtimeNs && st.ctimeNs === cached.ctimeNs && st.size === cached.size;
|
|
504
|
+
}
|
|
505
|
+
function oldEnoughToCache(st, walkStartNs) {
|
|
506
|
+
const newestNs = st.mtimeNs > st.ctimeNs ? st.mtimeNs : st.ctimeNs;
|
|
507
|
+
return newestNs < walkStartNs - exports._internals.timestampTrustMarginNs;
|
|
508
|
+
}
|
|
509
|
+
// Filesystems stamp times with coarse clocks, so a rewrite shortly after a
|
|
510
|
+
// hashed write can reuse the exact stamps. Files younger than the margin are
|
|
511
|
+
// simply re-hashed next walk.
|
|
512
|
+
const TIMESTAMP_TRUST_MARGIN_NS = 2000000000n;
|
|
513
|
+
/** Test seam — the hasher, the trust margin, and the walk clock. @internal */
|
|
514
|
+
exports._internals = {
|
|
515
|
+
hashFile,
|
|
516
|
+
timestampTrustMarginNs: TIMESTAMP_TRUST_MARGIN_NS,
|
|
517
|
+
nowNs: () => BigInt(Date.now()) * 1000000n,
|
|
518
|
+
};
|
|
519
|
+
exports.LocalFileStore = FileStore;
|
|
520
|
+
// Wire `Symbol.asyncDispose` at runtime when the host provides it — the
|
|
521
|
+
// repo's tsconfig targets ES2020 so the type-level `AsyncDisposable` lib is
|
|
522
|
+
// not available, but `await using` callers on newer engines still work.
|
|
523
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
524
|
+
if (asyncDispose) {
|
|
525
|
+
Object.defineProperty(FileStore.prototype, asyncDispose, {
|
|
526
|
+
value: FileStore.prototype.dispose,
|
|
527
|
+
configurable: true,
|
|
528
|
+
writable: true,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
//# sourceMappingURL=file-store.js.map
|