@poe-platform/safe-fs 0.1.721 → 0.1.723
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/README.md +67 -21
- package/dist/safe-fs/bridge/filesystem.d.ts +4 -0
- package/dist/safe-fs/bridge/filesystem.js +49 -10
- package/dist/safe-fs/bridge/types.d.ts +6 -0
- package/dist/safe-fs/contracts/descriptor.d.ts +5 -0
- package/dist/safe-fs/contracts/filesystem.d.ts +15 -0
- package/dist/safe-fs/contracts/io.d.ts +3 -1
- package/dist/safe-fs/contracts/io.js +48 -16
- package/dist/safe-fs/core.d.ts +2 -1
- package/dist/safe-fs/core.js +1 -1
- package/dist/safe-fs/fs/capabilities.js +6 -3
- package/dist/safe-fs/fs/descriptor.js +9 -2
- package/dist/safe-fs/fs/devices/index.d.ts +1 -0
- package/dist/safe-fs/fs/devices/index.js +24 -6
- package/dist/safe-fs/fs/memory/index.d.ts +2 -0
- package/dist/safe-fs/fs/memory/index.js +95 -4
- package/dist/safe-fs/fs/memory/limits.js +6 -4
- package/dist/safe-fs/fs/mount/index.d.ts +4 -0
- package/dist/safe-fs/fs/mount/index.js +76 -12
- package/dist/safe-fs/fs/object-publication/index.d.ts +4 -0
- package/dist/safe-fs/fs/object-publication/index.js +9 -8
- package/dist/safe-fs/fs/overlay/index.d.ts +4 -2
- package/dist/safe-fs/fs/overlay/index.js +92 -36
- package/dist/safe-fs/fs/quota/index.js +83 -23
- package/dist/safe-fs/fs/readonly/index.js +5 -3
- package/dist/safe-fs/fs/real/index.d.ts +21 -1
- package/dist/safe-fs/fs/real/index.js +266 -22
- package/dist/safe-fs/fs/s3/filesystem.d.ts +8 -0
- package/dist/safe-fs/fs/s3/filesystem.js +59 -12
- package/dist/safe-fs/fs/s3/http/request.d.ts +1 -1
- package/dist/safe-fs/fs/s3/http/request.js +1 -1
- package/dist/safe-fs/fs/s3/http/transport.js +8 -6
- package/dist/safe-fs/fs/s3/http/xml.d.ts +4 -1
- package/dist/safe-fs/fs/s3/http/xml.js +6 -2
- package/dist/safe-fs/fs/s3/manifest-admission.d.ts +8 -0
- package/dist/safe-fs/fs/s3/manifest-admission.js +171 -0
- package/dist/safe-fs/fs/s3/namespace.js +13 -10
- package/dist/safe-fs/fs/scoped.d.ts +1 -0
- package/dist/safe-fs/fs/scoped.js +54 -35
- package/dist/safe-fs/fs/webdav/webdav.d.ts +4 -1
- package/dist/safe-fs/fs/webdav/webdav.js +101 -23
- package/dist/safe-fs/node/filesystem.d.ts +8 -1
- package/dist/safe-fs/node/filesystem.js +7 -1
- package/dist/safe-fs/node/host.d.ts +3 -0
- package/dist/safe-fs/node/host.js +78 -0
- package/dist/safe-fs/node/index.d.ts +2 -1
- package/dist/safe-fs/node/index.js +2 -1
- package/dist/safe-fs/platform/browser.d.ts +1 -0
- package/dist/safe-fs/platform/browser.js +1 -0
- package/dist/safe-fs/platform/node.d.ts +1 -0
- package/dist/safe-fs/platform/node.js +1 -0
- package/dist/safe-fs/python/emscripten.d.ts +1 -0
- package/dist/safe-fs/python/emscripten.js +5 -5
- package/dist/safe-fs/python/filesystem.js +5 -5
- package/dist/safe-fs/python/native.d.ts +1 -0
- package/dist/safe-fs/python/native.js +3 -2
- package/dist/safe-fs/xml.js +32 -21
- package/package.json +1 -1
|
@@ -8,9 +8,10 @@ import { compareIdentity } from "../mount/identity.js";
|
|
|
8
8
|
import { compareEntries, registerEntryView } from "../mount/comparison.js";
|
|
9
9
|
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
10
10
|
function snapshotStat(stat) {
|
|
11
|
-
const { type, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, opaqueIdentity, opaqueVersion, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
|
|
11
|
+
const { type, filesystemType, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, opaqueIdentity, opaqueVersion, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
|
|
12
12
|
return {
|
|
13
13
|
type, size, mode, mtimeMs, atimeMs, ctimeMs,
|
|
14
|
+
...(filesystemType === undefined ? {} : { filesystemType }),
|
|
14
15
|
...(allocatedBytes === undefined ? {} : { allocatedBytes }),
|
|
15
16
|
...(ioBlockSize === undefined ? {} : { ioBlockSize }),
|
|
16
17
|
...(preferredIoBlockSize === undefined ? {} : { preferredIoBlockSize }),
|
|
@@ -80,15 +81,14 @@ export class OverlayFileSystem {
|
|
|
80
81
|
const entry = await this.required(path, options);
|
|
81
82
|
return { filesystem: entry.backend, path: entry.path, readOnly: this.capabilities.readOnly === true };
|
|
82
83
|
}, false));
|
|
83
|
-
this.maxBufferBytes = options.maxBufferBytes ??
|
|
84
|
-
|
|
84
|
+
this.maxBufferBytes = options.maxBufferBytes ?? Infinity;
|
|
85
|
+
if (options.maxBufferBytes !== undefined)
|
|
86
|
+
integer(options.maxBufferBytes, "/");
|
|
85
87
|
const writable = !this.#upper.capabilities.readOnly && this.#upper.capabilities.atomicRename === true;
|
|
86
|
-
const readable = [this.#upper, this.#lower].map((backend) =>
|
|
88
|
+
const readable = [this.#upper, this.#lower].map((backend) => retainedReadCapabilities(backend).retainedRead);
|
|
87
89
|
const streamingRead = readable.every((capability) => capability === true) ? true
|
|
88
90
|
: readable.every((capability) => capability === false) ? false : undefined;
|
|
89
|
-
const
|
|
90
|
-
const retainedRead = retained.every((capability) => capability === true) ? true
|
|
91
|
-
: retained.every((capability) => capability === false) ? false : undefined;
|
|
91
|
+
const retainedRead = streamingRead;
|
|
92
92
|
const streamingWrite = writable && this.#upper.capabilities.streamingWrite === true
|
|
93
93
|
&& this.#upper.capabilities.streamingRead === true
|
|
94
94
|
&& typeof this.#upper.writeStream === "function" && typeof this.#upper.readStream === "function"
|
|
@@ -101,7 +101,7 @@ export class OverlayFileSystem {
|
|
|
101
101
|
const effectiveStreamingWrite = requireCapabilities(streamingWrite, mutation);
|
|
102
102
|
const semantics = Object.fromEntries([
|
|
103
103
|
...["read", "stat", "readdir", "realpath", "access"].map(capability => [capability,
|
|
104
|
-
requireCapabilities(upper[capability], this.#lower.capabilities[capability])]),
|
|
104
|
+
requireCapabilities(upper[capability], this.#lower.capabilities[capability], ...(capability === "read" ? readable : []))]),
|
|
105
105
|
["write", requireCapabilities(mutation, upper.write)],
|
|
106
106
|
["exclusiveCreate", requireCapabilities(mutation, upper.exclusiveCreate)],
|
|
107
107
|
["mkdir", requireCapabilities(mutation, upper.mkdir)],
|
|
@@ -120,7 +120,7 @@ export class OverlayFileSystem {
|
|
|
120
120
|
this.capabilities = Object.freeze({
|
|
121
121
|
...semantics,
|
|
122
122
|
open: false,
|
|
123
|
-
atomicFilePublication: false, atomicFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
|
|
123
|
+
atomicFilePublication: false, atomicFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
124
124
|
implicitDirectories: false,
|
|
125
125
|
readlink: upper.readlink === true && this.#lower.capabilities.readlink === true ? true
|
|
126
126
|
: upper.readlink === false && this.#lower.capabilities.readlink === false ? false : undefined,
|
|
@@ -169,7 +169,7 @@ export class OverlayFileSystem {
|
|
|
169
169
|
}
|
|
170
170
|
capabilitiesFor(path, options = {}) {
|
|
171
171
|
return this.run(options, async () => {
|
|
172
|
-
const location = await this.resolve(path, options,
|
|
172
|
+
const location = await this.resolve(path, options, options.create !== undefined || options.creation !== "exclusive", true);
|
|
173
173
|
const backend = location.entry?.backend ?? this.#upper;
|
|
174
174
|
const capabilities = await backend.capabilitiesFor?.(location.path, options) ?? backend.capabilities;
|
|
175
175
|
options.signal?.throwIfAborted();
|
|
@@ -188,9 +188,60 @@ export class OverlayFileSystem {
|
|
|
188
188
|
fail("EISDIR", path);
|
|
189
189
|
}
|
|
190
190
|
this.permission(entry, 4);
|
|
191
|
-
return
|
|
191
|
+
return this.pinRead(entry, options);
|
|
192
192
|
}, false);
|
|
193
193
|
}
|
|
194
|
+
async pinRead(entry, options) {
|
|
195
|
+
// A retained object, not the resolved pathname, carries read authority.
|
|
196
|
+
if (compareIdentity(entry.stat, entry.stat) !== "same")
|
|
197
|
+
fail("ENOTSUP", entry.path, "overlay reads require stable backend identity");
|
|
198
|
+
const ancestors = [];
|
|
199
|
+
let parent = dirname(entry.path);
|
|
200
|
+
while (true) {
|
|
201
|
+
const stat = await entry.backend.lstat(parent, options);
|
|
202
|
+
if (stat.type !== "directory" || compareIdentity(stat, stat) !== "same")
|
|
203
|
+
fail("ENOTSUP", entry.path, "overlay reads require stable directory ancestry");
|
|
204
|
+
ancestors.push({ backend: entry.backend, path: parent, stat });
|
|
205
|
+
if (parent === "/")
|
|
206
|
+
break;
|
|
207
|
+
parent = dirname(parent);
|
|
208
|
+
}
|
|
209
|
+
const handle = await openRetainedReadFile(entry.backend, entry.path, options);
|
|
210
|
+
try {
|
|
211
|
+
if (compareIdentity(entry.stat, await handle.stat(options)) !== "same")
|
|
212
|
+
fail("ENOTSUP", entry.path, "backend read object changed during admission");
|
|
213
|
+
for (const ancestor of ancestors) {
|
|
214
|
+
const stat = await entry.backend.lstat(ancestor.path, options);
|
|
215
|
+
if (stat.type !== "directory" || compareIdentity(ancestor.stat, stat) !== "same")
|
|
216
|
+
fail("ENOTSUP", entry.path, "backend read ancestry changed during admission");
|
|
217
|
+
}
|
|
218
|
+
const current = await this.required(entry.path, options);
|
|
219
|
+
if (current.backend !== entry.backend || compareIdentity(entry.stat, current.stat) !== "same")
|
|
220
|
+
fail("ENOTSUP", entry.path, "overlay read layer changed during admission");
|
|
221
|
+
options.signal?.throwIfAborted();
|
|
222
|
+
return handle;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
await finishCleanup(() => handle.close(), true);
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async *handleBytes(handle, options) {
|
|
230
|
+
let position = options.start ?? 0;
|
|
231
|
+
const end = options.endExclusive ?? Number.MAX_SAFE_INTEGER;
|
|
232
|
+
const chunkSize = options.chunkSize ?? 64 * 1024;
|
|
233
|
+
while (position < end) {
|
|
234
|
+
const size = Math.min(chunkSize, end - position);
|
|
235
|
+
const chunk = await handle.read(position, size, options);
|
|
236
|
+
options.signal?.throwIfAborted();
|
|
237
|
+
if (!(chunk instanceof Uint8Array) || chunk.byteLength > size)
|
|
238
|
+
fail("EIO", "", "backend returned invalid retained bytes");
|
|
239
|
+
if (!chunk.byteLength)
|
|
240
|
+
break;
|
|
241
|
+
position += chunk.byteLength;
|
|
242
|
+
yield new Uint8Array(chunk);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
194
245
|
writable(path) {
|
|
195
246
|
if (this.#upper.capabilities.readOnly)
|
|
196
247
|
fail("EROFS", path);
|
|
@@ -435,13 +486,16 @@ export class OverlayFileSystem {
|
|
|
435
486
|
const limit = Math.min(options.maxBytes ?? this.maxBufferBytes, this.maxBufferBytes);
|
|
436
487
|
if (entry.stat.size > limit)
|
|
437
488
|
fail("EFBIG", entry.path);
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
489
|
+
const handle = await this.pinRead(entry, options);
|
|
490
|
+
let failed = true;
|
|
491
|
+
try {
|
|
492
|
+
const result = await collectBytes(this.handleBytes(handle, options), { ...options, ...(limit === Infinity ? {} : { maxBytes: limit }) });
|
|
493
|
+
failed = false;
|
|
494
|
+
return result;
|
|
495
|
+
}
|
|
496
|
+
finally {
|
|
497
|
+
await finishCleanup(() => handle.close(), failed);
|
|
498
|
+
}
|
|
445
499
|
}
|
|
446
500
|
async cleanGarbage(strict) {
|
|
447
501
|
const failures = [];
|
|
@@ -501,8 +555,16 @@ export class OverlayFileSystem {
|
|
|
501
555
|
this.permission(entry, 4);
|
|
502
556
|
if (entry.stat.size > this.maxBufferBytes)
|
|
503
557
|
fail("EFBIG", entry.path);
|
|
504
|
-
const
|
|
505
|
-
|
|
558
|
+
const handle = await this.pinRead(entry, options);
|
|
559
|
+
let failed = true;
|
|
560
|
+
try {
|
|
561
|
+
const source = this.bounded(this.handleBytes(handle, options), entry.path, options);
|
|
562
|
+
await this.streamToUpper(destination, source, { ...options, mode: 0o600, flag: "wx" });
|
|
563
|
+
failed = false;
|
|
564
|
+
}
|
|
565
|
+
finally {
|
|
566
|
+
await finishCleanup(() => handle.close(), failed);
|
|
567
|
+
}
|
|
506
568
|
}
|
|
507
569
|
else
|
|
508
570
|
await this.#upper.writeFile(destination, await this.bytes(entry, options), { ...options, mode: 0o600, flag: "wx" });
|
|
@@ -918,7 +980,7 @@ export class OverlayFileSystem {
|
|
|
918
980
|
if (this.#upper.truncate)
|
|
919
981
|
await this.#upper.truncate(temporary, length, options);
|
|
920
982
|
else {
|
|
921
|
-
const previous = await this.#upper.readFile(temporary, { ...options, maxBytes: this.maxBufferBytes });
|
|
983
|
+
const previous = await this.#upper.readFile(temporary, { ...options, ...(this.maxBufferBytes === Infinity ? {} : { maxBytes: this.maxBufferBytes }) });
|
|
922
984
|
const bytes = new Uint8Array(length);
|
|
923
985
|
bytes.set(previous.subarray(0, length));
|
|
924
986
|
await this.#upper.writeFile(temporary, bytes, options);
|
|
@@ -939,27 +1001,21 @@ export class OverlayFileSystem {
|
|
|
939
1001
|
if (options.endExclusive < start)
|
|
940
1002
|
fail("EINVAL", path);
|
|
941
1003
|
}
|
|
942
|
-
const
|
|
1004
|
+
const handle = await this.run(options, async () => {
|
|
943
1005
|
const entry = await this.required(path, options);
|
|
944
1006
|
if (entry.stat.type !== "file")
|
|
945
1007
|
fail("EISDIR", path);
|
|
946
1008
|
this.permission(entry, 4);
|
|
947
|
-
return entry;
|
|
1009
|
+
return this.pinRead(entry, options);
|
|
948
1010
|
});
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
options.signal?.throwIfAborted();
|
|
954
|
-
return;
|
|
1011
|
+
let failed = true;
|
|
1012
|
+
try {
|
|
1013
|
+
yield* readBytes(this.handleBytes(handle, options), options.signal);
|
|
1014
|
+
failed = false;
|
|
955
1015
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
for (let offset = start; offset < end; offset += chunkSize) {
|
|
959
|
-
options.signal?.throwIfAborted();
|
|
960
|
-
yield bytes.slice(offset, Math.min(end, offset + chunkSize));
|
|
1016
|
+
finally {
|
|
1017
|
+
await finishCleanup(() => handle.close(), failed);
|
|
961
1018
|
}
|
|
962
|
-
options.signal?.throwIfAborted();
|
|
963
1019
|
}
|
|
964
1020
|
async *bounded(source, path, options) {
|
|
965
1021
|
let size = 0;
|
|
@@ -1007,7 +1063,7 @@ export class OverlayFileSystem {
|
|
|
1007
1063
|
});
|
|
1008
1064
|
return;
|
|
1009
1065
|
}
|
|
1010
|
-
const bytes = await collectBytes(source, { maxBytes: this.maxBufferBytes, ...(options.signal ? { signal: options.signal } : {}) });
|
|
1066
|
+
const bytes = await collectBytes(source, { ...(this.maxBufferBytes === Infinity ? {} : { maxBytes: this.maxBufferBytes }), ...(options.signal ? { signal: options.signal } : {}) });
|
|
1011
1067
|
await this.writeFile(path, bytes, options);
|
|
1012
1068
|
}
|
|
1013
1069
|
}
|
|
@@ -27,7 +27,9 @@ function namespaceMetadata(metadata, signal) {
|
|
|
27
27
|
abort();
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
-
async function scanUsedBytes(fs, limits, options,
|
|
30
|
+
async function scanUsedBytes(fs, limits, options, changeInput, retained = typeof changeInput !== "function" && changeInput?.retained === true) {
|
|
31
|
+
let change = typeof changeInput === "function" ? undefined : changeInput;
|
|
32
|
+
let prepare = typeof changeInput === "function" ? changeInput : undefined;
|
|
31
33
|
let total = 0;
|
|
32
34
|
let retainedTotal = 0n;
|
|
33
35
|
let shrinkCredited = false;
|
|
@@ -40,12 +42,30 @@ async function scanUsedBytes(fs, limits, options, change, retained = change?.ret
|
|
|
40
42
|
if (!directory)
|
|
41
43
|
break;
|
|
42
44
|
const signal = options?.signal;
|
|
43
|
-
const listing = fs.readdir(directory.path, { ...options, ...(signal ? { signal } : {}), maxEntries: remaining });
|
|
45
|
+
const listing = fs.readdir(directory.path, { ...options, ...(signal ? { signal } : {}), ...(remaining === Infinity ? {} : { maxEntries: remaining }) });
|
|
44
46
|
const entries = await (retained ? namespaceMetadata(listing, signal) : listing);
|
|
45
47
|
options?.signal?.throwIfAborted();
|
|
46
48
|
const count = entries.length;
|
|
47
49
|
admitDirectoryEntries(count, remaining, directory.path);
|
|
48
50
|
remaining -= count;
|
|
51
|
+
if (prepare) {
|
|
52
|
+
change = await prepare();
|
|
53
|
+
prepare = undefined;
|
|
54
|
+
}
|
|
55
|
+
if (change?.stat.type === "directory") {
|
|
56
|
+
const stat = await fs.stat(directory.path, options);
|
|
57
|
+
const comparable = completeIdentity(stat) && completeIdentity(change.stat);
|
|
58
|
+
const comparison = comparable
|
|
59
|
+
? stat.identityScope === change.stat.identityScope && stat.dev === change.stat.dev && stat.ino === change.stat.ino ? "same" : "distinct"
|
|
60
|
+
: fs.compareEntry ? await fs.compareEntry(change.path, fs, directory.path, options) : "unknown";
|
|
61
|
+
options?.signal?.throwIfAborted();
|
|
62
|
+
if (comparison !== "same" && comparison !== "distinct" && comparison !== "unknown")
|
|
63
|
+
throw new FsError("EIO", { syscall: "compareEntry", path: change.path });
|
|
64
|
+
if (comparison !== "distinct") {
|
|
65
|
+
total += Math.max(0, change.delta);
|
|
66
|
+
possibleAliases++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
49
69
|
let processed = 0;
|
|
50
70
|
for (const entry of entries) {
|
|
51
71
|
options?.signal?.throwIfAborted();
|
|
@@ -69,7 +89,7 @@ async function scanUsedBytes(fs, limits, options, change, retained = change?.ret
|
|
|
69
89
|
if (!Number.isSafeInteger(stat.size) || stat.size < 0)
|
|
70
90
|
throw new FsError("EIO", { syscall: "lstat", path, message: "invalid quota entry size" });
|
|
71
91
|
retainedTotal += BigInt(stat.size);
|
|
72
|
-
if (!change || stat.type !== "file")
|
|
92
|
+
if (!change || change.stat.type !== "file" || stat.type !== "file")
|
|
73
93
|
continue;
|
|
74
94
|
const known = completeIdentity(stat);
|
|
75
95
|
const same = known && change.stat.identityScope === stat.identityScope && change.stat.dev === stat.dev && change.stat.ino === stat.ino;
|
|
@@ -89,7 +109,7 @@ async function scanUsedBytes(fs, limits, options, change, retained = change?.ret
|
|
|
89
109
|
continue;
|
|
90
110
|
}
|
|
91
111
|
total += stat.size;
|
|
92
|
-
if (!change || stat.type !== "file")
|
|
112
|
+
if (!change || change.stat.type !== "file" || stat.type !== "file")
|
|
93
113
|
continue;
|
|
94
114
|
const scope = change.stat.identityScope;
|
|
95
115
|
const comparable = [scope, stat.identityScope].every(value => typeof value === "symbol" || typeof value === "object" && value !== null)
|
|
@@ -136,11 +156,11 @@ export function withFileSystemQuota(fs, options) {
|
|
|
136
156
|
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0)
|
|
137
157
|
throw new RangeError("maxBytes must be a nonnegative safe integer");
|
|
138
158
|
const scanLimits = {
|
|
139
|
-
maxScanEntries: options.maxScanEntries
|
|
140
|
-
maxScanDepth: options.maxScanDepth
|
|
159
|
+
maxScanEntries: options.maxScanEntries ?? Infinity,
|
|
160
|
+
maxScanDepth: options.maxScanDepth ?? Infinity,
|
|
141
161
|
};
|
|
142
|
-
for (const [name, value] of Object.entries(
|
|
143
|
-
if (!Number.isSafeInteger(value) || value < 0)
|
|
162
|
+
for (const [name, value] of Object.entries({ maxScanEntries: options.maxScanEntries, maxScanDepth: options.maxScanDepth })) {
|
|
163
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
|
|
144
164
|
throw new RangeError(`${name} must be a nonnegative safe integer`);
|
|
145
165
|
}
|
|
146
166
|
let queue = Promise.resolve();
|
|
@@ -169,6 +189,35 @@ export function withFileSystemQuota(fs, options) {
|
|
|
169
189
|
return typeof original === "function" ? original.bind(fs) : original;
|
|
170
190
|
},
|
|
171
191
|
});
|
|
192
|
+
const creationChange = async (path, delta, fsOptions) => {
|
|
193
|
+
// Resolve existing ancestors before comparing directory identities. A dangling
|
|
194
|
+
// final symlink cannot safely identify its publication directory here.
|
|
195
|
+
let target;
|
|
196
|
+
try {
|
|
197
|
+
target = fs.canonicalizeMissingTarget?.(path, fsOptions) ?? path;
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
201
|
+
throw new FileSystemQuotaError(options.maxBytes);
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const entry = fs.canonicalizeMissingTarget ? undefined : await fs.lstat(target, fsOptions);
|
|
206
|
+
if (entry?.type === "symlink")
|
|
207
|
+
throw new FsError("ENOTSUP", { syscall: "write", path, message: "quota cannot locate missing symlink referent" });
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
const boundary = target.lastIndexOf("/");
|
|
214
|
+
const parent = await fs.realpath(boundary <= 0 ? "/" : target.slice(0, boundary), fsOptions);
|
|
215
|
+
const stat = await fs.stat(parent, fsOptions);
|
|
216
|
+
fsOptions?.signal?.throwIfAborted();
|
|
217
|
+
if (stat.type !== "directory")
|
|
218
|
+
throw new FsError("ENOTDIR", { syscall: "write", path });
|
|
219
|
+
return { path: parent, stat, delta };
|
|
220
|
+
};
|
|
172
221
|
const assertDelta = async (path, nextBytes, fsOptions) => {
|
|
173
222
|
fsOptions?.signal?.throwIfAborted();
|
|
174
223
|
let current;
|
|
@@ -181,7 +230,9 @@ export function withFileSystemQuota(fs, options) {
|
|
|
181
230
|
}
|
|
182
231
|
const projected = current?.type === "file" && nextBytes > current.size
|
|
183
232
|
? await usedBytes(fs, scanLimits, fsOptions, { path, stat: current, delta: nextBytes - current.size })
|
|
184
|
-
:
|
|
233
|
+
: !current && nextBytes > 0
|
|
234
|
+
? await usedBytes(fs, scanLimits, fsOptions, () => creationChange(path, nextBytes, fsOptions))
|
|
235
|
+
: await usedBytes(fs, scanLimits, fsOptions) - (current?.type === "directory" ? 0 : current?.size ?? 0) + nextBytes;
|
|
185
236
|
fsOptions?.signal?.throwIfAborted();
|
|
186
237
|
if (projected > options.maxBytes)
|
|
187
238
|
throw new FileSystemQuotaError(options.maxBytes);
|
|
@@ -189,11 +240,11 @@ export function withFileSystemQuota(fs, options) {
|
|
|
189
240
|
const mutations = {
|
|
190
241
|
open(path, openOptions) {
|
|
191
242
|
return openFileDescriptor(path, openOptions, {
|
|
192
|
-
positionedRead: true, positionedWrite: true, truncate: true, synchronization: "storage",
|
|
243
|
+
noFollow: true, positionedRead: true, positionedWrite: true, truncate: true, synchronization: "storage",
|
|
193
244
|
}, admitted => mutate(async () => {
|
|
194
245
|
admitted.signal?.throwIfAborted();
|
|
195
246
|
const open = fs.open;
|
|
196
|
-
const capabilities = admitted.creation === "exclusive" ? fs.capabilities : await fs.capabilitiesFor?.(path, admitted) ?? fs.capabilities;
|
|
247
|
+
const capabilities = admitted.noFollow || admitted.creation === "exclusive" ? fs.capabilities : await fs.capabilitiesFor?.(path, admitted) ?? fs.capabilities;
|
|
197
248
|
admitted.signal?.throwIfAborted();
|
|
198
249
|
if (!open || capabilities.open === false)
|
|
199
250
|
throw new FsError("ENOTSUP", { syscall: "open", path });
|
|
@@ -205,8 +256,15 @@ export function withFileSystemQuota(fs, options) {
|
|
|
205
256
|
const conditional = descriptor.capabilities.publication === "conditional" && pinned !== undefined;
|
|
206
257
|
let publishedSize = pinned?.size ?? 0;
|
|
207
258
|
let reserved = 0;
|
|
259
|
+
let publicationAliases = 1;
|
|
260
|
+
if (conditional) {
|
|
261
|
+
const baseline = await usedBytes(fs, scanLimits, admitted);
|
|
262
|
+
publicationAliases = await usedBytes(fs, scanLimits, admitted, () => creationChange(path, 1, admitted)) - baseline;
|
|
263
|
+
if (!Number.isSafeInteger(publicationAliases) || publicationAliases < 1)
|
|
264
|
+
throw new FsError("ENOTSUP", { syscall: "open", path, message: "quota cannot determine publication aliases" });
|
|
265
|
+
}
|
|
208
266
|
const reserve = (size) => {
|
|
209
|
-
const next = Math.max(0, size - publishedSize);
|
|
267
|
+
const next = Math.max(0, size - publishedSize) * publicationAliases;
|
|
210
268
|
reservedBytes += next - reserved;
|
|
211
269
|
reserved = next;
|
|
212
270
|
};
|
|
@@ -231,13 +289,13 @@ export function withFileSystemQuota(fs, options) {
|
|
|
231
289
|
if (!Number.isSafeInteger(nextBytes) || nextBytes < 0)
|
|
232
290
|
throw new FsError("EFBIG", { syscall: "write", path });
|
|
233
291
|
const projected = conditional
|
|
234
|
-
? await usedBytes(fs, scanLimits, operationOptions) - reserved + Math.max(0, nextBytes - publishedSize)
|
|
292
|
+
? await usedBytes(fs, scanLimits, operationOptions) - reserved + Math.max(0, nextBytes - publishedSize) * publicationAliases
|
|
235
293
|
: await usedBytes(fs, scanLimits, operationOptions, { path, stat, delta: nextBytes - stat.size, retained: true });
|
|
236
294
|
operationOptions.signal?.throwIfAborted();
|
|
237
295
|
if (projected > options.maxBytes)
|
|
238
296
|
throw new FileSystemQuotaError(options.maxBytes);
|
|
239
297
|
if (conditional)
|
|
240
|
-
reserve(Math.max(nextBytes, publishedSize + reserved));
|
|
298
|
+
reserve(Math.max(nextBytes, publishedSize + reserved / publicationAliases));
|
|
241
299
|
};
|
|
242
300
|
const forwarded = forwardFileDescriptor(descriptor, (syscall, operationOptions, action) => syscall === "close" && !conditional ? action() : mutate(async () => {
|
|
243
301
|
try {
|
|
@@ -389,15 +447,17 @@ export function withFileSystemQuota(fs, options) {
|
|
|
389
447
|
await fs.appendFile(path, data, appendOptions);
|
|
390
448
|
});
|
|
391
449
|
},
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
renameOptions
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
450
|
+
rename(source, destination, renameOptions) {
|
|
451
|
+
return mutate(async () => {
|
|
452
|
+
if (renameOptions?.noReplace) {
|
|
453
|
+
renameOptions.signal?.throwIfAborted();
|
|
454
|
+
const capabilities = await fs.capabilitiesFor?.(destination, renameOptions) ?? fs.capabilities;
|
|
455
|
+
renameOptions.signal?.throwIfAborted();
|
|
456
|
+
if (capabilities.atomicRenameNoReplace !== true)
|
|
457
|
+
throw new FsError("ENOTSUP", { syscall: "rename", path: source, dest: destination });
|
|
458
|
+
}
|
|
459
|
+
await fs.rename(source, destination, renameOptions);
|
|
460
|
+
});
|
|
401
461
|
},
|
|
402
462
|
copyFile(source, destination, copyOptions) {
|
|
403
463
|
return mutate(async () => {
|
|
@@ -11,9 +11,10 @@ function readOnly(syscall, path, dest) {
|
|
|
11
11
|
throw new FsError("EROFS", { syscall, path, ...(dest === undefined ? {} : { dest }) });
|
|
12
12
|
}
|
|
13
13
|
function snapshotStat(stat) {
|
|
14
|
-
const { type, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, opaqueIdentity, opaqueVersion, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
|
|
14
|
+
const { type, filesystemType, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, opaqueIdentity, opaqueVersion, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
|
|
15
15
|
return {
|
|
16
16
|
type, size, mode, mtimeMs, atimeMs, ctimeMs,
|
|
17
|
+
...(filesystemType === undefined ? {} : { filesystemType }),
|
|
17
18
|
...(revision === undefined ? {} : { revision }),
|
|
18
19
|
...(allocatedBytes === undefined ? {} : { allocatedBytes }),
|
|
19
20
|
...(ioBlockSize === undefined ? {} : { ioBlockSize }),
|
|
@@ -72,9 +73,10 @@ export class ReadOnlyFileSystem {
|
|
|
72
73
|
|| options.creation === "ifMissing" || options.creation === "exclusive"))
|
|
73
74
|
readOnly("open", path);
|
|
74
75
|
return openFileDescriptor(path, options, {
|
|
75
|
-
positionedRead: true, positionedWrite: false, truncate: false, synchronization: "none",
|
|
76
|
+
noFollow: true, positionedRead: true, positionedWrite: false, truncate: false, synchronization: "none",
|
|
76
77
|
}, async (admitted) => {
|
|
77
|
-
const capabilities =
|
|
78
|
+
const capabilities = admitted.noFollow ? this.#filesystem.capabilities
|
|
79
|
+
: await this.#filesystem.capabilitiesFor?.(path, admitted) ?? this.#filesystem.capabilities;
|
|
78
80
|
if (!this.#filesystem.open || capabilities.open === false)
|
|
79
81
|
throw new FsError("ENOTSUP", { syscall: "open", path });
|
|
80
82
|
admitted.signal?.throwIfAborted();
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
|
|
2
|
-
import type { AppendFileOptions, ByteSource, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, OpenReadFileOptions, OpenResizeFileOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/index.js";
|
|
2
|
+
import type { AppendFileOptions, ByteSource, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent, OpenReadFileOptions, OpenResizeFileOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/index.js";
|
|
3
3
|
export interface RealFileSystemOptions {
|
|
4
4
|
/** An existing, absolute host directory. It is never created implicitly. */
|
|
5
5
|
readonly root: string;
|
|
6
|
+
/**
|
|
7
|
+
* Trusted host primitive for an atomic no-replace move (e.g. renameat2 with
|
|
8
|
+
* RENAME_NOREPLACE). Receives resolved absolute host paths. Must reject EEXIST
|
|
9
|
+
* for any existing entry, including dangling symlinks, without mutation.
|
|
10
|
+
* Never implement with an existence check, ordinary rename, or copy/delete.
|
|
11
|
+
* Unsupported filesystems must reject ENOTSUP; cross-device moves reject EXDEV.
|
|
12
|
+
*/
|
|
13
|
+
readonly renameNoReplace?: (source: string, destination: string, options: FsOptions) => Promise<void>;
|
|
6
14
|
}
|
|
7
15
|
/**
|
|
8
16
|
* Async byte filesystem for trusted POSIX hosts (Windows is unsupported).
|
|
@@ -42,6 +50,7 @@ export interface RealFileSystemOptions {
|
|
|
42
50
|
export declare class RealFileSystem implements FileSystem {
|
|
43
51
|
readonly capabilities: FileSystemCapabilities;
|
|
44
52
|
private readonly configuredRoot;
|
|
53
|
+
private readonly renameNoReplace;
|
|
45
54
|
private rootPromise;
|
|
46
55
|
constructor(options: RealFileSystemOptions | string);
|
|
47
56
|
private root;
|
|
@@ -50,6 +59,17 @@ export declare class RealFileSystem implements FileSystem {
|
|
|
50
59
|
private path;
|
|
51
60
|
private operation;
|
|
52
61
|
private protectRoot;
|
|
62
|
+
private stagingSnapshot;
|
|
63
|
+
private expectStaging;
|
|
64
|
+
private stagingMetadata;
|
|
65
|
+
createStagedFile(directoryPath: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
|
|
66
|
+
private stagingPaths;
|
|
67
|
+
private checkStaging;
|
|
68
|
+
publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
|
|
69
|
+
removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
|
|
70
|
+
writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
|
|
71
|
+
removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
|
|
72
|
+
prepareDirectory(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
|
|
53
73
|
open(path: string, options: OpenFileOptions): Promise<FileDescriptor>;
|
|
54
74
|
private protectTerminal;
|
|
55
75
|
readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
|