@statewalker/webrun-files-composite 0.7.0 → 0.8.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.
@@ -24,9 +24,42 @@ function joinPath(...segments) {
24
24
  }
25
25
  //#endregion
26
26
  //#region src/composite-files-api.ts
27
+ /**
28
+ * Composite `FilesApi` that routes calls to one of several backends based
29
+ * on a path prefix. Mounts are matched by **longest prefix wins**, so a
30
+ * mount at `/a/b` takes precedence over a mount at `/a` for paths under
31
+ * `/a/b/...`. The mount point itself appears in listings as a synthetic
32
+ * directory and cannot be removed.
33
+ *
34
+ * Each backend can use a sub-directory of its own filesystem as the mount
35
+ * root via `fsPath` (constructor `rootPath` for the implicit root mount,
36
+ * `fsPath` argument for additional mounts). Cross-mount `move` is
37
+ * implemented as copy-then-delete; there is no atomicity guarantee.
38
+ *
39
+ * Access control and visibility filtering are intentionally **not** part of
40
+ * this class — wrap with {@link GuardedFilesApi} or {@link FilteredFilesApi}
41
+ * (or both) instead.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const fs = new CompositeFilesApi(localFs, "/projects")
46
+ * .mount("/docs", s3Fs, "/documentation")
47
+ * .mount("/cache", memFs);
48
+ * await fs.write("/readme.md", data); // → localFs:/projects/readme.md
49
+ * await fs.write("/docs/api.md", data); // → s3Fs:/documentation/api.md
50
+ * ```
51
+ */
27
52
  var CompositeFilesApi = class {
28
53
  mounts;
29
- guards = [];
54
+ /**
55
+ * @param root Default backend used for any path that does not match a
56
+ * more specific mount. All paths are routed here unless `mount()`
57
+ * intercepts them.
58
+ * @param rootPath Optional sub-directory of the root backend to use as
59
+ * the composite filesystem's `/`. For example, `rootPath = "/projects"`
60
+ * makes the composite path `/readme.md` resolve to `/projects/readme.md`
61
+ * in the root backend. Defaults to `"/"` (no remapping).
62
+ */
30
63
  constructor(root, rootPath) {
31
64
  this.mounts = [{
32
65
  prefix: "/",
@@ -34,6 +67,22 @@ var CompositeFilesApi = class {
34
67
  basePath: normalizePath(rootPath ?? "/")
35
68
  }];
36
69
  }
70
+ /**
71
+ * Attaches a backend to handle every composite path under `path`. The
72
+ * mount prefix is normalized; paths under it are resolved against the
73
+ * mount's `fsPath` sub-directory (defaulting to `"/"`).
74
+ *
75
+ * @param path Composite-namespace prefix (e.g. `"/docs"`). Mounting at
76
+ * `"/"` is forbidden — use the constructor `root` argument instead.
77
+ * @param api The backend `FilesApi` to delegate to for paths under
78
+ * `path`. Wrap it in {@link FilteredFilesApi} / {@link GuardedFilesApi}
79
+ * first if you want mount-local filtering or guards.
80
+ * @param fsPath Sub-directory of the mounted backend used as its mount
81
+ * root, e.g. `mount("/docs", s3, "/documentation")` makes
82
+ * `/docs/api.md` resolve to `/documentation/api.md` on `s3`.
83
+ * @returns `this`, for chaining.
84
+ * @throws If `path` normalizes to `"/"`.
85
+ */
37
86
  mount(path, api, fsPath) {
38
87
  const prefix = normalizePath(path);
39
88
  if (prefix === "/") throw new Error("Cannot mount at root — root is set via constructor");
@@ -45,14 +94,6 @@ var CompositeFilesApi = class {
45
94
  this.mounts.sort((a, b) => b.prefix.length - a.prefix.length);
46
95
  return this;
47
96
  }
48
- guard(operations, check, message) {
49
- this.guards.push({
50
- operations,
51
- check,
52
- message
53
- });
54
- return this;
55
- }
56
97
  resolve(path) {
57
98
  const normalized = normalizePath(path);
58
99
  for (const mount of this.mounts) {
@@ -90,33 +131,19 @@ var CompositeFilesApi = class {
90
131
  }
91
132
  return result;
92
133
  }
93
- checkGuard(operation, path) {
94
- const normalized = normalizePath(path);
95
- for (const guard of this.guards) {
96
- if (!guard.operations.includes(operation)) continue;
97
- if (!guard.check(normalized)) {
98
- const msg = guard.message ?? "Access denied";
99
- throw new Error(`${msg}: ${normalized}`);
100
- }
101
- }
102
- }
103
134
  read(path, options) {
104
- this.checkGuard("read", path);
105
135
  const { api, resolvedPath } = this.resolve(path);
106
136
  return api.read(resolvedPath, options);
107
137
  }
108
138
  async write(path, content) {
109
- this.checkGuard("write", path);
110
139
  const { api, resolvedPath } = this.resolve(path);
111
140
  return api.write(resolvedPath, content);
112
141
  }
113
142
  async mkdir(path) {
114
- this.checkGuard("mkdir", path);
115
143
  const { api, resolvedPath } = this.resolve(path);
116
144
  return api.mkdir(resolvedPath);
117
145
  }
118
146
  async *list(path, options) {
119
- this.checkGuard("list", path);
120
147
  const normalized = normalizePath(path);
121
148
  const { api, resolvedPath } = this.resolve(path);
122
149
  const childMounts = this.childMountPrefixes(normalized);
@@ -181,13 +208,10 @@ var CompositeFilesApi = class {
181
208
  async remove(path) {
182
209
  const normalized = normalizePath(path);
183
210
  if (this.isMountPoint(normalized)) throw new Error(`Cannot remove mount point: ${normalized}`);
184
- this.checkGuard("remove", path);
185
211
  const { api, resolvedPath } = this.resolve(path);
186
212
  return api.remove(resolvedPath);
187
213
  }
188
214
  async move(source, target) {
189
- this.checkGuard("move", source);
190
- this.checkGuard("move", target);
191
215
  const src = this.resolve(source);
192
216
  const tgt = this.resolve(target);
193
217
  if (src.api === tgt.api) return src.api.move(src.resolvedPath, tgt.resolvedPath);
@@ -196,8 +220,6 @@ var CompositeFilesApi = class {
196
220
  return copied;
197
221
  }
198
222
  async copy(source, target) {
199
- this.checkGuard("copy", source);
200
- this.checkGuard("copy", target);
201
223
  const src = this.resolve(source);
202
224
  const tgt = this.resolve(target);
203
225
  if (src.api === tgt.api) return src.api.copy(src.resolvedPath, tgt.resolvedPath);
@@ -236,4 +258,408 @@ var CompositeFilesApi = class {
236
258
  }
237
259
  };
238
260
  //#endregion
261
+ //#region src/glob-to-regexp.ts
262
+ /**
263
+ * Compiles a glob pattern into a `RegExp`.
264
+ *
265
+ * @example
266
+ * ```ts
267
+ * globToRegExp("*.js"); // /^.*\.js$/
268
+ * globToRegExp("*.js", { globstar: true }); // /^([^/]*)\.js$/
269
+ * globToRegExp("/foo/**", { globstar: true }) // /^\/foo\/((?:[^/]*(?:\/|$))*)$/
270
+ * globToRegExp("foo{bar,baz}", { extended: true }); // /^foo(bar|baz)$/
271
+ * ```
272
+ */
273
+ function globToRegExp(glob, opts = {}) {
274
+ if (typeof glob !== "string") throw new TypeError("Expected a string");
275
+ const str = String(glob);
276
+ let reStr = "";
277
+ const extended = !!opts.extended;
278
+ const globstar = !!opts.globstar;
279
+ let inGroup = false;
280
+ const flags = typeof opts.flags === "string" ? opts.flags : "";
281
+ for (let i = 0; i < str.length; i++) {
282
+ const c = str[i];
283
+ switch (c) {
284
+ case "/":
285
+ case "$":
286
+ case "^":
287
+ case "+":
288
+ case ".":
289
+ case "(":
290
+ case ")":
291
+ case "=":
292
+ case "!":
293
+ case "|":
294
+ reStr += `\\${c}`;
295
+ break;
296
+ case "?":
297
+ reStr += extended ? "." : `\\${c}`;
298
+ break;
299
+ case "[":
300
+ case "]":
301
+ reStr += extended ? c : `\\${c}`;
302
+ break;
303
+ case "{":
304
+ if (extended) {
305
+ inGroup = true;
306
+ reStr += "(";
307
+ } else reStr += `\\${c}`;
308
+ break;
309
+ case "}":
310
+ if (extended) {
311
+ inGroup = false;
312
+ reStr += ")";
313
+ } else reStr += `\\${c}`;
314
+ break;
315
+ case ",":
316
+ if (inGroup) reStr += "|";
317
+ else reStr += `\\${c}`;
318
+ break;
319
+ case "*": {
320
+ const prevChar = str[i - 1];
321
+ let starCount = 1;
322
+ while (str[i + 1] === "*") {
323
+ starCount++;
324
+ i++;
325
+ }
326
+ const nextChar = str[i + 1];
327
+ if (!globstar) reStr += ".*";
328
+ else if (starCount > 1 && (prevChar === "/" || prevChar === void 0) && (nextChar === "/" || nextChar === void 0)) {
329
+ reStr += "((?:[^/]*(?:\\/|$))*)";
330
+ i++;
331
+ } else reStr += "([^/]*)";
332
+ break;
333
+ }
334
+ default: reStr += c;
335
+ }
336
+ }
337
+ if (!flags?.includes("g")) reStr = `^${reStr}$`;
338
+ return new RegExp(reStr, flags);
339
+ }
340
+ //#endregion
341
+ //#region src/filtered-files-api.ts
342
+ /**
343
+ * Builds a {@link PathFilter} that hides any path whose normalized form
344
+ * equals one of the provided prefixes or lives under `${prefix}/`.
345
+ *
346
+ * Prefixes are normalized through `normalizePath` (so `"foo"`, `"/foo"`, and
347
+ * `"/foo/"` are equivalent). Empty / root entries are dropped — they would
348
+ * otherwise hide every path.
349
+ *
350
+ * Matching is **boundary-aware**: the prefix `"/priv"` does not match the
351
+ * path `"/private"` because there is no `/` boundary between them.
352
+ *
353
+ * @param pathPrefixes Path prefixes whose contents (and the prefix itself)
354
+ * should be hidden. Pass none to hide nothing.
355
+ *
356
+ * @example
357
+ * ```ts
358
+ * const filter = newPathFilter("/.git", "/node_modules");
359
+ * filter("/src/index.ts"); // true
360
+ * filter("/.git"); // false
361
+ * filter("/.git/HEAD"); // false
362
+ * filter("/notgit"); // true (boundary-aware, no false match)
363
+ * ```
364
+ */
365
+ function newPathFilter(...pathPrefixes) {
366
+ const normalized = pathPrefixes.map((p) => normalizePath(p)).filter((p) => p !== "/");
367
+ return (path) => {
368
+ const target = normalizePath(path);
369
+ for (const prefix of normalized) {
370
+ if (target === prefix) return false;
371
+ if (target.startsWith(`${prefix}/`)) return false;
372
+ }
373
+ return true;
374
+ };
375
+ }
376
+ /**
377
+ * Builds a {@link PathFilter} that hides any path whose normalized form
378
+ * matches at least one of the provided regular expressions.
379
+ *
380
+ * The path is normalized through `normalizePath` before matching, so a
381
+ * regexp anchored on `^/` always sees a leading slash and never a trailing
382
+ * one. The regexp's `lastIndex` is irrelevant — the filter calls `test`
383
+ * via a fresh evaluation each time, but stateful (`/g`, `/y`) regexps
384
+ * still mutate `lastIndex` across calls; pass non-stateful regexps unless
385
+ * you know what you are doing.
386
+ *
387
+ * @param pathRegexps Regular expressions whose match means "hide this
388
+ * path". Pass none to hide nothing.
389
+ *
390
+ * @example
391
+ * ```ts
392
+ * // Hide every dotfile and every *.log file
393
+ * const filter = newRegexpPathFilter(/\/\.[^/]+$/, /\.log$/);
394
+ * filter("/src/index.ts"); // true
395
+ * filter("/.env"); // false (matches /\/\.[^/]+$/)
396
+ * filter("/build.log"); // false (matches /\.log$/)
397
+ * ```
398
+ */
399
+ function newRegexpPathFilter(...pathRegexps) {
400
+ return (path) => {
401
+ const target = normalizePath(path);
402
+ for (const regexp of pathRegexps) if (regexp.test(target)) return false;
403
+ return true;
404
+ };
405
+ }
406
+ /**
407
+ * Builds a {@link PathFilter} that hides any path whose normalized form
408
+ * matches at least one of the provided glob patterns.
409
+ *
410
+ * Each glob is compiled with `extended: true` and `globstar: true`, the
411
+ * standard "filesystem-style" mode:
412
+ *
413
+ * - `*` matches any number of characters within a single path segment
414
+ * (does **not** cross `/`).
415
+ * - `**` between slashes matches zero or more whole path segments.
416
+ * - `?` matches exactly one character.
417
+ * - `[abc]` / `[a-z]` matches a single character in the set / range.
418
+ * - `{a,b,c}` matches one of the alternatives.
419
+ *
420
+ * Because matching is done on the **normalized** path (which always starts
421
+ * with `/`), patterns that should match anywhere in the tree need a
422
+ * leading `**​/`, e.g. `**​/*.log` to hide every `.log` file at any depth.
423
+ *
424
+ * Gotcha: `/foo/**` matches descendants of `/foo` but **not** `/foo`
425
+ * itself, because the glob requires a `/` after `foo` before `**` can
426
+ * match. To hide both the directory and its contents, list both prefixes:
427
+ * `newGlobPathFilter("/foo", "/foo/**")`. {@link newPathFilter} doesn't
428
+ * have this problem and may be a better fit for prefix-only hiding.
429
+ *
430
+ * @param pathGlobs Glob patterns whose match means "hide this path". Pass
431
+ * none to hide nothing.
432
+ *
433
+ * @example
434
+ * ```ts
435
+ * const filter = newGlobPathFilter("**​/*.log", "/.git", "/.git/**");
436
+ * filter("/src/index.ts"); // true
437
+ * filter("/build.log"); // false (matches **​/*.log)
438
+ * filter("/.git"); // false (matches /.git)
439
+ * filter("/.git/HEAD"); // false (matches /.git/**)
440
+ * ```
441
+ */
442
+ function newGlobPathFilter(...pathGlobs) {
443
+ return newRegexpPathFilter(...pathGlobs.map((glob) => globToRegExp(glob, {
444
+ extended: true,
445
+ globstar: true
446
+ })));
447
+ }
448
+ /**
449
+ * `FilesApi` decorator that hides every path the supplied {@link PathFilter}
450
+ * rejects. Hidden paths are treated as if they do not exist:
451
+ *
452
+ * - `read` / `list` yield empty iterables.
453
+ * - `stats` returns `undefined`; `exists` returns `false`.
454
+ * - `remove` returns `false` (no error, nothing changed).
455
+ * - `move` / `copy` return `false` if either endpoint is hidden.
456
+ * - `write` / `mkdir` reject with an `Error` (since silently dropping a
457
+ * write would lose data).
458
+ * - `list` recursively skips entries whose paths are hidden, so iterating a
459
+ * visible parent never reveals a hidden child.
460
+ *
461
+ * Wrap any `FilesApi` to scope its visibility without changing the
462
+ * underlying storage; the wrapped instance still holds the data, it is just
463
+ * not reachable through this decorator.
464
+ *
465
+ * Pair with one of the built-in {@link PathFilter} factories
466
+ * ({@link newPathFilter}, {@link newRegexpPathFilter},
467
+ * {@link newGlobPathFilter}) or pass any predicate of shape
468
+ * `(path) => boolean | Promise<boolean>`.
469
+ *
470
+ * @example
471
+ * ```ts
472
+ * import {
473
+ * FilteredFilesApi,
474
+ * newGlobPathFilter,
475
+ * newPathFilter,
476
+ * newRegexpPathFilter,
477
+ * } from "@statewalker/webrun-files-composite";
478
+ *
479
+ * // Hide by path prefix
480
+ * const noVcs = new FilteredFilesApi(sourceFiles, newPathFilter("/.git", "/.cache"));
481
+ * await noVcs.exists("/.git"); // false
482
+ * await noVcs.write("/.git/x", data); // throws "Path is hidden"
483
+ *
484
+ * // Hide by regexp
485
+ * const noLogs = new FilteredFilesApi(sourceFiles, newRegexpPathFilter(/\.log$/));
486
+ *
487
+ * // Hide by glob (extended + globstar mode)
488
+ * const noJunk = new FilteredFilesApi(
489
+ * sourceFiles,
490
+ * newGlobPathFilter("**​/*.log", "/.git", "/.git/**"),
491
+ * );
492
+ * ```
493
+ */
494
+ var FilteredFilesApi = class {
495
+ source;
496
+ pathFilter;
497
+ /**
498
+ * @param source The underlying `FilesApi` whose paths will be selectively
499
+ * hidden. Operations always delegate to this instance; the decorator
500
+ * only adds the visibility check.
501
+ * @param pathFilter Predicate that decides per-call whether a normalized
502
+ * path is visible. See {@link PathFilter}.
503
+ */
504
+ constructor(source, pathFilter) {
505
+ this.source = source;
506
+ this.pathFilter = pathFilter;
507
+ }
508
+ async isHidden(path) {
509
+ return await this.pathFilter(normalizePath(path)) === false;
510
+ }
511
+ async *read(path, options) {
512
+ if (await this.isHidden(path)) return;
513
+ yield* this.source.read(path, options);
514
+ }
515
+ async write(path, content) {
516
+ if (await this.isHidden(path)) throw new Error(`Path is hidden: ${path}`);
517
+ await this.source.write(path, content);
518
+ }
519
+ async mkdir(path) {
520
+ if (await this.isHidden(path)) throw new Error(`Path is hidden: ${path}`);
521
+ await this.source.mkdir(path);
522
+ }
523
+ async *list(path, options) {
524
+ if (await this.isHidden(path)) return;
525
+ for await (const entry of this.source.list(path, options)) {
526
+ if (await this.isHidden(entry.path)) continue;
527
+ yield entry;
528
+ }
529
+ }
530
+ async stats(path) {
531
+ if (await this.isHidden(path)) return void 0;
532
+ return this.source.stats(path);
533
+ }
534
+ async exists(path) {
535
+ if (await this.isHidden(path)) return false;
536
+ return this.source.exists(path);
537
+ }
538
+ async remove(path) {
539
+ if (await this.isHidden(path)) return false;
540
+ return this.source.remove(path);
541
+ }
542
+ async move(source, target) {
543
+ if (await this.isHidden(source) || await this.isHidden(target)) return false;
544
+ return this.source.move(source, target);
545
+ }
546
+ async copy(source, target) {
547
+ if (await this.isHidden(source) || await this.isHidden(target)) return false;
548
+ return this.source.copy(source, target);
549
+ }
550
+ };
551
+ //#endregion
552
+ //#region src/guarded-files-api.ts
553
+ /**
554
+ * `FilesApi` decorator that runs every call through an ordered list of
555
+ * {@link FileGuard}s. A guard fires when its `operations` set intersects the
556
+ * effective operation(s) for the current call. The first guard whose
557
+ * `check` returns `false` aborts the call by throwing an `Error` with that
558
+ * guard's `message` (defaulting to `"Access denied"`) followed by the
559
+ * normalized path.
560
+ *
561
+ * Effective operations per call:
562
+ *
563
+ * | Method | Operations checked |
564
+ * | ------------- | --------------------------------------------------- |
565
+ * | `read` | `read` |
566
+ * | `write` | `write` |
567
+ * | `mkdir` | `mkdir` |
568
+ * | `remove` | `remove` |
569
+ * | `list` | `list` on the path AND on each directory entry |
570
+ * | `stats` | `list` (a stat reveals existence like a tiny list) |
571
+ * | `exists` | `read` (existence is a read of metadata) |
572
+ * | `move(s, t)` | `move`+`read` on source; `move`+`write` on target |
573
+ * | `copy(s, t)` | `copy`+`read` on source; `copy`+`write` on target |
574
+ *
575
+ * The expanded checks for `move`/`copy` mean a guard that blocks `read` on
576
+ * a path also prevents move/copy *from* that path, and a `write`-blocking
577
+ * guard prevents move/copy *to* it. Likewise, an `exists` call respects any
578
+ * read guard, and `stats` respects any list guard.
579
+ *
580
+ * @example
581
+ * ```ts
582
+ * const api = new GuardedFilesApi(source, [
583
+ * {
584
+ * operations: ["write", "remove", "move", "mkdir"],
585
+ * check: (p) => !p.startsWith("/.system/"),
586
+ * message: "system folder is read-only",
587
+ * },
588
+ * ]);
589
+ * await api.write("/.system/cfg", data); // throws "system folder is read-only: /.system/cfg"
590
+ * ```
591
+ */
592
+ var GuardedFilesApi = class {
593
+ source;
594
+ guards;
595
+ /**
596
+ * @param source The underlying `FilesApi` whose calls will be policed.
597
+ * Allowed operations delegate straight through.
598
+ * @param guards Ordered list of access policies. The wrapper takes a
599
+ * defensive copy, so mutating the array afterwards has no effect.
600
+ * An empty list disables every check (the wrapper becomes a passthrough).
601
+ */
602
+ constructor(source, guards) {
603
+ this.source = source;
604
+ this.guards = [...guards];
605
+ }
606
+ checkGuard(path, ...operations) {
607
+ const normalized = normalizePath(path);
608
+ for (const guard of this.guards) {
609
+ if (!operations.some((op) => guard.operations.includes(op))) continue;
610
+ if (!guard.check(normalized)) {
611
+ const msg = guard.message ?? "Access denied";
612
+ throw new Error(`${msg}: ${normalized}`);
613
+ }
614
+ }
615
+ }
616
+ read(path, options) {
617
+ this.checkGuard(path, "read");
618
+ return this.source.read(path, options);
619
+ }
620
+ async write(path, content) {
621
+ this.checkGuard(path, "write");
622
+ return this.source.write(path, content);
623
+ }
624
+ async mkdir(path) {
625
+ this.checkGuard(path, "mkdir");
626
+ return this.source.mkdir(path);
627
+ }
628
+ async *list(path, options) {
629
+ this.checkGuard(path, "list");
630
+ for await (const info of this.source.list(path, options)) {
631
+ if (info.kind === "directory") this.checkGuard(info.path, "list");
632
+ yield info;
633
+ }
634
+ }
635
+ stats(path) {
636
+ this.checkGuard(path, "list");
637
+ return this.source.stats(path);
638
+ }
639
+ exists(path) {
640
+ this.checkGuard(path, "read");
641
+ return this.source.exists(path);
642
+ }
643
+ async remove(path) {
644
+ this.checkGuard(path, "remove");
645
+ return this.source.remove(path);
646
+ }
647
+ async move(source, target) {
648
+ this.checkGuard(source, "move", "read");
649
+ this.checkGuard(target, "move", "write");
650
+ return this.source.move(source, target);
651
+ }
652
+ async copy(source, target) {
653
+ this.checkGuard(source, "copy", "read");
654
+ this.checkGuard(target, "copy", "write");
655
+ return this.source.copy(source, target);
656
+ }
657
+ };
658
+ //#endregion
239
659
  exports.CompositeFilesApi = CompositeFilesApi;
660
+ exports.FilteredFilesApi = FilteredFilesApi;
661
+ exports.GuardedFilesApi = GuardedFilesApi;
662
+ exports.globToRegExp = globToRegExp;
663
+ exports.newGlobPathFilter = newGlobPathFilter;
664
+ exports.newPathFilter = newPathFilter;
665
+ exports.newRegexpPathFilter = newRegexpPathFilter;
@@ -1,16 +1,62 @@
1
1
  import type { FileInfo, FileStats, FilesApi, ListOptions, ReadOptions } from "@statewalker/webrun-files";
2
- import type { FileOperation } from "./types.js";
2
+ /**
3
+ * Composite `FilesApi` that routes calls to one of several backends based
4
+ * on a path prefix. Mounts are matched by **longest prefix wins**, so a
5
+ * mount at `/a/b` takes precedence over a mount at `/a` for paths under
6
+ * `/a/b/...`. The mount point itself appears in listings as a synthetic
7
+ * directory and cannot be removed.
8
+ *
9
+ * Each backend can use a sub-directory of its own filesystem as the mount
10
+ * root via `fsPath` (constructor `rootPath` for the implicit root mount,
11
+ * `fsPath` argument for additional mounts). Cross-mount `move` is
12
+ * implemented as copy-then-delete; there is no atomicity guarantee.
13
+ *
14
+ * Access control and visibility filtering are intentionally **not** part of
15
+ * this class — wrap with {@link GuardedFilesApi} or {@link FilteredFilesApi}
16
+ * (or both) instead.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const fs = new CompositeFilesApi(localFs, "/projects")
21
+ * .mount("/docs", s3Fs, "/documentation")
22
+ * .mount("/cache", memFs);
23
+ * await fs.write("/readme.md", data); // → localFs:/projects/readme.md
24
+ * await fs.write("/docs/api.md", data); // → s3Fs:/documentation/api.md
25
+ * ```
26
+ */
3
27
  export declare class CompositeFilesApi implements FilesApi {
4
28
  private mounts;
5
- private guards;
29
+ /**
30
+ * @param root Default backend used for any path that does not match a
31
+ * more specific mount. All paths are routed here unless `mount()`
32
+ * intercepts them.
33
+ * @param rootPath Optional sub-directory of the root backend to use as
34
+ * the composite filesystem's `/`. For example, `rootPath = "/projects"`
35
+ * makes the composite path `/readme.md` resolve to `/projects/readme.md`
36
+ * in the root backend. Defaults to `"/"` (no remapping).
37
+ */
6
38
  constructor(root: FilesApi, rootPath?: string);
39
+ /**
40
+ * Attaches a backend to handle every composite path under `path`. The
41
+ * mount prefix is normalized; paths under it are resolved against the
42
+ * mount's `fsPath` sub-directory (defaulting to `"/"`).
43
+ *
44
+ * @param path Composite-namespace prefix (e.g. `"/docs"`). Mounting at
45
+ * `"/"` is forbidden — use the constructor `root` argument instead.
46
+ * @param api The backend `FilesApi` to delegate to for paths under
47
+ * `path`. Wrap it in {@link FilteredFilesApi} / {@link GuardedFilesApi}
48
+ * first if you want mount-local filtering or guards.
49
+ * @param fsPath Sub-directory of the mounted backend used as its mount
50
+ * root, e.g. `mount("/docs", s3, "/documentation")` makes
51
+ * `/docs/api.md` resolve to `/documentation/api.md` on `s3`.
52
+ * @returns `this`, for chaining.
53
+ * @throws If `path` normalizes to `"/"`.
54
+ */
7
55
  mount(path: string, api: FilesApi, fsPath?: string): this;
8
- guard(operations: FileOperation[], check: (path: string) => boolean, message?: string): this;
9
56
  private resolve;
10
57
  private isMountPoint;
11
58
  /** Returns mount prefixes that are direct children of the given path. */
12
59
  private childMountPrefixes;
13
- private checkGuard;
14
60
  read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array>;
15
61
  write(path: string, content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>): Promise<void>;
16
62
  mkdir(path: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"composite-files-api.d.ts","sourceRoot":"","sources":["../src/composite-files-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAa,aAAa,EAAE,MAAM,YAAY,CAAC;AAQ3D,qBAAa,iBAAkB,YAAW,QAAQ;IAChD,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,MAAM,CAAmB;gBAErB,IAAI,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,MAAM;IAI7C,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAWzD,KAAK,CAAC,UAAU,EAAE,aAAa,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAO5F,OAAO,CAAC,OAAO;IAgBf,OAAO,CAAC,YAAY;IAKpB,yEAAyE;IACzE,OAAO,CAAC,kBAAkB;IAkB1B,OAAO,CAAC,UAAU;IAalB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,UAAU,CAAC;IAM9D,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC;IAMV,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMjC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,QAAQ,CAAC;IA+CnE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IASnD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAStC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUtC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAmBtD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiB5D,OAAO,CAAC,aAAa;YAOP,SAAS;IA4BvB,OAAO,CAAC,SAAS;IAejB,OAAO,CAAC,iBAAiB;CAQ1B"}
1
+ {"version":3,"file":"composite-files-api.d.ts","sourceRoot":"","sources":["../src/composite-files-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;AASnC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,iBAAkB,YAAW,QAAQ;IAChD,OAAO,CAAC,MAAM,CAAe;IAE7B;;;;;;;;OAQG;gBACS,IAAI,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,MAAM;IAI7C;;;;;;;;;;;;;;;OAeG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAazD,OAAO,CAAC,OAAO;IAgBf,OAAO,CAAC,YAAY;IAKpB,yEAAyE;IACzE,OAAO,CAAC,kBAAkB;IAkB1B,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,UAAU,CAAC;IAK9D,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC;IAKV,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,QAAQ,CAAC;IA8CnE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IASnD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAStC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAStC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBtD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAe5D,OAAO,CAAC,aAAa;YAOP,SAAS;IA4BvB,OAAO,CAAC,SAAS;IAejB,OAAO,CAAC,iBAAiB;CAQ1B"}