@poe-platform/safe-fs 0.1.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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +290 -0
  3. package/dist/safe-fs/bridge/confinement.d.ts +3 -0
  4. package/dist/safe-fs/bridge/confinement.js +171 -0
  5. package/dist/safe-fs/bridge/filesystem.d.ts +88 -0
  6. package/dist/safe-fs/bridge/filesystem.js +411 -0
  7. package/dist/safe-fs/bridge/index.d.ts +6 -0
  8. package/dist/safe-fs/bridge/index.js +18 -0
  9. package/dist/safe-fs/bridge/stats.d.ts +4 -0
  10. package/dist/safe-fs/bridge/stats.js +37 -0
  11. package/dist/safe-fs/bridge/types.d.ts +75 -0
  12. package/dist/safe-fs/bridge/types.js +1 -0
  13. package/dist/safe-fs/bridge/values.d.ts +6 -0
  14. package/dist/safe-fs/bridge/values.js +65 -0
  15. package/dist/safe-fs/config/memory.d.ts +3 -0
  16. package/dist/safe-fs/config/memory.js +9 -0
  17. package/dist/safe-fs/config/real.d.ts +5 -0
  18. package/dist/safe-fs/config/real.js +16 -0
  19. package/dist/safe-fs/config.d.ts +15 -0
  20. package/dist/safe-fs/config.js +46 -0
  21. package/dist/safe-fs/config.node.d.ts +2 -0
  22. package/dist/safe-fs/config.node.js +22 -0
  23. package/dist/safe-fs/contracts/abort.d.ts +5 -0
  24. package/dist/safe-fs/contracts/abort.js +34 -0
  25. package/dist/safe-fs/contracts/cleanup.d.ts +1 -0
  26. package/dist/safe-fs/contracts/cleanup.js +9 -0
  27. package/dist/safe-fs/contracts/errors.d.ts +50 -0
  28. package/dist/safe-fs/contracts/errors.js +77 -0
  29. package/dist/safe-fs/contracts/filesystem.d.ts +96 -0
  30. package/dist/safe-fs/contracts/filesystem.js +1 -0
  31. package/dist/safe-fs/contracts/index.d.ts +4 -0
  32. package/dist/safe-fs/contracts/index.js +4 -0
  33. package/dist/safe-fs/contracts/io.d.ts +8 -0
  34. package/dist/safe-fs/contracts/io.js +92 -0
  35. package/dist/safe-fs/contracts/path.d.ts +8 -0
  36. package/dist/safe-fs/contracts/path.js +8 -0
  37. package/dist/safe-fs/contracts/portable-path.d.ts +13 -0
  38. package/dist/safe-fs/contracts/portable-path.js +57 -0
  39. package/dist/safe-fs/contracts/virtual-path.d.ts +7 -0
  40. package/dist/safe-fs/contracts/virtual-path.js +66 -0
  41. package/dist/safe-fs/core.d.ts +12 -0
  42. package/dist/safe-fs/core.js +12 -0
  43. package/dist/safe-fs/fs/memory/index.d.ts +58 -0
  44. package/dist/safe-fs/fs/memory/index.js +532 -0
  45. package/dist/safe-fs/fs/mount/comparison.d.ts +19 -0
  46. package/dist/safe-fs/fs/mount/comparison.js +118 -0
  47. package/dist/safe-fs/fs/mount/identity.d.ts +2 -0
  48. package/dist/safe-fs/fs/mount/identity.js +12 -0
  49. package/dist/safe-fs/fs/mount/index.d.ts +44 -0
  50. package/dist/safe-fs/fs/mount/index.js +546 -0
  51. package/dist/safe-fs/fs/overlay/index.d.ts +73 -0
  52. package/dist/safe-fs/fs/overlay/index.js +927 -0
  53. package/dist/safe-fs/fs/readonly/index.d.ts +31 -0
  54. package/dist/safe-fs/fs/readonly/index.js +128 -0
  55. package/dist/safe-fs/fs/real/allocation.d.ts +1 -0
  56. package/dist/safe-fs/fs/real/allocation.js +8 -0
  57. package/dist/safe-fs/fs/real/index.d.ts +76 -0
  58. package/dist/safe-fs/fs/real/index.js +543 -0
  59. package/dist/safe-fs/fs/s3/authority.d.ts +7 -0
  60. package/dist/safe-fs/fs/s3/authority.js +31 -0
  61. package/dist/safe-fs/fs/s3/filesystem.d.ts +93 -0
  62. package/dist/safe-fs/fs/s3/filesystem.js +1024 -0
  63. package/dist/safe-fs/fs/s3/http/index.d.ts +2 -0
  64. package/dist/safe-fs/fs/s3/http/index.js +1 -0
  65. package/dist/safe-fs/fs/s3/http/request.d.ts +16 -0
  66. package/dist/safe-fs/fs/s3/http/request.js +196 -0
  67. package/dist/safe-fs/fs/s3/http/signature.d.ts +20 -0
  68. package/dist/safe-fs/fs/s3/http/signature.js +74 -0
  69. package/dist/safe-fs/fs/s3/http/transport.d.ts +3 -0
  70. package/dist/safe-fs/fs/s3/http/transport.js +455 -0
  71. package/dist/safe-fs/fs/s3/http/types.d.ts +30 -0
  72. package/dist/safe-fs/fs/s3/http/types.js +1 -0
  73. package/dist/safe-fs/fs/s3/http/xml.d.ts +11 -0
  74. package/dist/safe-fs/fs/s3/http/xml.js +188 -0
  75. package/dist/safe-fs/fs/s3/index.d.ts +6 -0
  76. package/dist/safe-fs/fs/s3/index.js +3 -0
  77. package/dist/safe-fs/fs/s3/mock.d.ts +48 -0
  78. package/dist/safe-fs/fs/s3/mock.js +233 -0
  79. package/dist/safe-fs/fs/s3/registry.d.ts +10 -0
  80. package/dist/safe-fs/fs/s3/registry.js +71 -0
  81. package/dist/safe-fs/fs/s3/transport.d.ts +103 -0
  82. package/dist/safe-fs/fs/s3/transport.js +28 -0
  83. package/dist/safe-fs/fs/webdav/index.d.ts +2 -0
  84. package/dist/safe-fs/fs/webdav/index.js +1 -0
  85. package/dist/safe-fs/fs/webdav/resource-id.d.ts +18 -0
  86. package/dist/safe-fs/fs/webdav/resource-id.js +109 -0
  87. package/dist/safe-fs/fs/webdav/webdav.d.ts +101 -0
  88. package/dist/safe-fs/fs/webdav/webdav.js +1226 -0
  89. package/dist/safe-fs/fs/webdav/xml.d.ts +15 -0
  90. package/dist/safe-fs/fs/webdav/xml.js +250 -0
  91. package/dist/safe-fs/index.d.ts +14 -0
  92. package/dist/safe-fs/index.js +12 -0
  93. package/dist/safe-fs/node/filesystem.d.ts +11 -0
  94. package/dist/safe-fs/node/filesystem.js +28 -0
  95. package/dist/safe-fs/node/index.d.ts +2 -0
  96. package/dist/safe-fs/node/index.js +1 -0
  97. package/dist/safe-fs/node/stats.d.ts +1 -0
  98. package/dist/safe-fs/node/stats.js +1 -0
  99. package/dist/safe-fs/node/values.d.ts +1 -0
  100. package/dist/safe-fs/node/values.js +1 -0
  101. package/dist/safe-fs/node-host.d.ts +1 -0
  102. package/dist/safe-fs/node-host.js +1 -0
  103. package/dist/safe-fs/node-unavailable.d.ts +1 -0
  104. package/dist/safe-fs/node-unavailable.js +1 -0
  105. package/dist/safe-fs/platform/browser.d.ts +12 -0
  106. package/dist/safe-fs/platform/browser.js +39 -0
  107. package/dist/safe-fs/platform/node.d.ts +13 -0
  108. package/dist/safe-fs/platform/node.js +26 -0
  109. package/package.json +108 -0
