@poe-platform/safe-fs 0.1.251 → 0.1.252
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/safe-fs/config/memory.d.ts +1 -1
- package/dist/safe-fs/config/memory.js +2 -4
- package/dist/safe-fs/core.d.ts +1 -0
- package/dist/safe-fs/core.js +1 -0
- package/dist/safe-fs/fs/memory/index.d.ts +11 -2
- package/dist/safe-fs/fs/memory/index.js +236 -75
- package/dist/safe-fs/fs/memory/ledger.d.ts +20 -0
- package/dist/safe-fs/fs/memory/ledger.js +53 -0
- package/dist/safe-fs/fs/memory/limits.d.ts +8 -0
- package/dist/safe-fs/fs/memory/limits.js +19 -0
- package/dist/safe-fs/fs/scoped.d.ts +2 -0
- package/dist/safe-fs/fs/scoped.js +98 -0
- package/dist/safe-fs/index.d.ts +1 -0
- package/dist/safe-fs/index.js +1 -0
- package/package.json +1 -1
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { FileSystemFactory } from "../contracts/filesystem.js";
|
|
2
|
-
import {
|
|
2
|
+
import type { FileSystemAdapterDescriptor } from "../config.js";
|
|
3
3
|
export declare function createMemoryFileSystemAdapter(create: FileSystemFactory): FileSystemAdapterDescriptor;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { normalizeMemoryFileSystemLimits } from "../fs/memory/limits.js";
|
|
2
2
|
export function createMemoryFileSystemAdapter(create) {
|
|
3
3
|
return {
|
|
4
|
-
validateOptions
|
|
5
|
-
readConfigRecord(options, "memory option", []);
|
|
6
|
-
},
|
|
4
|
+
validateOptions: normalizeMemoryFileSystemLimits,
|
|
7
5
|
create
|
|
8
6
|
};
|
|
9
7
|
}
|
package/dist/safe-fs/core.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./fs/readonly/index.js";
|
|
|
8
8
|
export * from "./fs/mount/index.js";
|
|
9
9
|
export * from "./fs/overlay/index.js";
|
|
10
10
|
export * from "./fs/quota/index.js";
|
|
11
|
+
export { scopeFileSystem } from "./fs/scoped.js";
|
|
11
12
|
export * from "./fs/webdav/index.js";
|
|
12
13
|
export * from "./bridge/index.js";
|
|
13
14
|
export { compareEntries } from "./fs/mount/comparison.js";
|
package/dist/safe-fs/core.js
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./fs/readonly/index.js";
|
|
|
8
8
|
export * from "./fs/mount/index.js";
|
|
9
9
|
export * from "./fs/overlay/index.js";
|
|
10
10
|
export * from "./fs/quota/index.js";
|
|
11
|
+
export { scopeFileSystem } from "./fs/scoped.js";
|
|
11
12
|
export * from "./fs/webdav/index.js";
|
|
12
13
|
export * from "./bridge/index.js";
|
|
13
14
|
export { compareEntries } from "./fs/mount/comparison.js";
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileStat, FileSystem, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
|
+
import { type MemoryFileSystemOptions } from "./limits.js";
|
|
4
|
+
export { defaultMemoryFileSystemLimits, type MemoryFileSystemLimits, type MemoryFileSystemOptions } from "./limits.js";
|
|
3
5
|
export declare class MemoryFileSystem implements FileSystem {
|
|
4
6
|
readonly capabilities: Readonly<{
|
|
5
7
|
read: true;
|
|
@@ -37,12 +39,17 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
37
39
|
}>;
|
|
38
40
|
private readonly identityScope;
|
|
39
41
|
private nextInode;
|
|
42
|
+
private readonly ledger;
|
|
40
43
|
private readonly root;
|
|
41
|
-
constructor();
|
|
44
|
+
constructor(options?: MemoryFileSystemOptions);
|
|
42
45
|
compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<EntryComparison>;
|
|
43
46
|
canonicalizeMissingTarget(path: string, options?: FsOptions): string | undefined;
|
|
44
47
|
private metadata;
|
|
45
48
|
private directory;
|
|
49
|
+
private addNode;
|
|
50
|
+
private releaseNode;
|
|
51
|
+
private releaseReference;
|
|
52
|
+
private replaceData;
|
|
46
53
|
private fail;
|
|
47
54
|
private validatePath;
|
|
48
55
|
private permission;
|
|
@@ -56,7 +63,9 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
56
63
|
private snapshot;
|
|
57
64
|
private bytes;
|
|
58
65
|
private allocate;
|
|
66
|
+
private prepareWrite;
|
|
59
67
|
private openWrite;
|
|
68
|
+
private writeData;
|
|
60
69
|
private writeAt;
|
|
61
70
|
readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
|
|
62
71
|
writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
|
|
@@ -81,4 +90,4 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
81
90
|
readStream(path: string, options?: ReadStreamOptions): ByteSource;
|
|
82
91
|
writeStream(path: string, source: ByteSource, options?: WriteFileOptions): Promise<void>;
|
|
83
92
|
}
|
|
84
|
-
export declare function createMemoryFileSystem(): MemoryFileSystem;
|
|
93
|
+
export declare function createMemoryFileSystem(options?: MemoryFileSystemOptions | Readonly<Record<string, unknown>>): MemoryFileSystem;
|
|
@@ -4,6 +4,9 @@ import { getOwnedS3Entry } from "../s3/registry.js";
|
|
|
4
4
|
import { getOwnedWebDavEntry } from "../webdav/resource-id.js";
|
|
5
5
|
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
6
6
|
import { resolveMissingTarget } from "./missing-target.js";
|
|
7
|
+
import { MemoryAllocation, MemoryLedger } from "./ledger.js";
|
|
8
|
+
import { normalizeMemoryFileSystemLimits } from "./limits.js";
|
|
9
|
+
export { defaultMemoryFileSystemLimits } from "./limits.js";
|
|
7
10
|
const typeModes = { file: 0o100000, directory: 0o040000, symlink: 0o120000 };
|
|
8
11
|
const ownedStats = new WeakMap();
|
|
9
12
|
const ownedStores = new WeakMap();
|
|
@@ -70,8 +73,12 @@ export class MemoryFileSystem {
|
|
|
70
73
|
})(this);
|
|
71
74
|
identityScope = Symbol();
|
|
72
75
|
nextInode = 1;
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
ledger;
|
|
77
|
+
root;
|
|
78
|
+
constructor(options = {}) {
|
|
79
|
+
this.ledger = new MemoryLedger(normalizeMemoryFileSystemLimits(options));
|
|
80
|
+
this.ledger.reserve(0, 1, "mkdir", "/");
|
|
81
|
+
this.root = this.directory(0o755);
|
|
75
82
|
const root = this.root;
|
|
76
83
|
ownedStores.set(this, {
|
|
77
84
|
root,
|
|
@@ -105,13 +112,45 @@ export class MemoryFileSystem {
|
|
|
105
112
|
metadata(mode) {
|
|
106
113
|
const now = Date.now();
|
|
107
114
|
return {
|
|
108
|
-
mode, ino: this.nextInode++, nlink: 1,
|
|
115
|
+
mode, ino: this.nextInode++, nlink: 1, references: 0,
|
|
109
116
|
atimeMs: now, mtimeMs: now, ctimeMs: now, birthtimeMs: now,
|
|
110
117
|
};
|
|
111
118
|
}
|
|
112
119
|
directory(mode) {
|
|
113
120
|
return { ...this.metadata(typeModes.directory | mode), type: "directory", entries: new Map() };
|
|
114
121
|
}
|
|
122
|
+
addNode(parent, name, create, syscall, path, retainedBytes = 0) {
|
|
123
|
+
const bytes = name.length * 2 + retainedBytes;
|
|
124
|
+
this.ledger.reserve(bytes, 2, syscall, path);
|
|
125
|
+
try {
|
|
126
|
+
const node = create();
|
|
127
|
+
parent.entries.set(name, node);
|
|
128
|
+
this.changed(parent);
|
|
129
|
+
return node;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
this.ledger.release(bytes, 2);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
releaseNode(node) {
|
|
137
|
+
if (node.nlink !== 0 || node.references !== 0)
|
|
138
|
+
return;
|
|
139
|
+
this.ledger.release(node.type === "symlink" ? node.target.length * 2 : 0, 1);
|
|
140
|
+
if (node.type === "file")
|
|
141
|
+
node.allocation.release();
|
|
142
|
+
}
|
|
143
|
+
releaseReference(node, path) {
|
|
144
|
+
node.references--;
|
|
145
|
+
this.ledger.release(path.length * 2, 1);
|
|
146
|
+
this.releaseNode(node);
|
|
147
|
+
}
|
|
148
|
+
replaceData(node, allocation, length = allocation.data.byteLength) {
|
|
149
|
+
const previous = node.allocation;
|
|
150
|
+
node.data = allocation.data.subarray(0, length);
|
|
151
|
+
node.allocation = allocation;
|
|
152
|
+
previous.release();
|
|
153
|
+
}
|
|
115
154
|
fail(code, syscall, path, dest) {
|
|
116
155
|
throw new FsError(code, { syscall, path, ...(dest === undefined ? {} : { dest }) });
|
|
117
156
|
}
|
|
@@ -163,9 +202,8 @@ export class MemoryFileSystem {
|
|
|
163
202
|
let node = current.entries.get(component);
|
|
164
203
|
if (!node && options.createDirectories !== undefined) {
|
|
165
204
|
this.permission(current, 3, syscall, path);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
this.changed(current);
|
|
205
|
+
const mode = options.createDirectories;
|
|
206
|
+
node = this.addNode(current, component, () => this.directory(mode), syscall, path);
|
|
169
207
|
}
|
|
170
208
|
if (!node) {
|
|
171
209
|
if (options.allowMissing && pending.length === 0) {
|
|
@@ -222,20 +260,29 @@ export class MemoryFileSystem {
|
|
|
222
260
|
atimeMs: node.atimeMs, mtimeMs: node.mtimeMs, ctimeMs: node.ctimeMs, birthtimeMs: node.birthtimeMs,
|
|
223
261
|
};
|
|
224
262
|
}
|
|
225
|
-
bytes(data) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
263
|
+
bytes(data, syscall, path) {
|
|
264
|
+
const allocation = this.allocate(data.byteLength, syscall, path);
|
|
265
|
+
try {
|
|
266
|
+
allocation.data.set(data);
|
|
267
|
+
return allocation;
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
allocation.release();
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
229
273
|
}
|
|
230
274
|
allocate(length, syscall, path) {
|
|
275
|
+
this.ledger.fileSize(length, syscall, path);
|
|
276
|
+
this.ledger.reserve(length, 0, syscall, path);
|
|
231
277
|
try {
|
|
232
|
-
return new Uint8Array(length);
|
|
278
|
+
return new MemoryAllocation(new Uint8Array(length), this.ledger);
|
|
233
279
|
}
|
|
234
280
|
catch (cause) {
|
|
281
|
+
this.ledger.release(length, 0);
|
|
235
282
|
throw new FsError("EFBIG", { syscall, path, cause });
|
|
236
283
|
}
|
|
237
284
|
}
|
|
238
|
-
|
|
285
|
+
prepareWrite(path, options, syscall) {
|
|
239
286
|
const flag = options.flag ?? "w";
|
|
240
287
|
if (!["w", "wx", "a", "ax"].includes(flag))
|
|
241
288
|
this.fail("EINVAL", syscall, path);
|
|
@@ -248,13 +295,62 @@ export class MemoryFileSystem {
|
|
|
248
295
|
if (location.node.type !== "file")
|
|
249
296
|
this.fail("EISDIR", syscall, path);
|
|
250
297
|
this.permission(location.node, 2, syscall, path);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
this.permission(location.parent, 3, syscall, path);
|
|
301
|
+
}
|
|
302
|
+
return { location, mode, append: flag === "a" || flag === "ax" };
|
|
303
|
+
}
|
|
304
|
+
openWrite(path, options, syscall, target = this.prepareWrite(path, options, syscall)) {
|
|
305
|
+
const { location, mode } = target;
|
|
306
|
+
if (location.node)
|
|
251
307
|
return location.node;
|
|
308
|
+
return this.addNode(location.parent, location.name, () => {
|
|
309
|
+
const allocation = new MemoryAllocation(new Uint8Array(), this.ledger);
|
|
310
|
+
return { ...this.metadata(typeModes.file | mode), type: "file", data: allocation.data, allocation };
|
|
311
|
+
}, syscall, path);
|
|
312
|
+
}
|
|
313
|
+
writeData(path, data, options, syscall) {
|
|
314
|
+
if (!(data instanceof Uint8Array))
|
|
315
|
+
throw new TypeError("Memory files require Uint8Array data");
|
|
316
|
+
const target = this.prepareWrite(path, options, syscall);
|
|
317
|
+
const current = target.location.node;
|
|
318
|
+
const length = (target.append ? current?.data.byteLength ?? 0 : 0) + data.byteLength;
|
|
319
|
+
this.ledger.fileSize(length, syscall, path);
|
|
320
|
+
const growth = target.append && length > (current?.allocation.data.byteLength ?? 0) ? length : 0;
|
|
321
|
+
this.ledger.check(data.byteLength + growth + (current ? 0 : target.location.name.length * 2), current ? 0 : 2, syscall, path);
|
|
322
|
+
const copied = this.bytes(data, syscall, path);
|
|
323
|
+
let allocation = copied;
|
|
324
|
+
let transferred = false;
|
|
325
|
+
try {
|
|
326
|
+
if (!current) {
|
|
327
|
+
if (target.append && length > 0) {
|
|
328
|
+
const capacity = Math.min(Math.max(length, 64), this.ledger.limits.maxFileBytes, this.ledger.availableBytes - target.location.name.length * 2);
|
|
329
|
+
allocation = this.allocate(capacity, syscall, path);
|
|
330
|
+
allocation.data.set(copied.data);
|
|
331
|
+
}
|
|
332
|
+
const node = this.addNode(target.location.parent, target.location.name, () => ({
|
|
333
|
+
...this.metadata(typeModes.file | target.mode), type: "file",
|
|
334
|
+
data: allocation.data.subarray(0, length), allocation,
|
|
335
|
+
}), syscall, path);
|
|
336
|
+
transferred = true;
|
|
337
|
+
return node;
|
|
338
|
+
}
|
|
339
|
+
if (target.append)
|
|
340
|
+
this.writeAt(current, copied.data, current.data.byteLength, syscall, path);
|
|
341
|
+
else {
|
|
342
|
+
this.replaceData(current, copied);
|
|
343
|
+
transferred = true;
|
|
344
|
+
this.changed(current);
|
|
345
|
+
}
|
|
346
|
+
return current;
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
if (allocation !== copied && !transferred)
|
|
350
|
+
allocation.release();
|
|
351
|
+
if (!transferred || allocation !== copied)
|
|
352
|
+
copied.release();
|
|
252
353
|
}
|
|
253
|
-
this.permission(location.parent, 3, syscall, path);
|
|
254
|
-
const node = { ...this.metadata(typeModes.file | mode), type: "file", data: new Uint8Array() };
|
|
255
|
-
location.parent.entries.set(location.name, node);
|
|
256
|
-
this.changed(location.parent);
|
|
257
|
-
return node;
|
|
258
354
|
}
|
|
259
355
|
writeAt(node, data, position, syscall, path) {
|
|
260
356
|
if (data.byteLength === 0) {
|
|
@@ -262,21 +358,31 @@ export class MemoryFileSystem {
|
|
|
262
358
|
return;
|
|
263
359
|
}
|
|
264
360
|
const end = position + data.byteLength;
|
|
265
|
-
|
|
266
|
-
this.fail("EFBIG", syscall, path);
|
|
361
|
+
this.ledger.fileSize(end, syscall, path);
|
|
267
362
|
const length = Math.max(node.data.byteLength, end);
|
|
268
|
-
let
|
|
269
|
-
if (length
|
|
270
|
-
|
|
363
|
+
let allocation = node.allocation;
|
|
364
|
+
if (length > allocation.data.byteLength) {
|
|
365
|
+
this.ledger.check(length, 0, syscall, path);
|
|
366
|
+
const capacity = Math.min(Math.max(length, node.data.byteLength * 2, 64), this.ledger.limits.maxFileBytes, this.ledger.availableBytes);
|
|
367
|
+
allocation = this.allocate(capacity, syscall, path);
|
|
271
368
|
}
|
|
272
|
-
|
|
273
|
-
storage =
|
|
274
|
-
|
|
369
|
+
try {
|
|
370
|
+
const storage = allocation.data;
|
|
371
|
+
if (allocation !== node.allocation)
|
|
372
|
+
storage.set(node.data);
|
|
373
|
+
if (position > node.data.byteLength)
|
|
374
|
+
storage.fill(0, node.data.byteLength, position);
|
|
375
|
+
storage.set(data, position);
|
|
275
376
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
377
|
+
catch (error) {
|
|
378
|
+
if (allocation !== node.allocation)
|
|
379
|
+
allocation.release();
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
if (allocation !== node.allocation)
|
|
383
|
+
this.replaceData(node, allocation, length);
|
|
384
|
+
else
|
|
385
|
+
node.data = allocation.data.subarray(0, length);
|
|
280
386
|
this.changed(node);
|
|
281
387
|
}
|
|
282
388
|
async readFile(path, options = {}) {
|
|
@@ -292,20 +398,11 @@ export class MemoryFileSystem {
|
|
|
292
398
|
}
|
|
293
399
|
async writeFile(path, data, options = {}) {
|
|
294
400
|
options.signal?.throwIfAborted();
|
|
295
|
-
|
|
296
|
-
const node = this.openWrite(path, options, "writeFile");
|
|
297
|
-
if (options.flag === "a" || options.flag === "ax")
|
|
298
|
-
this.writeAt(node, copied, node.data.byteLength, "writeFile", path);
|
|
299
|
-
else {
|
|
300
|
-
node.data = copied;
|
|
301
|
-
this.changed(node);
|
|
302
|
-
}
|
|
401
|
+
this.writeData(path, data, options, "writeFile");
|
|
303
402
|
}
|
|
304
403
|
async appendFile(path, data, options = {}) {
|
|
305
404
|
options.signal?.throwIfAborted();
|
|
306
|
-
|
|
307
|
-
const node = this.openWrite(path, { ...options, flag: "a" }, "appendFile");
|
|
308
|
-
this.writeAt(node, copied, node.data.byteLength, "appendFile", path);
|
|
405
|
+
this.writeData(path, data, { ...options, flag: "a" }, "appendFile");
|
|
309
406
|
}
|
|
310
407
|
async stat(path, options = {}) {
|
|
311
408
|
options.signal?.throwIfAborted();
|
|
@@ -342,8 +439,7 @@ export class MemoryFileSystem {
|
|
|
342
439
|
if (location.node)
|
|
343
440
|
this.fail("EEXIST", "mkdir", path);
|
|
344
441
|
this.permission(location.parent, 3, "mkdir", path);
|
|
345
|
-
location.parent
|
|
346
|
-
this.changed(location.parent);
|
|
442
|
+
this.addNode(location.parent, location.name, () => this.directory(mode), "mkdir", path);
|
|
347
443
|
}
|
|
348
444
|
async rmdir(path, options = {}) {
|
|
349
445
|
options.signal?.throwIfAborted();
|
|
@@ -359,8 +455,10 @@ export class MemoryFileSystem {
|
|
|
359
455
|
if (node.entries.size > 0)
|
|
360
456
|
this.fail("ENOTEMPTY", "rmdir", path);
|
|
361
457
|
location.parent.entries.delete(location.name);
|
|
458
|
+
this.ledger.release(location.name.length * 2, 1);
|
|
362
459
|
node.nlink = 0;
|
|
363
460
|
node.ctimeMs = Date.now();
|
|
461
|
+
this.releaseNode(node);
|
|
364
462
|
this.changed(location.parent);
|
|
365
463
|
}
|
|
366
464
|
async rm(path, options = {}) {
|
|
@@ -392,9 +490,16 @@ export class MemoryFileSystem {
|
|
|
392
490
|
}
|
|
393
491
|
}
|
|
394
492
|
location.parent.entries.delete(location.name);
|
|
493
|
+
this.ledger.release(location.name.length * 2, 1);
|
|
395
494
|
for (const entry of removed) {
|
|
495
|
+
if (entry.type === "directory") {
|
|
496
|
+
for (const name of entry.entries.keys())
|
|
497
|
+
this.ledger.release(name.length * 2, 1);
|
|
498
|
+
entry.entries.clear();
|
|
499
|
+
}
|
|
396
500
|
entry.nlink--;
|
|
397
501
|
entry.ctimeMs = Date.now();
|
|
502
|
+
this.releaseNode(entry);
|
|
398
503
|
}
|
|
399
504
|
this.changed(location.parent);
|
|
400
505
|
}
|
|
@@ -423,11 +528,25 @@ export class MemoryFileSystem {
|
|
|
423
528
|
this.fail("EISDIR", "rename", source, destination);
|
|
424
529
|
if (target.node.type === "directory" && target.node.entries.size > 0)
|
|
425
530
|
this.fail("ENOTEMPTY", "rename", source, destination);
|
|
531
|
+
}
|
|
532
|
+
const nameGrowth = target.node ? 0 : (target.name.length - origin.name.length) * 2;
|
|
533
|
+
this.ledger.reserve(Math.max(0, nameGrowth), 0, "rename", source);
|
|
534
|
+
try {
|
|
535
|
+
target.parent.entries.set(target.name, node);
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
this.ledger.release(Math.max(0, nameGrowth), 0);
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
origin.parent.entries.delete(origin.name);
|
|
542
|
+
if (target.node) {
|
|
543
|
+
this.ledger.release(origin.name.length * 2, 1);
|
|
426
544
|
target.node.nlink--;
|
|
427
545
|
target.node.ctimeMs = Date.now();
|
|
546
|
+
this.releaseNode(target.node);
|
|
428
547
|
}
|
|
429
|
-
|
|
430
|
-
|
|
548
|
+
else if (nameGrowth < 0)
|
|
549
|
+
this.ledger.release(-nameGrowth, 0);
|
|
431
550
|
this.changed(origin.parent);
|
|
432
551
|
this.changed(target.parent);
|
|
433
552
|
node.ctimeMs = Date.now();
|
|
@@ -448,8 +567,7 @@ export class MemoryFileSystem {
|
|
|
448
567
|
this.fail("EEXIST", "copyFile", source, destination);
|
|
449
568
|
if (target.node === origin)
|
|
450
569
|
this.fail("EINVAL", "copyFile", source, destination);
|
|
451
|
-
const node = this.
|
|
452
|
-
node.data = new Uint8Array(origin.data);
|
|
570
|
+
const node = this.writeData(destination, origin.data, { mode: origin.mode & 0o7777, flag: options.exclusive ? "wx" : "w" }, "copyFile");
|
|
453
571
|
node.mode = origin.mode;
|
|
454
572
|
this.changed(node);
|
|
455
573
|
origin.atimeMs = Date.now();
|
|
@@ -484,8 +602,9 @@ export class MemoryFileSystem {
|
|
|
484
602
|
if (location.node)
|
|
485
603
|
this.fail("EEXIST", "symlink", path);
|
|
486
604
|
this.permission(location.parent, 3, "symlink", path);
|
|
487
|
-
location.parent
|
|
488
|
-
|
|
605
|
+
this.addNode(location.parent, location.name, () => ({
|
|
606
|
+
...this.metadata(typeModes.symlink | 0o777), type: "symlink", target,
|
|
607
|
+
}), "symlink", path, target.length * 2);
|
|
489
608
|
}
|
|
490
609
|
async link(existingPath, newPath, options = {}) {
|
|
491
610
|
options.signal?.throwIfAborted();
|
|
@@ -496,7 +615,14 @@ export class MemoryFileSystem {
|
|
|
496
615
|
if (target.node)
|
|
497
616
|
this.fail("EEXIST", "link", existingPath, newPath);
|
|
498
617
|
this.permission(target.parent, 3, "link", newPath);
|
|
499
|
-
|
|
618
|
+
this.ledger.reserve(target.name.length * 2, 1, "link", newPath);
|
|
619
|
+
try {
|
|
620
|
+
target.parent.entries.set(target.name, node);
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
this.ledger.release(target.name.length * 2, 1);
|
|
624
|
+
throw error;
|
|
625
|
+
}
|
|
500
626
|
node.nlink++;
|
|
501
627
|
node.ctimeMs = Date.now();
|
|
502
628
|
this.changed(target.parent);
|
|
@@ -523,8 +649,14 @@ export class MemoryFileSystem {
|
|
|
523
649
|
const node = this.file(path, "truncate");
|
|
524
650
|
this.permission(node, 2, "truncate", path);
|
|
525
651
|
const data = this.allocate(length, "truncate", path);
|
|
526
|
-
|
|
527
|
-
|
|
652
|
+
try {
|
|
653
|
+
data.data.set(node.data.subarray(0, length));
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
data.release();
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
this.replaceData(node, data);
|
|
528
660
|
this.changed(node);
|
|
529
661
|
}
|
|
530
662
|
async openReadFile(path, options = {}) {
|
|
@@ -535,7 +667,7 @@ export class MemoryFileSystem {
|
|
|
535
667
|
|| Object.getOwnPropertyDescriptor(this, "root")?.value !== owner.root)
|
|
536
668
|
unsupported();
|
|
537
669
|
for (const name of ["openReadFile", "readFile", "readStream", "stat", "lstat", "realpath", "access",
|
|
538
|
-
"file", "resolve", "permission", "validatePath", "fail", "snapshot", "integer"]) {
|
|
670
|
+
"file", "resolve", "permission", "validatePath", "fail", "snapshot", "integer", "releaseReference", "releaseNode"]) {
|
|
539
671
|
const descriptor = Object.getOwnPropertyDescriptor(this, name)
|
|
540
672
|
?? Object.getOwnPropertyDescriptor(MemoryFileSystem.prototype, name);
|
|
541
673
|
if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value)
|
|
@@ -545,8 +677,11 @@ export class MemoryFileSystem {
|
|
|
545
677
|
unsupported();
|
|
546
678
|
let node = this.file(path, "openReadFile");
|
|
547
679
|
this.permission(node, 4, "openReadFile", path);
|
|
680
|
+
this.ledger.reserve(path.length * 2, 1, "openReadFile", path);
|
|
681
|
+
node.references++;
|
|
548
682
|
const snapshot = this.snapshot.bind(this);
|
|
549
683
|
const integer = this.integer.bind(this);
|
|
684
|
+
const releaseReference = this.releaseReference.bind(this);
|
|
550
685
|
let closing;
|
|
551
686
|
const current = (signal, syscall) => {
|
|
552
687
|
signal?.throwIfAborted();
|
|
@@ -570,7 +705,10 @@ export class MemoryFileSystem {
|
|
|
570
705
|
return bytes;
|
|
571
706
|
},
|
|
572
707
|
close() {
|
|
573
|
-
node
|
|
708
|
+
if (node) {
|
|
709
|
+
releaseReference(node, path);
|
|
710
|
+
node = undefined;
|
|
711
|
+
}
|
|
574
712
|
closing ??= Promise.resolve();
|
|
575
713
|
return closing;
|
|
576
714
|
},
|
|
@@ -591,33 +729,55 @@ export class MemoryFileSystem {
|
|
|
591
729
|
}
|
|
592
730
|
const node = this.file(path, "readStream");
|
|
593
731
|
this.permission(node, 4, "readStream", path);
|
|
732
|
+
this.ledger.reserve(path.length * 2, 1, "readStream", path);
|
|
733
|
+
node.references++;
|
|
734
|
+
const allocation = node.allocation;
|
|
735
|
+
allocation.retain();
|
|
594
736
|
const data = node.data;
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
737
|
+
try {
|
|
738
|
+
const end = Math.min(options.endExclusive ?? data.byteLength, data.byteLength);
|
|
739
|
+
node.atimeMs = Date.now();
|
|
740
|
+
for (let offset = start; offset < end; offset += chunkSize) {
|
|
741
|
+
options.signal?.throwIfAborted();
|
|
742
|
+
yield data.slice(offset, Math.min(offset + chunkSize, end));
|
|
743
|
+
}
|
|
598
744
|
options.signal?.throwIfAborted();
|
|
599
|
-
yield data.slice(offset, Math.min(offset + chunkSize, end));
|
|
600
745
|
}
|
|
601
|
-
|
|
746
|
+
finally {
|
|
747
|
+
allocation.release();
|
|
748
|
+
this.releaseReference(node, path);
|
|
749
|
+
}
|
|
602
750
|
}
|
|
603
751
|
async writeStream(path, source, options = {}) {
|
|
604
752
|
options.signal?.throwIfAborted();
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
let
|
|
608
|
-
|
|
609
|
-
node
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
753
|
+
this.validatePath(path, "writeStream");
|
|
754
|
+
this.ledger.reserve(path.length * 2, 1, "writeStream", path);
|
|
755
|
+
let node;
|
|
756
|
+
try {
|
|
757
|
+
node = this.openWrite(path, options, "writeStream");
|
|
758
|
+
node.references++;
|
|
759
|
+
const append = options.flag === "a" || options.flag === "ax";
|
|
760
|
+
let position = 0;
|
|
761
|
+
if (!append) {
|
|
762
|
+
this.replaceData(node, new MemoryAllocation(new Uint8Array(), this.ledger));
|
|
763
|
+
this.changed(node);
|
|
764
|
+
}
|
|
765
|
+
for await (const chunk of source) {
|
|
766
|
+
options.signal?.throwIfAborted();
|
|
767
|
+
if (!(chunk instanceof Uint8Array))
|
|
768
|
+
throw new TypeError("Memory files require Uint8Array data");
|
|
769
|
+
this.writeAt(node, chunk, append ? node.data.byteLength : position, "writeStream", path);
|
|
770
|
+
if (!append)
|
|
771
|
+
position += chunk.byteLength;
|
|
772
|
+
}
|
|
613
773
|
options.signal?.throwIfAborted();
|
|
614
|
-
if (!(chunk instanceof Uint8Array))
|
|
615
|
-
throw new TypeError("Memory files require Uint8Array data");
|
|
616
|
-
this.writeAt(node, chunk, append ? node.data.byteLength : position, "writeStream", path);
|
|
617
|
-
if (!append)
|
|
618
|
-
position += chunk.byteLength;
|
|
619
774
|
}
|
|
620
|
-
|
|
775
|
+
finally {
|
|
776
|
+
if (node)
|
|
777
|
+
this.releaseReference(node, path);
|
|
778
|
+
else
|
|
779
|
+
this.ledger.release(path.length * 2, 1);
|
|
780
|
+
}
|
|
621
781
|
}
|
|
622
782
|
}
|
|
623
783
|
const memoryImplementation = Object.getOwnPropertyDescriptors(MemoryFileSystem.prototype);
|
|
@@ -629,7 +789,8 @@ function stockDescriptorWrite(filesystem) {
|
|
|
629
789
|
|| Object.getOwnPropertyDescriptor(filesystem, "capabilities")?.value !== owner.capabilities)
|
|
630
790
|
return false;
|
|
631
791
|
for (const name of ["writeStream", "writeFile", "appendFile", "access", "stat", "lstat", "realpath",
|
|
632
|
-
"openWrite", "
|
|
792
|
+
"openWrite", "prepareWrite", "writeData", "addNode", "replaceData", "releaseReference", "releaseNode",
|
|
793
|
+
"resolve", "permission", "validatePath", "mode", "bytes", "allocate", "changed", "metadata", "integer", "writeAt", "fail"]) {
|
|
633
794
|
const descriptor = Object.getOwnPropertyDescriptor(filesystem, name)
|
|
634
795
|
?? Object.getOwnPropertyDescriptor(MemoryFileSystem.prototype, name);
|
|
635
796
|
if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value)
|
|
@@ -637,6 +798,6 @@ function stockDescriptorWrite(filesystem) {
|
|
|
637
798
|
}
|
|
638
799
|
return true;
|
|
639
800
|
}
|
|
640
|
-
export function createMemoryFileSystem() {
|
|
641
|
-
return new MemoryFileSystem();
|
|
801
|
+
export function createMemoryFileSystem(options = {}) {
|
|
802
|
+
return new MemoryFileSystem(normalizeMemoryFileSystemLimits(options));
|
|
642
803
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MemoryFileSystemLimits } from "./limits.js";
|
|
2
|
+
export declare class MemoryLedger {
|
|
3
|
+
readonly limits: Readonly<MemoryFileSystemLimits>;
|
|
4
|
+
private retainedBytes;
|
|
5
|
+
private metadataUnits;
|
|
6
|
+
constructor(limits: Readonly<MemoryFileSystemLimits>);
|
|
7
|
+
get availableBytes(): number;
|
|
8
|
+
fileSize(length: number, syscall: string, path: string): void;
|
|
9
|
+
check(bytes: number, units: number, syscall: string, path: string): void;
|
|
10
|
+
reserve(bytes: number, units: number, syscall: string, path: string): void;
|
|
11
|
+
release(bytes: number, units: number): void;
|
|
12
|
+
}
|
|
13
|
+
export declare class MemoryAllocation {
|
|
14
|
+
readonly data: Uint8Array;
|
|
15
|
+
private readonly ledger;
|
|
16
|
+
private references;
|
|
17
|
+
constructor(data: Uint8Array, ledger: MemoryLedger);
|
|
18
|
+
retain(): void;
|
|
19
|
+
release(): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { FsError } from "../../contracts/errors.js";
|
|
2
|
+
export class MemoryLedger {
|
|
3
|
+
limits;
|
|
4
|
+
retainedBytes = 0;
|
|
5
|
+
metadataUnits = 0;
|
|
6
|
+
constructor(limits) {
|
|
7
|
+
this.limits = limits;
|
|
8
|
+
}
|
|
9
|
+
get availableBytes() {
|
|
10
|
+
return this.limits.maxRetainedBytes - this.retainedBytes;
|
|
11
|
+
}
|
|
12
|
+
fileSize(length, syscall, path) {
|
|
13
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > this.limits.maxFileBytes) {
|
|
14
|
+
throw new FsError("EFBIG", { syscall, path });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
check(bytes, units, syscall, path) {
|
|
18
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > this.availableBytes ||
|
|
19
|
+
!Number.isSafeInteger(units) || units < 0 || units > this.limits.maxMetadataUnits - this.metadataUnits) {
|
|
20
|
+
throw new FsError("ENOSPC", { syscall, path });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
reserve(bytes, units, syscall, path) {
|
|
24
|
+
this.check(bytes, units, syscall, path);
|
|
25
|
+
this.retainedBytes += bytes;
|
|
26
|
+
this.metadataUnits += units;
|
|
27
|
+
}
|
|
28
|
+
release(bytes, units) {
|
|
29
|
+
if (bytes < 0 || units < 0 || bytes > this.retainedBytes || units > this.metadataUnits) {
|
|
30
|
+
throw new Error("Invalid Memory filesystem reservation release");
|
|
31
|
+
}
|
|
32
|
+
this.retainedBytes -= bytes;
|
|
33
|
+
this.metadataUnits -= units;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class MemoryAllocation {
|
|
37
|
+
data;
|
|
38
|
+
ledger;
|
|
39
|
+
references = 1;
|
|
40
|
+
constructor(data, ledger) {
|
|
41
|
+
this.data = data;
|
|
42
|
+
this.ledger = ledger;
|
|
43
|
+
}
|
|
44
|
+
retain() {
|
|
45
|
+
this.references++;
|
|
46
|
+
}
|
|
47
|
+
release() {
|
|
48
|
+
if (this.references <= 0)
|
|
49
|
+
throw new Error("Memory allocation already released");
|
|
50
|
+
if (--this.references === 0)
|
|
51
|
+
this.ledger.release(this.data.byteLength, 0);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface MemoryFileSystemLimits {
|
|
2
|
+
readonly maxFileBytes: number;
|
|
3
|
+
readonly maxRetainedBytes: number;
|
|
4
|
+
readonly maxMetadataUnits: number;
|
|
5
|
+
}
|
|
6
|
+
export type MemoryFileSystemOptions = Partial<MemoryFileSystemLimits>;
|
|
7
|
+
export declare const defaultMemoryFileSystemLimits: Readonly<MemoryFileSystemLimits>;
|
|
8
|
+
export declare function normalizeMemoryFileSystemLimits(options: unknown): Readonly<MemoryFileSystemLimits>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readConfigRecord } from "../../config.js";
|
|
2
|
+
export const defaultMemoryFileSystemLimits = Object.freeze({
|
|
3
|
+
maxFileBytes: 16 * 1024 * 1024,
|
|
4
|
+
maxRetainedBytes: 64 * 1024 * 1024,
|
|
5
|
+
maxMetadataUnits: 10_000,
|
|
6
|
+
});
|
|
7
|
+
export function normalizeMemoryFileSystemLimits(options) {
|
|
8
|
+
const keys = ["maxFileBytes", "maxRetainedBytes", "maxMetadataUnits"];
|
|
9
|
+
const record = readConfigRecord(options, "memory option", keys);
|
|
10
|
+
const limits = { ...defaultMemoryFileSystemLimits };
|
|
11
|
+
for (const key of keys) {
|
|
12
|
+
const value = Object.hasOwn(record, key) ? record[key] : limits[key];
|
|
13
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < (key === "maxMetadataUnits" ? 1 : 0)) {
|
|
14
|
+
throw new RangeError(`${key} must be a ${key === "maxMetadataUnits" ? "positive" : "nonnegative"} safe integer`);
|
|
15
|
+
}
|
|
16
|
+
limits[key] = value;
|
|
17
|
+
}
|
|
18
|
+
return Object.freeze(limits);
|
|
19
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { finishCleanup } from "../contracts/cleanup.js";
|
|
2
|
+
import { registerEntryView } from "./mount/comparison.js";
|
|
3
|
+
const originals = new WeakMap();
|
|
4
|
+
const operations = new Set([
|
|
5
|
+
"access", "appendFile", "canonicalizeMissingTarget", "capabilitiesFor", "chmod", "compareEntry",
|
|
6
|
+
"copyFile", "link", "lstat", "mkdir", "openReadFile", "readFile", "readStream", "readdir",
|
|
7
|
+
"readlink", "realpath", "rename", "rm", "rmdir", "stat", "symlink", "truncate", "utimes",
|
|
8
|
+
"writeFile", "writeStream",
|
|
9
|
+
]);
|
|
10
|
+
export function scopeFileSystem(filesystem, charge, signal) {
|
|
11
|
+
const original = originals.get(filesystem) ?? filesystem;
|
|
12
|
+
const methods = new Map();
|
|
13
|
+
const assertOpen = (options) => {
|
|
14
|
+
signal.throwIfAborted();
|
|
15
|
+
options?.signal?.throwIfAborted();
|
|
16
|
+
};
|
|
17
|
+
const admit = (options) => {
|
|
18
|
+
assertOpen(options);
|
|
19
|
+
charge();
|
|
20
|
+
};
|
|
21
|
+
const wrapHandle = (handle) => ({
|
|
22
|
+
async stat(options) { admit(options); return handle.stat(options); },
|
|
23
|
+
async read(position, maxBytes, options) { admit(options); return handle.read(position, maxBytes, options); },
|
|
24
|
+
close: handle.close.bind(handle),
|
|
25
|
+
});
|
|
26
|
+
const wrapStream = (source, options) => ({
|
|
27
|
+
[Symbol.asyncIterator]() {
|
|
28
|
+
assertOpen(options);
|
|
29
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
30
|
+
let closing;
|
|
31
|
+
const close = (value) => closing ??= Promise.resolve().then(() => iterator.return ? iterator.return(value) : { done: true, value: undefined });
|
|
32
|
+
const advance = async (operation) => {
|
|
33
|
+
try {
|
|
34
|
+
assertOpen(options);
|
|
35
|
+
if (closing) {
|
|
36
|
+
await closing;
|
|
37
|
+
return { done: true, value: undefined };
|
|
38
|
+
}
|
|
39
|
+
const result = await operation();
|
|
40
|
+
assertOpen(options);
|
|
41
|
+
if (closing) {
|
|
42
|
+
await closing;
|
|
43
|
+
return { done: true, value: undefined };
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
await finishCleanup(close, true);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
next: () => advance(() => iterator.next()),
|
|
54
|
+
return: close,
|
|
55
|
+
...(iterator.throw ? { throw: (error) => advance(() => iterator.throw(error)) } : {}),
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const view = new Proxy(Object.create(original), {
|
|
60
|
+
set(_target, property, value) {
|
|
61
|
+
return Reflect.set(original, property, value, original);
|
|
62
|
+
},
|
|
63
|
+
get(_target, property) {
|
|
64
|
+
const method = Reflect.get(original, property, original);
|
|
65
|
+
if (typeof method !== "function")
|
|
66
|
+
return method;
|
|
67
|
+
const cached = methods.get(property);
|
|
68
|
+
if (cached?.original === method)
|
|
69
|
+
return cached.scoped;
|
|
70
|
+
const dispatch = (...args) => {
|
|
71
|
+
if (operations.has(property)) {
|
|
72
|
+
const options = args.at(-1);
|
|
73
|
+
admit(options && typeof options === "object" && "signal" in options ? options : undefined);
|
|
74
|
+
}
|
|
75
|
+
if (property === "compareEntry") {
|
|
76
|
+
const peer = args[1];
|
|
77
|
+
args[1] = originals.get(peer) ?? peer;
|
|
78
|
+
}
|
|
79
|
+
return Reflect.apply(method, original, args);
|
|
80
|
+
};
|
|
81
|
+
const scoped = property === "openReadFile"
|
|
82
|
+
? async (...args) => wrapHandle(await dispatch(...args))
|
|
83
|
+
: property === "readStream"
|
|
84
|
+
? (...args) => wrapStream(dispatch(...args), args[1])
|
|
85
|
+
: operations.has(property) && property !== "canonicalizeMissingTarget"
|
|
86
|
+
? async (...args) => dispatch(...args)
|
|
87
|
+
: dispatch;
|
|
88
|
+
methods.set(property, { original: method, scoped });
|
|
89
|
+
return scoped;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
originals.set(view, original);
|
|
93
|
+
registerEntryView(view, async (path, options) => {
|
|
94
|
+
assertOpen(options);
|
|
95
|
+
return { filesystem: original, path };
|
|
96
|
+
});
|
|
97
|
+
return view;
|
|
98
|
+
}
|
package/dist/safe-fs/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./fs/readonly/index.js";
|
|
|
9
9
|
export * from "./fs/mount/index.js";
|
|
10
10
|
export * from "./fs/overlay/index.js";
|
|
11
11
|
export * from "./fs/quota/index.js";
|
|
12
|
+
export { scopeFileSystem } from "./fs/scoped.js";
|
|
12
13
|
export * from "./node/index.js";
|
|
13
14
|
export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
|
|
14
15
|
export type { FileSystemConfig, FileSystemAdapterDescriptor, FileSystemAdapterRegistry } from "./config.js";
|
package/dist/safe-fs/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./fs/readonly/index.js";
|
|
|
8
8
|
export * from "./fs/mount/index.js";
|
|
9
9
|
export * from "./fs/overlay/index.js";
|
|
10
10
|
export * from "./fs/quota/index.js";
|
|
11
|
+
export { scopeFileSystem } from "./fs/scoped.js";
|
|
11
12
|
export * from "./node/index.js";
|
|
12
13
|
export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
|
|
13
14
|
export { createNodeFileSystemAdapterRegistry } from "./config.node.js";
|