@poe-platform/safe-fs 0.1.720 → 0.1.722
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 +64 -18
- package/dist/safe-fs/bridge/filesystem.d.ts +2 -0
- package/dist/safe-fs/bridge/filesystem.js +17 -7
- package/dist/safe-fs/bridge/types.d.ts +2 -0
- package/dist/safe-fs/contracts/descriptor.d.ts +5 -0
- package/dist/safe-fs/contracts/filesystem.d.ts +12 -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 +1 -0
- package/dist/safe-fs/fs/memory/index.js +75 -3
- 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 +74 -11
- package/dist/safe-fs/fs/object-publication/index.js +6 -6
- 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 +18 -16
- 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 +58 -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/namespace.js +12 -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 +30 -22
- package/dist/safe-fs/node/filesystem.d.ts +4 -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
|
}
|
|
@@ -40,7 +40,7 @@ async function scanUsedBytes(fs, limits, options, change, retained = change?.ret
|
|
|
40
40
|
if (!directory)
|
|
41
41
|
break;
|
|
42
42
|
const signal = options?.signal;
|
|
43
|
-
const listing = fs.readdir(directory.path, { ...options, ...(signal ? { signal } : {}), maxEntries: remaining });
|
|
43
|
+
const listing = fs.readdir(directory.path, { ...options, ...(signal ? { signal } : {}), ...(remaining === Infinity ? {} : { maxEntries: remaining }) });
|
|
44
44
|
const entries = await (retained ? namespaceMetadata(listing, signal) : listing);
|
|
45
45
|
options?.signal?.throwIfAborted();
|
|
46
46
|
const count = entries.length;
|
|
@@ -136,11 +136,11 @@ export function withFileSystemQuota(fs, options) {
|
|
|
136
136
|
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0)
|
|
137
137
|
throw new RangeError("maxBytes must be a nonnegative safe integer");
|
|
138
138
|
const scanLimits = {
|
|
139
|
-
maxScanEntries: options.maxScanEntries
|
|
140
|
-
maxScanDepth: options.maxScanDepth
|
|
139
|
+
maxScanEntries: options.maxScanEntries ?? Infinity,
|
|
140
|
+
maxScanDepth: options.maxScanDepth ?? Infinity,
|
|
141
141
|
};
|
|
142
|
-
for (const [name, value] of Object.entries(
|
|
143
|
-
if (!Number.isSafeInteger(value) || value < 0)
|
|
142
|
+
for (const [name, value] of Object.entries({ maxScanEntries: options.maxScanEntries, maxScanDepth: options.maxScanDepth })) {
|
|
143
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
|
|
144
144
|
throw new RangeError(`${name} must be a nonnegative safe integer`);
|
|
145
145
|
}
|
|
146
146
|
let queue = Promise.resolve();
|
|
@@ -189,11 +189,11 @@ export function withFileSystemQuota(fs, options) {
|
|
|
189
189
|
const mutations = {
|
|
190
190
|
open(path, openOptions) {
|
|
191
191
|
return openFileDescriptor(path, openOptions, {
|
|
192
|
-
positionedRead: true, positionedWrite: true, truncate: true, synchronization: "storage",
|
|
192
|
+
noFollow: true, positionedRead: true, positionedWrite: true, truncate: true, synchronization: "storage",
|
|
193
193
|
}, admitted => mutate(async () => {
|
|
194
194
|
admitted.signal?.throwIfAborted();
|
|
195
195
|
const open = fs.open;
|
|
196
|
-
const capabilities = admitted.creation === "exclusive" ? fs.capabilities : await fs.capabilitiesFor?.(path, admitted) ?? fs.capabilities;
|
|
196
|
+
const capabilities = admitted.noFollow || admitted.creation === "exclusive" ? fs.capabilities : await fs.capabilitiesFor?.(path, admitted) ?? fs.capabilities;
|
|
197
197
|
admitted.signal?.throwIfAborted();
|
|
198
198
|
if (!open || capabilities.open === false)
|
|
199
199
|
throw new FsError("ENOTSUP", { syscall: "open", path });
|
|
@@ -389,15 +389,17 @@ export function withFileSystemQuota(fs, options) {
|
|
|
389
389
|
await fs.appendFile(path, data, appendOptions);
|
|
390
390
|
});
|
|
391
391
|
},
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
renameOptions
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
392
|
+
rename(source, destination, renameOptions) {
|
|
393
|
+
return mutate(async () => {
|
|
394
|
+
if (renameOptions?.noReplace) {
|
|
395
|
+
renameOptions.signal?.throwIfAborted();
|
|
396
|
+
const capabilities = await fs.capabilitiesFor?.(destination, renameOptions) ?? fs.capabilities;
|
|
397
|
+
renameOptions.signal?.throwIfAborted();
|
|
398
|
+
if (capabilities.atomicRenameNoReplace !== true)
|
|
399
|
+
throw new FsError("ENOTSUP", { syscall: "rename", path: source, dest: destination });
|
|
400
|
+
}
|
|
401
|
+
await fs.rename(source, destination, renameOptions);
|
|
402
|
+
});
|
|
401
403
|
},
|
|
402
404
|
copyFile(source, destination, copyOptions) {
|
|
403
405
|
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>;
|