@@ -0,0 +1,546 @@
1
+ import { FsError, isFsError, toFsError } from "../../contracts/errors.js";
2
+ import { readBytes } from "../../contracts/io.js";
3
+ import { finishCleanup } from "../../contracts/cleanup.js";
4
+ import { normalizePath, validatePath } from "../../contracts/virtual-path.js";
5
+ import { compareIdentity } from "./identity.js";
6
+ import { compareEntries, registerEntryAuthority, registerEntryView } from "./comparison.js";
7
+ const syntheticStat = Object.freeze({
8
+ type: "directory", size: 0, mode: 0o40555,
9
+ mtimeMs: 0, atimeMs: 0, ctimeMs: 0,
10
+ });
11
+ function within(parent, path) {
12
+ return parent === "/" || path === parent || path.startsWith(`${parent}/`);
13
+ }
14
+ function globalPath(path) {
15
+ return typeof path === "string" && path.startsWith("/") ? path : `/${String(path)}`;
16
+ }
17
+ function fail(code) {
18
+ throw new FsError(code);
19
+ }
20
+ function snapshotStat(stat) {
21
+ const { type, size, allocatedBytes, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
22
+ return {
23
+ type, size, mode, mtimeMs, atimeMs, ctimeMs,
24
+ ...(allocatedBytes === undefined ? {} : { allocatedBytes }),
25
+ ...(birthtimeMs === undefined ? {} : { birthtimeMs }),
26
+ ...(identityScope === undefined ? {} : { identityScope }),
27
+ ...(ino === undefined ? {} : { ino }),
28
+ ...(dev === undefined ? {} : { dev }),
29
+ ...(nlink === undefined ? {} : { nlink }),
30
+ ...(uid === undefined ? {} : { uid }),
31
+ ...(gid === undefined ? {} : { gid }),
32
+ };
33
+ }
34
+ export class MountFileSystem {
35
+ capabilities;
36
+ mounts;
37
+ constructor(options) {
38
+ const mounts = [];
39
+ const paths = new Set();
40
+ const add = (path, backend) => {
41
+ if (backend instanceof MountFileSystem) {
42
+ for (const child of backend.mounts) {
43
+ add(path === "/" ? child.path : child.path === "/" ? path : `${path}${child.path}`, child.backend);
44
+ }
45
+ return;
46
+ }
47
+ if (paths.has(path)) {
48
+ throw new FsError("EINVAL", { syscall: "mount", path });
49
+ }
50
+ paths.add(path);
51
+ mounts.push({ path, backend });
52
+ };
53
+ add("/", options.root);
54
+ for (const [input, backend] of Object.entries(options.mounts ?? {})) {
55
+ try {
56
+ validatePath(input);
57
+ }
58
+ catch (error) {
59
+ throw new FsError("EINVAL", { syscall: "mount", path: globalPath(input), cause: error });
60
+ }
61
+ const path = normalizePath(input);
62
+ if (!input.startsWith("/") || path === "/") {
63
+ throw new FsError("EINVAL", { syscall: "mount", path: globalPath(input) });
64
+ }
65
+ add(path, backend);
66
+ }
67
+ this.mounts = mounts.sort((left, right) => right.path.length - left.path.length);
68
+ registerEntryView(this, (path, options) => this.operation("compareEntry", path, options, async () => {
69
+ const location = await this.resolve(path, options);
70
+ if (location.synthetic)
71
+ return { filesystem: this, path, stat: syntheticStat, readOnly: true };
72
+ return { filesystem: location.mount.backend, path: location.local };
73
+ }));
74
+ registerEntryAuthority(this, async () => "unknown");
75
+ const all = (capability, methods = []) => mounts.every(({ backend }) => backend.capabilities[capability] === true
76
+ && methods.every((method) => typeof backend[method] === "function"));
77
+ const streaming = (capability, method) => all(capability, [method]) ? true : mounts.some(({ backend }) => backend.capabilities[capability] !== false && typeof backend[method] === "function") ? undefined : false;
78
+ const streamingRead = streaming("streamingRead", "readStream");
79
+ const streamingWrite = streaming("streamingWrite", "writeStream");
80
+ this.capabilities = Object.freeze({
81
+ get snapshotRmdir() { return mounts.some(({ backend }) => backend.capabilities.snapshotRmdir === true); },
82
+ readOnly: all("readOnly"),
83
+ symlinks: all("symlinks", ["readlink", "symlink"]),
84
+ hardlinks: all("hardlinks", ["link"]),
85
+ permissions: all("permissions", ["chmod"]),
86
+ timestamps: all("timestamps", ["utimes"]),
87
+ atomicRename: mounts.length === 1 && all("atomicRename"),
88
+ ...(streamingRead === undefined ? {} : { streamingRead }),
89
+ ...(streamingWrite === undefined ? {} : { streamingWrite }),
90
+ });
91
+ }
92
+ select(path) {
93
+ return this.mounts.find((mount) => within(mount.path, path));
94
+ }
95
+ protected(path) {
96
+ return path === "/" || this.mounts.some((mount) => within(path, mount.path));
97
+ }
98
+ error(error, syscall, path, options, dest) {
99
+ if (options.signal?.aborted && error === options.signal.reason)
100
+ return error;
101
+ return new FsError(toFsError(error).code, {
102
+ cause: error, syscall, path: globalPath(path), ...(dest === undefined ? {} : { dest: globalPath(dest) }),
103
+ });
104
+ }
105
+ async operation(syscall, path, options, action, dest) {
106
+ try {
107
+ options.signal?.throwIfAborted();
108
+ const result = await action();
109
+ options.signal?.throwIfAborted();
110
+ return result;
111
+ }
112
+ catch (error) {
113
+ throw this.error(error, syscall, path, options, dest);
114
+ }
115
+ }
116
+ components(path) {
117
+ validatePath(path);
118
+ if (!path)
119
+ fail("ENOENT");
120
+ const parts = path.split("/").filter(Boolean).map((name) => ({ name }));
121
+ if (path.endsWith("/"))
122
+ parts.push({ name: ".", trailing: true });
123
+ return parts;
124
+ }
125
+ async lookup(path, options, parent, plannedParent = false) {
126
+ options.signal?.throwIfAborted();
127
+ const mount = this.select(path);
128
+ const local = mount.path === "/" ? path : path.slice(mount.path.length) || "/";
129
+ let stat;
130
+ if (!parent?.synthetic && !plannedParent || parent?.mount !== mount) {
131
+ try {
132
+ stat = await mount.backend.lstat(local, options);
133
+ }
134
+ catch (error) {
135
+ if (toFsError(error).code !== "ENOENT")
136
+ throw error;
137
+ }
138
+ }
139
+ options.signal?.throwIfAborted();
140
+ if (path === mount.path && stat?.type !== "directory")
141
+ fail(stat ? "ENOTDIR" : "ENOENT");
142
+ const synthetic = this.protected(path) && stat?.type !== "directory";
143
+ return { path, mount, local, stat: synthetic ? syntheticStat : stat, synthetic };
144
+ }
145
+ async resolve(path, options, settings = {}) {
146
+ let pending = this.components(path);
147
+ const stack = [await this.lookup("/", options)];
148
+ let boundary;
149
+ let verification;
150
+ const verified = async (location) => {
151
+ if (verification) {
152
+ const canonical = await verification.mount.backend.realpath(verification.path, options);
153
+ validatePath(canonical);
154
+ if (!canonical.startsWith("/") || normalizePath(canonical) !== canonical)
155
+ fail("EIO");
156
+ if (verification.finalName !== undefined) {
157
+ try {
158
+ await verification.mount.backend.lstat(`${verification.path}/${verification.finalName}`, options);
159
+ }
160
+ catch (error) {
161
+ if (location.stat || toFsError(error).code !== "ENOENT")
162
+ throw error;
163
+ }
164
+ }
165
+ const expected = verification.finalName === undefined ? canonical
166
+ : `${canonical === "/" ? "" : canonical}/${verification.finalName}`;
167
+ if (location.mount !== verification.mount || location.local !== expected)
168
+ fail("ENOTSUP");
169
+ }
170
+ return location;
171
+ };
172
+ let links = 0;
173
+ while (pending.length > 0) {
174
+ options.signal?.throwIfAborted();
175
+ const component = pending.shift();
176
+ const current = stack[stack.length - 1];
177
+ if (current.stat?.type !== "directory")
178
+ fail("ENOTDIR");
179
+ if (!current.synthetic && !settings.createDirectories?.has(current.path)) {
180
+ const mode = current.mount.backend.capabilities.permissions === false ? 0 : 1;
181
+ await current.mount.backend.access(current.local, mode, options);
182
+ }
183
+ if (component.name === ".")
184
+ continue;
185
+ if (component.name === "..") {
186
+ if (boundary && current.path === boundary.path)
187
+ fail("EACCES");
188
+ if (stack.length > 1)
189
+ stack.pop();
190
+ if (boundary && stack[stack.length - 1].mount !== boundary)
191
+ fail("EACCES");
192
+ continue;
193
+ }
194
+ const nextPath = `${current.path === "/" ? "" : current.path}/${component.name}`;
195
+ if (boundary && this.select(nextPath) !== boundary)
196
+ fail("EACCES");
197
+ let next = settings.createDirectories?.get(nextPath)
198
+ ?? await this.lookup(nextPath, options, current, settings.createDirectories?.has(current.path));
199
+ if (!next.stat) {
200
+ if (settings.createDirectories) {
201
+ if (current.synthetic)
202
+ fail("ENOTSUP");
203
+ this.mutable(next);
204
+ next = { ...next, stat: syntheticStat };
205
+ settings.createDirectories.set(nextPath, next);
206
+ }
207
+ else if (settings.allowMissing && (pending.length === 0
208
+ || settings.missingDirectory && pending.length === 1 && pending[0].trailing)) {
209
+ if (current.synthetic)
210
+ fail("ENOTSUP");
211
+ return verified(next);
212
+ }
213
+ else {
214
+ fail("ENOENT");
215
+ }
216
+ }
217
+ const final = pending.length === 0
218
+ || settings.entry && pending.length === 1 && pending[0].trailing;
219
+ if (next.stat?.type === "symlink" && (settings.followFinal !== false || !final)) {
220
+ if (++links > 40)
221
+ fail("ELOOP");
222
+ boundary ??= next.mount;
223
+ if (boundary !== next.mount)
224
+ fail("EACCES");
225
+ if (!verification) {
226
+ const suffix = pending.map((part) => part.name);
227
+ const last = suffix.at(-1);
228
+ const finalName = settings.followFinal === false && last !== undefined && last !== "." && last !== ".."
229
+ ? suffix.pop() : undefined;
230
+ verification = {
231
+ mount: next.mount,
232
+ path: `${next.local}${suffix.length === 0 ? "" : `/${suffix.join("/")}`}`,
233
+ finalName,
234
+ };
235
+ }
236
+ const readlink = this.optional(next, "readlink", "symlinks");
237
+ const target = await readlink.call(next.mount.backend, next.local, options);
238
+ const targetParts = this.components(target);
239
+ if (target.startsWith("/")) {
240
+ const rootIndex = stack.findIndex((entry) => entry.path === next.mount.path);
241
+ if (rootIndex < 0)
242
+ fail("EACCES");
243
+ stack.splice(rootIndex + 1);
244
+ }
245
+ pending = [...targetParts, ...pending];
246
+ }
247
+ else {
248
+ stack.push(next);
249
+ }
250
+ }
251
+ return verified(stack[stack.length - 1]);
252
+ }
253
+ mutable(location) {
254
+ if (this.protected(location.path))
255
+ fail("EBUSY");
256
+ if (location.mount.backend.capabilities.readOnly)
257
+ fail("EROFS");
258
+ }
259
+ entryPath(path) {
260
+ const terminal = path.split("/").filter(Boolean).at(-1);
261
+ if (terminal === "." || terminal === "..")
262
+ fail("EINVAL");
263
+ }
264
+ optional(location, method, capability) {
265
+ const backend = location.mount.backend;
266
+ const implementation = backend[method];
267
+ if (!implementation || capability && backend.capabilities[capability] === false)
268
+ fail("ENOTSUP");
269
+ return implementation;
270
+ }
271
+ readFile(path, options = {}) {
272
+ return this.operation("readFile", path, options, async () => {
273
+ const location = await this.resolve(path, options);
274
+ if (location.synthetic)
275
+ fail("EISDIR");
276
+ return location.mount.backend.readFile(location.local, options);
277
+ });
278
+ }
279
+ writeFile(path, data, options = {}) {
280
+ return this.operation("writeFile", path, options, async () => {
281
+ const location = await this.resolve(path, options, { allowMissing: true, followFinal: !options.flag?.endsWith("x") });
282
+ this.mutable(location);
283
+ await location.mount.backend.writeFile(location.local, data, options);
284
+ });
285
+ }
286
+ appendFile(path, data, options = {}) {
287
+ return this.operation("appendFile", path, options, async () => {
288
+ const location = await this.resolve(path, options, { allowMissing: true });
289
+ this.mutable(location);
290
+ await location.mount.backend.appendFile(location.local, data, options);
291
+ });
292
+ }
293
+ stat(path, options = {}) {
294
+ return this.operation("stat", path, options, async () => snapshotStat((await this.resolve(path, options)).stat));
295
+ }
296
+ lstat(path, options = {}) {
297
+ return this.operation("lstat", path, options, async () => snapshotStat((await this.resolve(path, options, { followFinal: false })).stat));
298
+ }
299
+ readdir(path, options = {}) {
300
+ return this.operation("readdir", path, options, async () => {
301
+ const location = await this.resolve(path, options);
302
+ if (location.stat?.type !== "directory")
303
+ fail("ENOTDIR");
304
+ const entries = new Map();
305
+ if (!location.synthetic) {
306
+ for (const entry of await location.mount.backend.readdir(location.local, options)) {
307
+ const { name, type } = entry;
308
+ if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0"))
309
+ fail("EIO");
310
+ entries.set(name, { name, type });
311
+ }
312
+ }
313
+ for (const mount of this.mounts) {
314
+ if (mount.path !== location.path && within(location.path, mount.path)) {
315
+ const suffix = mount.path.slice(location.path === "/" ? 1 : location.path.length + 1);
316
+ const name = suffix.split("/")[0];
317
+ entries.set(name, { name, type: "directory" });
318
+ }
319
+ }
320
+ return [...entries.values()].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
321
+ });
322
+ }
323
+ mkdir(path, options = {}) {
324
+ return this.operation("mkdir", path, options, async () => {
325
+ const directories = new Map();
326
+ const location = await this.resolve(path, options, options.recursive
327
+ ? { createDirectories: directories }
328
+ : { followFinal: false, allowMissing: true, missingDirectory: true, entry: true });
329
+ if (options.recursive) {
330
+ if (location.stat?.type !== "directory")
331
+ fail("EEXIST");
332
+ for (const directory of directories.values()) {
333
+ options.signal?.throwIfAborted();
334
+ await directory.mount.backend.mkdir(directory.local, { ...options, recursive: false });
335
+ }
336
+ return;
337
+ }
338
+ this.mutable(location);
339
+ await location.mount.backend.mkdir(location.local, options);
340
+ });
341
+ }
342
+ rmdir(path, options = {}) {
343
+ return this.operation("rmdir", path, options, async () => {
344
+ const location = await this.resolve(path, options, { followFinal: false, entry: true });
345
+ this.mutable(location);
346
+ this.entryPath(path);
347
+ if (location.stat?.type !== "directory")
348
+ fail("ENOTDIR");
349
+ const backend = location.mount.backend;
350
+ if (!backend.rmdir)
351
+ fail("ENOTSUP");
352
+ options.signal?.throwIfAborted();
353
+ await backend.rmdir(location.local, options);
354
+ });
355
+ }
356
+ rm(path, options = {}) {
357
+ return this.operation("rm", path, options, async () => {
358
+ let location;
359
+ try {
360
+ location = await this.resolve(path, options, { followFinal: false, entry: true });
361
+ }
362
+ catch (error) {
363
+ if (options.force && isFsError(error, "ENOENT"))
364
+ return;
365
+ throw error;
366
+ }
367
+ this.mutable(location);
368
+ this.entryPath(path);
369
+ await location.mount.backend.rm(location.local, options);
370
+ });
371
+ }
372
+ rename(source, destination, options = {}) {
373
+ return this.operation("rename", source, options, async () => {
374
+ const origin = await this.resolve(source, options, { followFinal: false, entry: true });
375
+ const target = await this.resolve(destination, options, {
376
+ followFinal: false, allowMissing: true, entry: true, missingDirectory: origin.stat?.type === "directory",
377
+ });
378
+ if (this.protected(origin.path) || this.protected(target.path))
379
+ fail("EBUSY");
380
+ if (origin.mount !== target.mount)
381
+ fail("EXDEV");
382
+ this.mutable(origin);
383
+ this.mutable(target);
384
+ this.entryPath(source);
385
+ this.entryPath(destination);
386
+ await origin.mount.backend.rename(origin.local, target.local, options);
387
+ }, destination);
388
+ }
389
+ copyFile(source, destination, options = {}) {
390
+ return this.operation("copyFile", source, options, async () => {
391
+ const origin = await this.resolve(source, options);
392
+ if (origin.stat?.type === "directory")
393
+ fail("EISDIR");
394
+ const target = await this.resolve(destination, options, { allowMissing: true, followFinal: !options.exclusive });
395
+ if (this.protected(target.path))
396
+ fail("EBUSY");
397
+ this.mutable(target);
398
+ if (options.exclusive && target.stat)
399
+ fail("EEXIST");
400
+ if (origin.mount.backend === target.mount.backend && origin.local === target.local)
401
+ fail("EINVAL");
402
+ let identity = compareIdentity(origin.stat, target.stat);
403
+ if (identity === "same")
404
+ fail("EINVAL");
405
+ if (target.stat?.type === "directory")
406
+ fail("EISDIR");
407
+ if (target.stat && identity === "unknown") {
408
+ identity = await compareEntries(origin.mount.backend, origin.local, target.mount.backend, target.local, options);
409
+ if (identity === "same")
410
+ fail("EINVAL");
411
+ }
412
+ if (target.stat && identity === "unknown")
413
+ fail("ENOTSUP");
414
+ if (origin.mount.backend === target.mount.backend) {
415
+ await origin.mount.backend.copyFile(origin.local, target.local, { ...options, exclusive: options.exclusive || !target.stat });
416
+ return;
417
+ }
418
+ const reader = origin.mount.backend;
419
+ const writer = target.mount.backend;
420
+ const writeOptions = {
421
+ ...(options.signal ? { signal: options.signal } : {}), flag: options.exclusive || !target.stat ? "wx" : "w",
422
+ ...(writer.capabilities.permissions === true ? { mode: origin.stat.mode & 0o7777 } : {}),
423
+ };
424
+ if (reader.readStream && reader.capabilities.streamingRead !== false
425
+ && writer.writeStream && writer.capabilities.streamingWrite !== false) {
426
+ const source = readBytes(reader.readStream(origin.local, options), options.signal);
427
+ let failed = false;
428
+ try {
429
+ await writer.writeStream(target.local, source, writeOptions);
430
+ }
431
+ catch (error) {
432
+ failed = true;
433
+ throw error;
434
+ }
435
+ finally {
436
+ await finishCleanup(() => source.return(undefined), failed);
437
+ }
438
+ }
439
+ else {
440
+ const maxBytes = 64 * 1024 * 1024;
441
+ const data = await reader.readFile(origin.local, { ...options, maxBytes });
442
+ if (data.byteLength > maxBytes)
443
+ fail("EFBIG");
444
+ options.signal?.throwIfAborted();
445
+ await writer.writeFile(target.local, data, writeOptions);
446
+ }
447
+ }, destination);
448
+ }
449
+ realpath(path, options = {}) {
450
+ return this.operation("realpath", path, options, async () => (await this.resolve(path, options)).path);
451
+ }
452
+ compareEntry(path, peer, peerPath, options = {}) {
453
+ return this.operation("compareEntry", path, options, () => compareEntries(this, path, peer, peerPath, options), peerPath);
454
+ }
455
+ access(path, mode = 0, options = {}) {
456
+ return this.operation("access", path, options, async () => {
457
+ if (!Number.isInteger(mode) || mode < 0 || mode > 7)
458
+ fail("EINVAL");
459
+ const location = await this.resolve(path, options);
460
+ if (location.synthetic) {
461
+ if (mode & 2)
462
+ fail("EACCES");
463
+ }
464
+ else {
465
+ await location.mount.backend.access(location.local, mode, options);
466
+ }
467
+ });
468
+ }
469
+ readlink(path, options = {}) {
470
+ return this.operation("readlink", path, options, async () => {
471
+ const location = await this.resolve(path, options, { followFinal: false });
472
+ if (location.stat?.type !== "symlink")
473
+ fail("EINVAL");
474
+ return this.optional(location, "readlink", "symlinks").call(location.mount.backend, location.local, options);
475
+ });
476
+ }
477
+ symlink(target, path, options = {}) {
478
+ return this.operation("symlink", path, options, async () => {
479
+ this.components(target);
480
+ const location = await this.resolve(path, options, { followFinal: false, allowMissing: true });
481
+ this.mutable(location);
482
+ await this.optional(location, "symlink", "symlinks").call(location.mount.backend, target, location.local, options);
483
+ });
484
+ }
485
+ link(existingPath, newPath, options = {}) {
486
+ return this.operation("link", existingPath, options, async () => {
487
+ const origin = await this.resolve(existingPath, options, { followFinal: false, entry: true });
488
+ const target = await this.resolve(newPath, options, { followFinal: false, allowMissing: true, entry: true });
489
+ if (this.protected(origin.path) || this.protected(target.path))
490
+ fail("EBUSY");
491
+ if (origin.mount !== target.mount)
492
+ fail("EXDEV");
493
+ this.mutable(origin);
494
+ this.mutable(target);
495
+ await this.optional(origin, "link", "hardlinks").call(origin.mount.backend, origin.local, target.local, options);
496
+ }, newPath);
497
+ }
498
+ chmod(path, mode, options = {}) {
499
+ return this.operation("chmod", path, options, async () => {
500
+ const location = await this.resolve(path, options);
501
+ this.mutable(location);
502
+ await this.optional(location, "chmod", "permissions").call(location.mount.backend, location.local, mode, options);
503
+ });
504
+ }
505
+ utimes(path, atimeMs, mtimeMs, options = {}) {
506
+ return this.operation("utimes", path, options, async () => {
507
+ const location = await this.resolve(path, options);
508
+ this.mutable(location);
509
+ await this.optional(location, "utimes", "timestamps").call(location.mount.backend, location.local, atimeMs, mtimeMs, options);
510
+ });
511
+ }
512
+ truncate(path, length = 0, options = {}) {
513
+ return this.operation("truncate", path, options, async () => {
514
+ const location = await this.resolve(path, options);
515
+ this.mutable(location);
516
+ await this.optional(location, "truncate").call(location.mount.backend, location.local, length, options);
517
+ });
518
+ }
519
+ async *readStream(path, options = {}) {
520
+ try {
521
+ options.signal?.throwIfAborted();
522
+ const location = await this.resolve(path, options);
523
+ if (location.synthetic)
524
+ fail("EISDIR");
525
+ const source = this.optional(location, "readStream", "streamingRead").call(location.mount.backend, location.local, options);
526
+ for await (const chunk of readBytes(source, options.signal)) {
527
+ options.signal?.throwIfAborted();
528
+ yield chunk;
529
+ }
530
+ options.signal?.throwIfAborted();
531
+ }
532
+ catch (error) {
533
+ throw this.error(error, "readStream", path, options);
534
+ }
535
+ }
536
+ writeStream(path, source, options = {}) {
537
+ return this.operation("writeStream", path, options, async () => {
538
+ const location = await this.resolve(path, options, { allowMissing: true, followFinal: !options.flag?.endsWith("x") });
539
+ this.mutable(location);
540
+ await this.optional(location, "writeStream", "streamingWrite").call(location.mount.backend, location.local, source, options);
541
+ });
542
+ }
543
+ }
544
+ export function createMountFileSystem(options) {
545
+ return new MountFileSystem(options);
546
+ }
@@ -0,0 +1,73 @@
1
+ import type { ByteSource } from "../../contracts/io.js";
2
+ import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
3
+ export interface OverlayFileSystemOptions {
4
+ readonly upper: FileSystem;
5
+ readonly lower: FileSystem;
6
+ readonly maxBufferBytes?: number;
7
+ }
8
+ export declare class OverlayFileSystem implements FileSystem {
9
+ #private;
10
+ readonly capabilities: FileSystemCapabilities;
11
+ private readonly maxBufferBytes;
12
+ private readonly whiteouts;
13
+ private readonly opaque;
14
+ private readonly garbage;
15
+ private readonly activeStages;
16
+ private readonly linkMetadata;
17
+ private readonly linkOrigins;
18
+ private queue;
19
+ constructor(options: OverlayFileSystemOptions);
20
+ private run;
21
+ private writable;
22
+ private permission;
23
+ private maybeStat;
24
+ private hidden;
25
+ private lowerVisible;
26
+ private lookup;
27
+ private resolve;
28
+ private linkTarget;
29
+ private movableLinkTarget;
30
+ private required;
31
+ private listing;
32
+ private bytes;
33
+ private cleanGarbage;
34
+ cleanup(options?: FsOptions): Promise<void>;
35
+ private staged;
36
+ private preserve;
37
+ private clone;
38
+ private rememberLink;
39
+ private ensureDirectory;
40
+ private copyUp;
41
+ private parent;
42
+ private makeDirectory;
43
+ private writeOptions;
44
+ private writeLocation;
45
+ private replace;
46
+ readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
47
+ writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
48
+ appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
49
+ stat(path: string, options?: FsOptions): Promise<FileStat>;
50
+ lstat(path: string, options?: FsOptions): Promise<FileStat>;
51
+ compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").EntryComparison>;
52
+ readdir(path: string, options?: FsOptions): Promise<DirectoryEntry[]>;
53
+ realpath(path: string, options?: FsOptions): Promise<string>;
54
+ access(path: string, requestedMode?: number, options?: FsOptions): Promise<void>;
55
+ mkdir(path: string, options?: MkdirOptions): Promise<void>;
56
+ rmdir(path: string, options?: FsOptions): Promise<void>;
57
+ rm(path: string, options?: RemoveOptions): Promise<void>;
58
+ private materialize;
59
+ rename(source: string, destination: string, options?: FsOptions): Promise<void>;
60
+ copyFile(source: string, destination: string, options?: CopyFileOptions): Promise<void>;
61
+ readlink(path: string, options?: FsOptions): Promise<string>;
62
+ symlink(target: string, path: string, options?: FsOptions): Promise<void>;
63
+ link(_existingPath: string, newPath: string, options?: FsOptions): Promise<void>;
64
+ private metadata;
65
+ chmod(path: string, permissions: number, options?: FsOptions): Promise<void>;
66
+ utimes(path: string, atimeMs: number, mtimeMs: number, options?: FsOptions): Promise<void>;
67
+ truncate(path: string, length?: number, options?: FsOptions): Promise<void>;
68
+ readStream(path: string, options?: ReadStreamOptions): ByteSource;
69
+ private bounded;
70
+ private streamToUpper;
71
+ writeStream(path: string, source: ByteSource, options?: WriteFileOptions): Promise<void>;
72
+ }
73
+ export declare function createOverlayFileSystem(options: OverlayFileSystemOptions): OverlayFileSystem;