@crouter/api 0.3.387 → 0.3.388
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/api/__tests__/integration/client.test.js +97 -0
- package/dist/api/client.d.ts +7 -0
- package/dist/api/client.js +40 -21
- package/dist/core/asset-root.d.ts +7 -0
- package/dist/core/asset-root.js +18 -0
- package/dist/core/canvas/boot-id.d.ts +6 -0
- package/dist/core/canvas/boot-id.js +26 -0
- package/dist/core/canvas/paths.d.ts +72 -0
- package/dist/core/canvas/paths.js +163 -0
- package/dist/core/canvas/pid.d.ts +391 -0
- package/dist/core/canvas/pid.js +948 -0
- package/dist/core/command-plugins/bundle.d.ts +149 -0
- package/dist/core/command-plugins/bundle.js +588 -0
- package/dist/core/command-plugins/endpoint.d.ts +24 -0
- package/dist/core/command-plugins/endpoint.js +51 -0
- package/dist/core/config.d.ts +233 -0
- package/dist/core/config.js +1120 -0
- package/dist/core/env-name.d.ts +6 -0
- package/dist/core/env-name.js +9 -0
- package/dist/core/errors.d.ts +38 -0
- package/dist/core/errors.js +90 -0
- package/dist/core/events/emit.d.ts +6 -0
- package/dist/core/events/emit.js +42 -0
- package/dist/core/events/envelope.d.ts +2 -0
- package/dist/core/events/envelope.js +84 -0
- package/dist/core/events/errors.d.ts +4 -0
- package/dist/core/events/errors.js +69 -0
- package/dist/core/events/operation-id.d.ts +4 -0
- package/dist/core/events/operation-id.js +24 -0
- package/dist/core/events/serialize.d.ts +4 -0
- package/dist/core/events/serialize.js +199 -0
- package/dist/core/events/source.d.ts +16 -0
- package/dist/core/events/source.js +31 -0
- package/dist/core/events/types.d.ts +68 -0
- package/dist/core/events/types.js +11 -0
- package/dist/core/exclusive-lock.d.ts +34 -0
- package/dist/core/exclusive-lock.js +197 -0
- package/dist/core/fs-utils.d.ts +44 -0
- package/dist/core/fs-utils.js +208 -0
- package/dist/core/help.d.ts +309 -0
- package/dist/core/help.js +406 -0
- package/dist/core/human/page-catalog.d.ts +57 -0
- package/dist/core/human/page-catalog.js +172 -0
- package/dist/core/installed-plugins.d.ts +2 -0
- package/dist/core/installed-plugins.js +79 -0
- package/dist/core/io.d.ts +122 -0
- package/dist/core/io.js +373 -0
- package/dist/core/keybindings/attach-control.d.ts +49 -0
- package/dist/core/keybindings/attach-control.js +42 -0
- package/dist/core/keybindings/catalog.d.ts +18 -0
- package/dist/core/keybindings/catalog.js +257 -0
- package/dist/core/keybindings/types.d.ts +42 -0
- package/dist/core/keybindings/types.js +1 -0
- package/dist/core/layout.d.ts +26 -0
- package/dist/core/layout.js +94 -0
- package/dist/core/locked-file.d.ts +27 -0
- package/dist/core/locked-file.js +118 -0
- package/dist/core/log.d.ts +9 -0
- package/dist/core/log.js +89 -0
- package/dist/core/manifest.d.ts +5 -0
- package/dist/core/manifest.js +15 -0
- package/dist/core/plugin-env.d.ts +8 -0
- package/dist/core/plugin-env.js +31 -0
- package/dist/core/plugin-extensions.d.ts +29 -0
- package/dist/core/plugin-extensions.js +191 -0
- package/dist/core/plugin-swap-lock.d.ts +9 -0
- package/dist/core/plugin-swap-lock.js +31 -0
- package/dist/core/preview-result-path.d.ts +4 -0
- package/dist/core/preview-result-path.js +26 -0
- package/dist/core/profiles/env-store.d.ts +22 -0
- package/dist/core/profiles/env-store.js +163 -0
- package/dist/core/profiles/fuzzy-match.d.ts +19 -0
- package/dist/core/profiles/fuzzy-match.js +92 -0
- package/dist/core/profiles/manifest.d.ts +120 -0
- package/dist/core/profiles/manifest.js +529 -0
- package/dist/core/rate-limit-scope.d.ts +25 -0
- package/dist/core/rate-limit-scope.js +64 -0
- package/dist/core/render.d.ts +12 -0
- package/dist/core/render.js +138 -0
- package/dist/core/resolver.d.ts +14 -0
- package/dist/core/resolver.js +111 -0
- package/dist/core/runtime/branded-host.d.ts +25 -0
- package/dist/core/runtime/branded-host.js +264 -0
- package/dist/core/runtime/broker/daemon-ops.d.ts +65 -0
- package/dist/core/runtime/broker/daemon-ops.js +177 -0
- package/dist/core/runtime/broker/signal-stream.d.ts +30 -0
- package/dist/core/runtime/broker/signal-stream.js +149 -0
- package/dist/core/scope.d.ts +32 -0
- package/dist/core/scope.js +184 -0
- package/dist/core/scoped-state/db.d.ts +17 -0
- package/dist/core/scoped-state/db.js +247 -0
- package/dist/core/scoped-state/migrate.d.ts +8 -0
- package/dist/core/scoped-state/migrate.js +187 -0
- package/dist/core/scoped-state/paths.d.ts +9 -0
- package/dist/core/scoped-state/paths.js +27 -0
- package/dist/core/scoped-state/profiles.d.ts +27 -0
- package/dist/core/scoped-state/profiles.js +93 -0
- package/dist/core/scoped-state/providers.d.ts +24 -0
- package/dist/core/scoped-state/providers.js +19 -0
- package/dist/core/scoped-state/schema.d.ts +6 -0
- package/dist/core/scoped-state/schema.js +43 -0
- package/dist/core/scoped-state/settings.d.ts +28 -0
- package/dist/core/scoped-state/settings.js +83 -0
- package/dist/core/spaces/open-beneath.d.ts +71 -0
- package/dist/core/spaces/open-beneath.js +581 -0
- package/dist/core/sqlite-statements.d.ts +4 -0
- package/dist/core/sqlite-statements.js +17 -0
- package/dist/core/subscription-state.d.ts +121 -0
- package/dist/core/subscription-state.js +287 -0
- package/dist/core/user-settings.d.ts +377 -0
- package/dist/core/user-settings.js +458 -0
- package/dist/daemon/broker-signals/bus.d.ts +30 -0
- package/dist/daemon/broker-signals/bus.js +87 -0
- package/dist/daemon/manage.d.ts +176 -0
- package/dist/daemon/manage.js +664 -0
- package/dist/daemon/pidfile.d.ts +8 -0
- package/dist/daemon/pidfile.js +37 -0
- package/dist/daemon/startup-policy.d.ts +1 -0
- package/dist/daemon/startup-policy.js +1 -0
- package/dist/native/linux.d.ts +29 -0
- package/dist/native/linux.js +20 -0
- package/dist/shared/env.d.ts +116 -0
- package/dist/shared/env.js +271 -0
- package/dist/shared/inbox-entry-body.d.ts +22 -0
- package/dist/shared/inbox-entry-body.js +116 -0
- package/dist/shared/working-activity.d.ts +9 -0
- package/dist/shared/working-activity.js +27 -0
- package/dist/types.d.ts +562 -0
- package/dist/types.js +186 -0
- package/package.json +1 -1
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import { chmodSync, chownSync, closeSync, constants, fchmodSync, fchownSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readlinkSync, readSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeSync, } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { loadLinuxNative, O_CLOEXEC, O_PATH, RESOLVE_BENEATH, RESOLVE_NO_SYMLINKS } from '../../native/linux.js';
|
|
5
|
+
// Every path the daemon opens for a caller resolves beneath the root of the
|
|
6
|
+
// permission that names it, with no symlink anywhere on the way: on Linux the
|
|
7
|
+
// kernel enforces it (openat2). Every Linux open is O_NONBLOCK and keeps only a
|
|
8
|
+
// regular file (or a directory where one is expected), so a FIFO, socket,
|
|
9
|
+
// device or leased file an agent plants is refused instead of blocking the
|
|
10
|
+
// daemon. A Mac has
|
|
11
|
+
// no addon and opens by pathname.
|
|
12
|
+
const RESOLVE = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS;
|
|
13
|
+
const DIR = O_PATH | constants.O_DIRECTORY | O_CLOEXEC;
|
|
14
|
+
let cached = null;
|
|
15
|
+
function native() {
|
|
16
|
+
if (cached === null)
|
|
17
|
+
cached = loadLinuxNative();
|
|
18
|
+
return cached;
|
|
19
|
+
}
|
|
20
|
+
function code(error) {
|
|
21
|
+
return error.code;
|
|
22
|
+
}
|
|
23
|
+
/** A path this module refused: it meets a symlink, leaves its root, is not a
|
|
24
|
+
* regular file, or is held by a lease. */
|
|
25
|
+
class BeneathRefusal extends Error {
|
|
26
|
+
}
|
|
27
|
+
export function isBeneathRefusal(error) {
|
|
28
|
+
return error instanceof BeneathRefusal;
|
|
29
|
+
}
|
|
30
|
+
/** A kernel refusal re-thrown as a `BeneathRefusal` naming the caller's path (the
|
|
31
|
+
* addon's message is bare): ELOOP a symlink, EXDEV an escape, ENXIO a socket or
|
|
32
|
+
* a FIFO opened non-blocking for writing with no reader, EAGAIN a file lease
|
|
33
|
+
* (F_SETLEASE) the non-blocking open would otherwise wait out. */
|
|
34
|
+
function refused(error, shown) {
|
|
35
|
+
switch (code(error)) {
|
|
36
|
+
case 'ELOOP':
|
|
37
|
+
case 'EXDEV': return new BeneathRefusal(`path meets a symlink: ${shown}`);
|
|
38
|
+
case 'ENXIO': return new BeneathRefusal(`path is not a regular file: ${shown}`);
|
|
39
|
+
case 'EAGAIN': return new BeneathRefusal(`path is held by a lease: ${shown}`);
|
|
40
|
+
default: return error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function openat2(n, dirfd, rel, flags, shown) {
|
|
44
|
+
try {
|
|
45
|
+
return n.openat2(dirfd, rel, flags, RESOLVE);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
throw refused(error, shown);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** `fd` if it is a regular file (or, with `directory`, a directory); otherwise
|
|
52
|
+
* it is closed and refused. */
|
|
53
|
+
function regular(fd, shown, directory) {
|
|
54
|
+
let stat;
|
|
55
|
+
try {
|
|
56
|
+
stat = fstatSync(fd);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
closeSync(fd);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
if (stat.isFile() || (directory && stat.isDirectory()))
|
|
63
|
+
return fd;
|
|
64
|
+
closeSync(fd);
|
|
65
|
+
throw new BeneathRefusal(`path is not a regular file: ${shown}`);
|
|
66
|
+
}
|
|
67
|
+
/** Codes that mean "nothing is there to use", as a pathname existence check reads them. */
|
|
68
|
+
const ABSENT = new Set(['ENOENT', 'ENOTDIR', 'EACCES']);
|
|
69
|
+
/** Whether a beneath read failed because nothing usable is there: absent, not a
|
|
70
|
+
* directory, unreadable, or refused (see `BeneathRefusal`). */
|
|
71
|
+
export function isAbsentOrRefused(error) {
|
|
72
|
+
return ABSENT.has(code(error) ?? '') || isBeneathRefusal(error);
|
|
73
|
+
}
|
|
74
|
+
function inside(root, path) {
|
|
75
|
+
const rel = relative(resolve(root), resolve(path));
|
|
76
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || rel.startsWith(sep))
|
|
77
|
+
throw new Error(`${path} is outside ${root}`);
|
|
78
|
+
return rel;
|
|
79
|
+
}
|
|
80
|
+
function entry(name) {
|
|
81
|
+
if (name === '' || name === '..' || name.includes('/'))
|
|
82
|
+
throw new Error(`not one path component: ${name}`);
|
|
83
|
+
return name;
|
|
84
|
+
}
|
|
85
|
+
/** An fd for `path` opened with `flags`, resolved beneath `root`. Never creates:
|
|
86
|
+
* `openat2` takes no mode, so a file it created would be mode 0000. On Linux
|
|
87
|
+
* the fd is a regular file or a directory, except with O_PATH, which opens
|
|
88
|
+
* nothing and is only stat'd. */
|
|
89
|
+
export function openBeneath(root, path, flags) {
|
|
90
|
+
if (flags & constants.O_CREAT)
|
|
91
|
+
throw new Error('openBeneath never creates; use writeFileBeneath or createFileBeneath');
|
|
92
|
+
const rel = inside(root, path);
|
|
93
|
+
const n = native();
|
|
94
|
+
if (!n)
|
|
95
|
+
return openSync(path, flags);
|
|
96
|
+
const slash = openSync('/', DIR);
|
|
97
|
+
let rootFd;
|
|
98
|
+
try {
|
|
99
|
+
rootFd = openat2(n, slash, relative('/', resolve(root)) || '.', DIR, root);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
closeSync(slash);
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
if (flags & O_PATH)
|
|
106
|
+
return openat2(n, rootFd, rel || '.', flags | O_CLOEXEC, path);
|
|
107
|
+
return regular(openat2(n, rootFd, rel || '.', flags | O_CLOEXEC | constants.O_NONBLOCK, path), path, true);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
closeSync(rootFd);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function readFileBeneath(root, path) {
|
|
114
|
+
const fd = openBeneath(root, path, constants.O_RDONLY);
|
|
115
|
+
try {
|
|
116
|
+
return readFileSync(fd);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
closeSync(fd);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** The file's text, or null when it is missing or refused. */
|
|
123
|
+
export function readTextBeneathIfExists(root, path) {
|
|
124
|
+
try {
|
|
125
|
+
return readFileBeneath(root, path).toString('utf8');
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (code(error) === 'ENOENT' || isBeneathRefusal(error))
|
|
129
|
+
return null;
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export function statBeneath(root, path) {
|
|
134
|
+
if (!native())
|
|
135
|
+
return statSync(path);
|
|
136
|
+
const fd = openBeneath(root, path, O_PATH);
|
|
137
|
+
try {
|
|
138
|
+
return fstatSync(fd);
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
closeSync(fd);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function existsBeneath(root, path) {
|
|
145
|
+
try {
|
|
146
|
+
statBeneath(root, path);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (ABSENT.has(code(error) ?? ''))
|
|
151
|
+
return false;
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Run `fn` with `at(name)`, a pathname that reaches one entry of `dir` after
|
|
156
|
+
* `dir` itself resolved beneath `root` (`at()` is the directory). With
|
|
157
|
+
* `create`, missing directories on the way are made. */
|
|
158
|
+
export function withDirBeneath(root, dir, fn, create = false) {
|
|
159
|
+
inside(root, dir);
|
|
160
|
+
const n = native();
|
|
161
|
+
if (!n) {
|
|
162
|
+
if (create)
|
|
163
|
+
mkdirSync(dir, { recursive: true });
|
|
164
|
+
return fn((name) => name === undefined ? dir : join(dir, entry(name)));
|
|
165
|
+
}
|
|
166
|
+
let fd = openSync('/', DIR);
|
|
167
|
+
try {
|
|
168
|
+
for (const part of relative('/', resolve(dir)).split(sep).filter(Boolean)) {
|
|
169
|
+
let next;
|
|
170
|
+
try {
|
|
171
|
+
next = openat2(n, fd, part, DIR, dir);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (!create || code(error) !== 'ENOENT')
|
|
175
|
+
throw error;
|
|
176
|
+
try {
|
|
177
|
+
mkdirSync(`/proc/self/fd/${fd}/${part}`);
|
|
178
|
+
}
|
|
179
|
+
catch (made) {
|
|
180
|
+
if (code(made) !== 'EEXIST')
|
|
181
|
+
throw made;
|
|
182
|
+
}
|
|
183
|
+
next = openat2(n, fd, part, DIR, dir);
|
|
184
|
+
}
|
|
185
|
+
closeSync(fd);
|
|
186
|
+
fd = next;
|
|
187
|
+
}
|
|
188
|
+
const base = `/proc/self/fd/${fd}`;
|
|
189
|
+
return fn((name) => name === undefined ? base : `${base}/${entry(name)}`);
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
closeSync(fd);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** Open the final component for writing. On Linux it must be a regular file,
|
|
196
|
+
* never a symlink (O_NOFOLLOW's ELOOP is the same refusal openat2 gives). */
|
|
197
|
+
function openForWrite(at, flags, mode, shown) {
|
|
198
|
+
if (!native())
|
|
199
|
+
return openSync(at, flags, mode);
|
|
200
|
+
let fd;
|
|
201
|
+
try {
|
|
202
|
+
fd = openSync(at, flags | constants.O_NOFOLLOW | constants.O_NONBLOCK, mode);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
throw refused(error, shown);
|
|
206
|
+
}
|
|
207
|
+
return regular(fd, shown, false);
|
|
208
|
+
}
|
|
209
|
+
function writeAll(fd, data) {
|
|
210
|
+
let offset = 0;
|
|
211
|
+
while (offset < data.length)
|
|
212
|
+
offset += writeSync(fd, data, offset, data.length - offset);
|
|
213
|
+
}
|
|
214
|
+
/** Write `data` to `path` beneath `root`, making missing parents. `atomic`
|
|
215
|
+
* writes a temporary file and renames it over the target, so on Linux a
|
|
216
|
+
* symlink at the target is replaced, never followed. A Mac writes through a
|
|
217
|
+
* symlink to its target, as it always has (a dangling one fails ENOENT). */
|
|
218
|
+
export function writeFileBeneath(root, path, data, options = {}) {
|
|
219
|
+
const bytes = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
|
|
220
|
+
const mode = options.mode ?? 0o666;
|
|
221
|
+
let dir = dirname(path);
|
|
222
|
+
let name = basename(path);
|
|
223
|
+
let base = root;
|
|
224
|
+
if (options.atomic && !native()) {
|
|
225
|
+
let link = false;
|
|
226
|
+
try {
|
|
227
|
+
link = lstatSync(path).isSymbolicLink();
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (code(error) !== 'ENOENT')
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
if (link) {
|
|
234
|
+
const target = realpathSync(path);
|
|
235
|
+
dir = dirname(target);
|
|
236
|
+
name = basename(target);
|
|
237
|
+
base = '/';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
withDirBeneath(base, dir, (at) => {
|
|
241
|
+
if (!options.atomic) {
|
|
242
|
+
const fd = openForWrite(at(name), constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC, mode, path);
|
|
243
|
+
try {
|
|
244
|
+
writeAll(fd, bytes);
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
closeSync(fd);
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const temporary = at(`.${name}.${randomUUID()}.tmp`);
|
|
252
|
+
let fd = openSync(temporary, 'wx', mode);
|
|
253
|
+
try {
|
|
254
|
+
writeAll(fd, bytes);
|
|
255
|
+
fsyncSync(fd);
|
|
256
|
+
closeSync(fd);
|
|
257
|
+
fd = undefined;
|
|
258
|
+
renameSync(temporary, at(name));
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
if (fd !== undefined)
|
|
262
|
+
closeSync(fd);
|
|
263
|
+
try {
|
|
264
|
+
unlinkSync(temporary);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (code(error) !== 'ENOENT')
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}, true);
|
|
272
|
+
}
|
|
273
|
+
/** Create `path` beneath `root` only if absent (`wx`), making missing parents. */
|
|
274
|
+
export function createFileBeneath(root, path, data, mode = 0o666) {
|
|
275
|
+
const bytes = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
|
|
276
|
+
withDirBeneath(root, dirname(path), (at) => {
|
|
277
|
+
const fd = openSync(at(basename(path)), 'wx', mode);
|
|
278
|
+
try {
|
|
279
|
+
writeAll(fd, bytes);
|
|
280
|
+
fsyncSync(fd);
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
closeSync(fd);
|
|
284
|
+
}
|
|
285
|
+
}, true);
|
|
286
|
+
}
|
|
287
|
+
export function appendFileBeneath(root, path, data) {
|
|
288
|
+
withDirBeneath(root, dirname(path), (at) => {
|
|
289
|
+
const fd = openForWrite(at(basename(path)), constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND, 0o666, path);
|
|
290
|
+
try {
|
|
291
|
+
writeAll(fd, Buffer.from(data, 'utf8'));
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
closeSync(fd);
|
|
295
|
+
}
|
|
296
|
+
}, true);
|
|
297
|
+
}
|
|
298
|
+
export function unlinkBeneath(root, path) {
|
|
299
|
+
withDirBeneath(root, dirname(path), (at) => unlinkSync(at(basename(path))));
|
|
300
|
+
}
|
|
301
|
+
export function rmdirBeneath(root, path) {
|
|
302
|
+
withDirBeneath(root, dirname(path), (at) => rmdirSync(at(basename(path))));
|
|
303
|
+
}
|
|
304
|
+
export function renameBeneath(root, from, to) {
|
|
305
|
+
withDirBeneath(root, dirname(from), (src) => withDirBeneath(root, dirname(to), (dst) => renameSync(src(basename(from)), dst(basename(to))), true));
|
|
306
|
+
}
|
|
307
|
+
/** link(2) without replacement: fails if `to` exists. */
|
|
308
|
+
export function linkBeneath(root, from, to) {
|
|
309
|
+
withDirBeneath(root, dirname(from), (src) => withDirBeneath(root, dirname(to), (dst) => linkSync(src(basename(from)), dst(basename(to)))));
|
|
310
|
+
}
|
|
311
|
+
export function fsyncDirBeneath(root, dir) {
|
|
312
|
+
withDirBeneath(root, dir, (at) => {
|
|
313
|
+
const fd = openSync(at(), constants.O_RDONLY);
|
|
314
|
+
try {
|
|
315
|
+
fsyncSync(fd);
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
closeSync(fd);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
/** Directory entries typed by the directory itself (a symlink stays a symlink). */
|
|
323
|
+
export function readdirBeneath(root, dir) {
|
|
324
|
+
return withDirBeneath(root, dir, (at) => readdirSync(at(), { withFileTypes: true }));
|
|
325
|
+
}
|
|
326
|
+
/** The first `limit` entries by name, each with its own (unfollowed on Linux)
|
|
327
|
+
* stat; an entry removed before its stat is left out. */
|
|
328
|
+
export function listBeneath(root, dir, limit) {
|
|
329
|
+
return withDirBeneath(root, dir, (at) => readdirSync(at()).sort((a, b) => a.localeCompare(b)).slice(0, limit).flatMap((name) => {
|
|
330
|
+
try {
|
|
331
|
+
return [{ name, stat: native() ? lstatSync(at(name)) : statSync(at(name)) }];
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
if (code(error) === 'ENOENT')
|
|
335
|
+
return [];
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
/** Regular files under `dir` matching `predicate`, never descending a symlink. */
|
|
341
|
+
export function walkFilesBeneath(root, dir, predicate = () => true, skipDir = () => false) {
|
|
342
|
+
const out = [];
|
|
343
|
+
const stack = [dir];
|
|
344
|
+
while (stack.length) {
|
|
345
|
+
const current = stack.pop();
|
|
346
|
+
let entries;
|
|
347
|
+
try {
|
|
348
|
+
entries = readdirBeneath(root, current);
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (current === dir && !ABSENT.has(code(error) ?? ''))
|
|
352
|
+
throw error;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
for (const e of entries) {
|
|
356
|
+
const full = join(current, e.name);
|
|
357
|
+
if (e.isDirectory()) {
|
|
358
|
+
if (!skipDir(e.name))
|
|
359
|
+
stack.push(full);
|
|
360
|
+
}
|
|
361
|
+
else if (e.isFile() && predicate(e.name))
|
|
362
|
+
out.push(full);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
/** Copy the entries of `from` (beneath `fromRoot`) into `to` (beneath
|
|
368
|
+
* `toRoot`): directories and regular files with their permission bits, and
|
|
369
|
+
* symlinks as links, never followed. Any other entry, any entry removed or
|
|
370
|
+
* refused after the directory was read, and any top-level name in `skip`, is
|
|
371
|
+
* left out. A name `to` already holds is overwritten, except by a symlink. */
|
|
372
|
+
export function copyTreeBeneath(fromRoot, from, toRoot, to, skip = new Set()) {
|
|
373
|
+
copyEntries(fromRoot, from, readdirBeneath(fromRoot, from), toRoot, to, skip);
|
|
374
|
+
}
|
|
375
|
+
function copyEntries(fromRoot, from, entries, toRoot, to, skip) {
|
|
376
|
+
withDirBeneath(toRoot, to, () => undefined, true);
|
|
377
|
+
for (const entry of entries) {
|
|
378
|
+
if (skip.has(entry.name))
|
|
379
|
+
continue;
|
|
380
|
+
const source = join(from, entry.name);
|
|
381
|
+
const target = join(to, entry.name);
|
|
382
|
+
if (entry.isDirectory()) {
|
|
383
|
+
let mode;
|
|
384
|
+
let children;
|
|
385
|
+
try {
|
|
386
|
+
mode = statBeneath(fromRoot, source).mode & 0o777;
|
|
387
|
+
children = readdirBeneath(fromRoot, source);
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
if (isAbsentOrRefused(error))
|
|
391
|
+
continue;
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
copyEntries(fromRoot, source, children, toRoot, target, new Set());
|
|
395
|
+
withDirBeneath(toRoot, to, (at) => chmodSync(at(entry.name), mode));
|
|
396
|
+
}
|
|
397
|
+
else if (entry.isSymbolicLink()) {
|
|
398
|
+
let link;
|
|
399
|
+
try {
|
|
400
|
+
link = withDirBeneath(fromRoot, from, (at) => readlinkSync(at(entry.name)));
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
if (code(error) === 'EINVAL' || isAbsentOrRefused(error))
|
|
404
|
+
continue;
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
withDirBeneath(toRoot, to, (at) => symlinkSync(link, at(entry.name)));
|
|
408
|
+
}
|
|
409
|
+
else if (entry.isFile()) {
|
|
410
|
+
let fd;
|
|
411
|
+
try {
|
|
412
|
+
fd = openBeneath(fromRoot, source, constants.O_RDONLY);
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
if (isAbsentOrRefused(error))
|
|
416
|
+
continue;
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
copyFileFd(fd, toRoot, target);
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
closeSync(fd);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/** Copy the open regular file `fd` to `path` beneath `root` in 1 MiB chunks,
|
|
429
|
+
* with its permission bits. */
|
|
430
|
+
function copyFileFd(fd, root, path) {
|
|
431
|
+
const mode = fstatSync(fd).mode & 0o777;
|
|
432
|
+
withDirBeneath(root, dirname(path), (at) => {
|
|
433
|
+
const out = openForWrite(at(basename(path)), constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC, mode, path);
|
|
434
|
+
try {
|
|
435
|
+
fchmodSync(out, mode);
|
|
436
|
+
const chunk = Buffer.allocUnsafe(1 << 20);
|
|
437
|
+
for (let read = readSync(fd, chunk); read > 0; read = readSync(fd, chunk))
|
|
438
|
+
writeAll(out, chunk.subarray(0, read));
|
|
439
|
+
}
|
|
440
|
+
finally {
|
|
441
|
+
closeSync(out);
|
|
442
|
+
}
|
|
443
|
+
}, true);
|
|
444
|
+
}
|
|
445
|
+
/** Set the mode (and, with `owner`, the owner) of the file or directory at
|
|
446
|
+
* `path` beneath `root` through an fd opened without following a symlink. A
|
|
447
|
+
* Mac sets them by pathname, as it always has. */
|
|
448
|
+
export function setModeBeneath(root, path, mode, owner) {
|
|
449
|
+
if (!native()) {
|
|
450
|
+
if (owner)
|
|
451
|
+
chownSync(path, owner.uid, owner.gid);
|
|
452
|
+
chmodSync(path, mode);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const fd = openBeneath(root, path, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
456
|
+
try {
|
|
457
|
+
const stat = fstatSync(fd);
|
|
458
|
+
if (!stat.isFile() && !stat.isDirectory())
|
|
459
|
+
throw new BeneathRefusal(`path is not a regular file or directory: ${path}`);
|
|
460
|
+
if (owner)
|
|
461
|
+
fchownSync(fd, owner.uid, owner.gid);
|
|
462
|
+
fchmodSync(fd, mode);
|
|
463
|
+
}
|
|
464
|
+
finally {
|
|
465
|
+
closeSync(fd);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function unlinkIfPresent(path) {
|
|
469
|
+
try {
|
|
470
|
+
unlinkSync(path);
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
if (code(error) !== 'ENOENT')
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/** Remove the tree at `path` beneath `root`, never descending a symlink: a
|
|
478
|
+
* symlink at `path` or inside it is unlinked itself. Absent is not an error.
|
|
479
|
+
* On Linux the walk holds one directory fd per level and opens each child
|
|
480
|
+
* relative to its parent, so a deep tree costs one open per entry. */
|
|
481
|
+
export function removeTreeBeneath(root, path) {
|
|
482
|
+
const n = native();
|
|
483
|
+
if (!n) {
|
|
484
|
+
rmSync(path, { recursive: true, force: true });
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
withDirBeneath(root, dirname(path), (at) => {
|
|
489
|
+
const parent = openSync(at(), DIR);
|
|
490
|
+
try {
|
|
491
|
+
removeTreeAt(n, parent, basename(path), path);
|
|
492
|
+
}
|
|
493
|
+
finally {
|
|
494
|
+
closeSync(parent);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
if (code(error) !== 'ENOENT')
|
|
500
|
+
throw error;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function removeTreeAt(n, parentFd, name, shown) {
|
|
504
|
+
const stack = [];
|
|
505
|
+
const enter = (at, child, childShown) => {
|
|
506
|
+
let fd;
|
|
507
|
+
try {
|
|
508
|
+
fd = openat2(n, at, child, DIR, childShown);
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
if (code(error) === 'ENOENT')
|
|
512
|
+
return;
|
|
513
|
+
if (code(error) !== 'ENOTDIR' && !(error instanceof BeneathRefusal))
|
|
514
|
+
throw error;
|
|
515
|
+
unlinkIfPresent(`/proc/self/fd/${at}/${child}`);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
stack.push({ fd, parentFd: at, name: child, shown: childShown, dirs: [] });
|
|
519
|
+
const level = stack[stack.length - 1];
|
|
520
|
+
for (const e of readdirSync(`/proc/self/fd/${fd}`, { withFileTypes: true })) {
|
|
521
|
+
if (e.isDirectory())
|
|
522
|
+
level.dirs.push(e.name);
|
|
523
|
+
else
|
|
524
|
+
unlinkIfPresent(`/proc/self/fd/${fd}/${e.name}`);
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
try {
|
|
528
|
+
enter(parentFd, name, shown);
|
|
529
|
+
while (stack.length > 0) {
|
|
530
|
+
const level = stack[stack.length - 1];
|
|
531
|
+
const child = level.dirs.pop();
|
|
532
|
+
if (child !== undefined) {
|
|
533
|
+
enter(level.fd, child, join(level.shown, child));
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
stack.pop();
|
|
537
|
+
closeSync(level.fd);
|
|
538
|
+
try {
|
|
539
|
+
rmdirSync(`/proc/self/fd/${level.parentFd}/${level.name}`);
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
if (code(error) !== 'ENOENT')
|
|
543
|
+
throw error;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
finally {
|
|
548
|
+
for (const level of stack)
|
|
549
|
+
closeSync(level.fd);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/** A pathname that connects to the unix socket at `path` beneath `root`
|
|
553
|
+
* without following a symlink, and `release`, to call once the connect has
|
|
554
|
+
* settled. On Linux it is the magic link of an O_PATH fd held until release. */
|
|
555
|
+
export function socketBeneath(root, path) {
|
|
556
|
+
if (!native())
|
|
557
|
+
return { path, release: () => undefined };
|
|
558
|
+
const fd = openBeneath(root, path, O_PATH);
|
|
559
|
+
let stat;
|
|
560
|
+
try {
|
|
561
|
+
stat = fstatSync(fd);
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
closeSync(fd);
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
if (!stat.isSocket()) {
|
|
568
|
+
closeSync(fd);
|
|
569
|
+
throw new BeneathRefusal(`path is not a socket: ${path}`);
|
|
570
|
+
}
|
|
571
|
+
let open = true;
|
|
572
|
+
return {
|
|
573
|
+
path: `/proc/self/fd/${fd}`,
|
|
574
|
+
release: () => {
|
|
575
|
+
if (!open)
|
|
576
|
+
return;
|
|
577
|
+
open = false;
|
|
578
|
+
closeSync(fd);
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DatabaseSync, StatementSync } from 'node:sqlite';
|
|
2
|
+
export declare function prepared(db: DatabaseSync, sql: string): StatementSync;
|
|
3
|
+
/** Explicitly release compiled statements before closing or migrating a connection. */
|
|
4
|
+
export declare function forgetPrepared(db: DatabaseSync): void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Statements belong to a connection, not a database path. Cache only fixed SQL:
|
|
2
|
+
* generated IN lists and schema-dependent SQL remain uncached at their call sites.
|
|
3
|
+
*/
|
|
4
|
+
const statements = new WeakMap();
|
|
5
|
+
export function prepared(db, sql) {
|
|
6
|
+
let byDb = statements.get(db);
|
|
7
|
+
if (!byDb)
|
|
8
|
+
statements.set(db, byDb = new Map());
|
|
9
|
+
let statement = byDb.get(sql);
|
|
10
|
+
if (!statement)
|
|
11
|
+
byDb.set(sql, statement = db.prepare(sql));
|
|
12
|
+
return statement;
|
|
13
|
+
}
|
|
14
|
+
/** Explicitly release compiled statements before closing or migrating a connection. */
|
|
15
|
+
export function forgetPrepared(db) {
|
|
16
|
+
statements.delete(db);
|
|
17
|
+
}
|