@poe-platform/safe-fs 0.1.807 → 0.1.808
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 +19 -0
- package/dist/safe-fs/contracts/filesystem.d.ts +5 -0
- package/dist/safe-fs/core.d.ts +2 -0
- package/dist/safe-fs/core.js +2 -0
- package/dist/safe-fs/fs/capabilities.js +2 -2
- package/dist/safe-fs/fs/devices/index.d.ts +1 -0
- package/dist/safe-fs/fs/devices/index.js +40 -2
- package/dist/safe-fs/fs/memory/atomic-view.d.ts +1 -0
- package/dist/safe-fs/fs/memory/index.d.ts +4 -1
- package/dist/safe-fs/fs/memory/index.js +56 -9
- package/dist/safe-fs/fs/mount/index.d.ts +3 -1
- package/dist/safe-fs/fs/mount/index.js +154 -3
- package/dist/safe-fs/fs/overlay/index.d.ts +2 -0
- package/dist/safe-fs/fs/overlay/index.js +167 -59
- package/dist/safe-fs/fs/overlay/memory-publication.d.ts +1 -0
- package/dist/safe-fs/fs/overlay/memory-publication.js +5 -0
- package/dist/safe-fs/fs/quota/index.js +9 -4
- package/dist/safe-fs/fs/read-file-stream.d.ts +7 -0
- package/dist/safe-fs/fs/read-file-stream.js +103 -0
- package/dist/safe-fs/fs/readonly/index.d.ts +1 -0
- package/dist/safe-fs/fs/readonly/index.js +10 -0
- package/dist/safe-fs/fs/real/index.d.ts +1 -0
- package/dist/safe-fs/fs/real/index.js +27 -0
- package/dist/safe-fs/fs/s3/filesystem.d.ts +2 -1
- package/dist/safe-fs/fs/s3/filesystem.js +55 -16
- package/dist/safe-fs/fs/scoped.js +10 -4
- package/dist/safe-fs/index.d.ts +1 -0
- package/dist/safe-fs/index.js +1 -0
- package/dist/safe-fs/runtime-core.d.ts +1 -0
- package/dist/safe-fs/runtime-core.js +1 -0
- package/dist/safe-fs/storage.d.ts +49 -0
- package/dist/safe-fs/storage.js +265 -0
- package/dist/safe-fs/testing/object-io-metrics.js +1 -0
- package/package.json +9 -6
|
@@ -95,8 +95,8 @@ export class OverlayFileSystem {
|
|
|
95
95
|
: readable.every((capability) => capability === false) ? false : undefined;
|
|
96
96
|
const retainedRead = streamingRead;
|
|
97
97
|
const streamingWrite = writable && this.#upper.capabilities.streamingWrite === true
|
|
98
|
-
&& this.#upper.
|
|
99
|
-
&& typeof this.#upper.writeStream === "function"
|
|
98
|
+
&& retainedReadCapabilities(this.#upper).retainedRead === true
|
|
99
|
+
&& typeof this.#upper.writeStream === "function"
|
|
100
100
|
? readable.every((capability) => capability === true) ? true : undefined : false;
|
|
101
101
|
const append = !writable || this.#upper.capabilities.append === false ? false
|
|
102
102
|
: this.#upper.capabilities.append === true ? true : undefined;
|
|
@@ -117,8 +117,8 @@ export class OverlayFileSystem {
|
|
|
117
117
|
["rename", requireCapabilities(mutation, upper.rename, upper.write)],
|
|
118
118
|
["copy", requireCapabilities(mutation, upper.write)],
|
|
119
119
|
["exclusiveCopy", requireCapabilities(mutation, upper.exclusiveCreate)],
|
|
120
|
-
["truncate", requireCapabilities(mutation,
|
|
121
|
-
["streamingAppend", requireCapabilities(mutation, streamingWrite
|
|
120
|
+
["truncate", requireCapabilities(mutation, streamingWrite)],
|
|
121
|
+
["streamingAppend", requireCapabilities(mutation, streamingWrite)],
|
|
122
122
|
["randomAccessWrite", requireCapabilities(mutation, upper.randomAccessWrite)],
|
|
123
123
|
["explicitDirectories", requireCapabilities(upper.explicitDirectories, this.#lower.capabilities.explicitDirectories)],
|
|
124
124
|
].filter(([, value]) => value !== undefined));
|
|
@@ -578,7 +578,7 @@ export class OverlayFileSystem {
|
|
|
578
578
|
fail("ENOTSUP", path, "upper cannot preserve timestamps during copy-up");
|
|
579
579
|
await this.#upper.utimes(path, stat.atimeMs, stat.mtimeMs, options);
|
|
580
580
|
}
|
|
581
|
-
async clone(entry, destination, options
|
|
581
|
+
async clone(entry, destination, options) {
|
|
582
582
|
if (entry.stat.type !== "directory" && ((entry.stat.nlink ?? 1) > 1
|
|
583
583
|
|| (entry.backend.capabilities.hardlinks === true && entry.stat.nlink === undefined))) {
|
|
584
584
|
fail("ENOTSUP", entry.path, "copy-up cannot preserve hardlink identity");
|
|
@@ -591,23 +591,14 @@ export class OverlayFileSystem {
|
|
|
591
591
|
}
|
|
592
592
|
if (entry.stat.type === "directory")
|
|
593
593
|
await this.#upper.mkdir(destination, { ...options, mode: entry.stat.mode & 0o7777 });
|
|
594
|
-
else
|
|
594
|
+
else {
|
|
595
|
+
if (this.#upper.capabilities.streamingWrite !== true || !this.#upper.writeStream)
|
|
596
|
+
fail("ENOTSUP", destination, "copy-up requires a streaming upper");
|
|
595
597
|
this.permission(entry, 4);
|
|
596
598
|
if (entry.stat.size > this.maxBufferBytes)
|
|
597
599
|
fail("EFBIG", entry.path);
|
|
598
|
-
|
|
599
|
-
let failed = true;
|
|
600
|
-
try {
|
|
601
|
-
const source = this.bounded(this.handleBytes(handle, options), entry.path, options);
|
|
602
|
-
await this.streamToUpper(destination, source, { ...options, mode: 0o600, flag: "wx" });
|
|
603
|
-
failed = false;
|
|
604
|
-
}
|
|
605
|
-
finally {
|
|
606
|
-
await finishCleanup(() => handle.close(), failed);
|
|
607
|
-
}
|
|
600
|
+
await this.streamToUpper(destination, this.bounded(this.entryBytes(entry, options), entry.path, options), { ...options, mode: 0o600, flag: "wx" });
|
|
608
601
|
}
|
|
609
|
-
else
|
|
610
|
-
await this.#upper.writeFile(destination, await this.bytes(entry, options), { ...options, mode: 0o600, flag: "wx" });
|
|
611
602
|
await this.preserve(destination, entry.stat, options);
|
|
612
603
|
}
|
|
613
604
|
rememberLink(entry, destination) {
|
|
@@ -686,11 +677,11 @@ export class OverlayFileSystem {
|
|
|
686
677
|
}
|
|
687
678
|
return location;
|
|
688
679
|
}
|
|
689
|
-
async replace(location, options, operation,
|
|
680
|
+
async replace(location, options, operation, commitGuard) {
|
|
690
681
|
await this.parent(location.path, options);
|
|
691
682
|
await this.staged(options, async (temporary) => {
|
|
692
683
|
if (location.entry)
|
|
693
|
-
await this.clone(location.entry, temporary, options
|
|
684
|
+
await this.clone(location.entry, temporary, options);
|
|
694
685
|
await operation(temporary);
|
|
695
686
|
options.signal?.throwIfAborted();
|
|
696
687
|
commitGuard?.();
|
|
@@ -769,6 +760,72 @@ export class OverlayFileSystem {
|
|
|
769
760
|
const limit = directoryEntryLimit(options, path);
|
|
770
761
|
return this.run(options, async () => this.listing(await this.required(path, options), options, limit), false);
|
|
771
762
|
}
|
|
763
|
+
async *iterateDirectory(path, options = {}) {
|
|
764
|
+
const directory = await this.run(options, async () => {
|
|
765
|
+
const entry = await this.required(path, options);
|
|
766
|
+
if (entry.stat.type !== "directory")
|
|
767
|
+
fail("ENOTDIR", path);
|
|
768
|
+
this.permission(entry, 4);
|
|
769
|
+
return entry.path;
|
|
770
|
+
}, false);
|
|
771
|
+
const admit = async () => {
|
|
772
|
+
const current = await this.required(path, options);
|
|
773
|
+
if (current.path !== directory || current.stat.type !== "directory")
|
|
774
|
+
fail("EBUSY", path);
|
|
775
|
+
this.permission(current, 4);
|
|
776
|
+
};
|
|
777
|
+
for (const backend of [this.#upper, this.#lower]) {
|
|
778
|
+
const iterator = await this.run(options, async () => {
|
|
779
|
+
await admit();
|
|
780
|
+
if (backend === this.#lower && (!await this.lowerVisible(directory, options) || this.opaque.has(directory)))
|
|
781
|
+
return undefined;
|
|
782
|
+
if ((await this.maybeStat(backend, directory, options))?.type !== "directory")
|
|
783
|
+
return undefined;
|
|
784
|
+
if (!backend.iterateDirectory)
|
|
785
|
+
fail("ENOTSUP", directory);
|
|
786
|
+
return backend.iterateDirectory(directory, options)[Symbol.asyncIterator]();
|
|
787
|
+
}, false);
|
|
788
|
+
if (!iterator)
|
|
789
|
+
continue;
|
|
790
|
+
let complete = false, failed = false;
|
|
791
|
+
try {
|
|
792
|
+
while (true) {
|
|
793
|
+
const result = await this.run(options, async () => {
|
|
794
|
+
await admit();
|
|
795
|
+
if (backend === this.#lower && (!await this.lowerVisible(directory, options) || this.opaque.has(directory)))
|
|
796
|
+
fail("EBUSY", path);
|
|
797
|
+
const next = await iterator.next();
|
|
798
|
+
options.signal?.throwIfAborted();
|
|
799
|
+
if (next.done)
|
|
800
|
+
return { done: true };
|
|
801
|
+
const name = next.value.name;
|
|
802
|
+
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0"))
|
|
803
|
+
fail("EIO", directory, "invalid backend directory entry");
|
|
804
|
+
const child = await this.lookup(directory === "/" ? `/${name}` : `${directory}/${name}`, options);
|
|
805
|
+
options.signal?.throwIfAborted();
|
|
806
|
+
// A lower name shadowed by upper storage is skipped by lookup,
|
|
807
|
+
// without retaining a directory-sized set of previously seen names.
|
|
808
|
+
return { done: false, entry: child?.backend === backend ? { name, type: child.stat.type } : undefined };
|
|
809
|
+
}, false);
|
|
810
|
+
if (result.done) {
|
|
811
|
+
complete = true;
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
if (result.entry)
|
|
815
|
+
yield result.entry;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
catch (error) {
|
|
819
|
+
failed = true;
|
|
820
|
+
throw error;
|
|
821
|
+
}
|
|
822
|
+
finally {
|
|
823
|
+
if (!complete)
|
|
824
|
+
await finishCleanup(() => iterator.return?.(), failed);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
options.signal?.throwIfAborted();
|
|
828
|
+
}
|
|
772
829
|
async realpath(path, options = {}) {
|
|
773
830
|
return this.run(options, async () => (await this.required(path, options)).path, false);
|
|
774
831
|
}
|
|
@@ -983,9 +1040,10 @@ export class OverlayFileSystem {
|
|
|
983
1040
|
}
|
|
984
1041
|
}
|
|
985
1042
|
await this.cleanGarbage(false);
|
|
986
|
-
|
|
1043
|
+
if (original.stat.size > this.maxBufferBytes)
|
|
1044
|
+
fail("EFBIG", source);
|
|
987
1045
|
await this.replace(target, options, async (temporary) => {
|
|
988
|
-
await this
|
|
1046
|
+
await this.streamToUpper(temporary, this.entryBytes(original, options), { ...options, mode: original.stat.mode & 0o7777, flag: "w" });
|
|
989
1047
|
if (this.#upper.chmod)
|
|
990
1048
|
await this.#upper.chmod(temporary, original.stat.mode & 0o7777, options);
|
|
991
1049
|
else if (((await this.#upper.lstat(temporary, options)).mode & 0o7777) !== (original.stat.mode & 0o7777))
|
|
@@ -1057,7 +1115,7 @@ export class OverlayFileSystem {
|
|
|
1057
1115
|
await this.#upper.chmod(entry.path, permissions, controls);
|
|
1058
1116
|
}
|
|
1059
1117
|
else {
|
|
1060
|
-
await this.replace({ path: entry.path, entry }, controls, temporary => this.#upper.chmod(temporary, permissions, controls),
|
|
1118
|
+
await this.replace({ path: entry.path, entry }, controls, temporary => this.#upper.chmod(temporary, permissions, controls), guard);
|
|
1061
1119
|
}
|
|
1062
1120
|
});
|
|
1063
1121
|
}
|
|
@@ -1079,15 +1137,30 @@ export class OverlayFileSystem {
|
|
|
1079
1137
|
if (entry.stat.type !== "file")
|
|
1080
1138
|
fail("EISDIR", path);
|
|
1081
1139
|
this.permission(entry, 2);
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1140
|
+
if ((entry.stat.nlink ?? 1) > 1 || (entry.backend.capabilities.hardlinks === true && entry.stat.nlink === undefined)) {
|
|
1141
|
+
fail("ENOTSUP", path, "copy-up cannot preserve hardlink identity");
|
|
1142
|
+
}
|
|
1143
|
+
await this.parent(entry.path, options);
|
|
1144
|
+
await this.staged(options, async (temporary) => {
|
|
1145
|
+
const prefix = this.entryBytes(entry, { ...options, endExclusive: length });
|
|
1146
|
+
const source = async function* () {
|
|
1147
|
+
let written = 0;
|
|
1148
|
+
for await (const chunk of prefix) {
|
|
1149
|
+
written += chunk.byteLength;
|
|
1150
|
+
yield chunk;
|
|
1151
|
+
}
|
|
1152
|
+
while (written < length) {
|
|
1153
|
+
options.signal?.throwIfAborted();
|
|
1154
|
+
const zero = new Uint8Array(Math.min(64 * 1024, length - written));
|
|
1155
|
+
written += zero.byteLength;
|
|
1156
|
+
yield zero;
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
await this.streamToUpper(temporary, source(), { ...options, mode: 0o600, flag: "wx" });
|
|
1160
|
+
await this.preserve(temporary, entry.stat, options);
|
|
1161
|
+
await this.#upper.utimes(temporary, entry.stat.atimeMs, Date.now(), options);
|
|
1162
|
+
options.signal?.throwIfAborted();
|
|
1163
|
+
await this.#upper.rename(temporary, entry.path, options);
|
|
1091
1164
|
});
|
|
1092
1165
|
});
|
|
1093
1166
|
}
|
|
@@ -1130,7 +1203,7 @@ export class OverlayFileSystem {
|
|
|
1130
1203
|
}
|
|
1131
1204
|
}
|
|
1132
1205
|
async streamToUpper(path, source, options) {
|
|
1133
|
-
if (!this.#upper.writeStream)
|
|
1206
|
+
if (!this.#upper.writeStream || this.#upper.capabilities.streamingWrite !== true)
|
|
1134
1207
|
fail("ENOTSUP", path);
|
|
1135
1208
|
const input = readBytes(source, options.signal);
|
|
1136
1209
|
let failed = false;
|
|
@@ -1145,39 +1218,74 @@ export class OverlayFileSystem {
|
|
|
1145
1218
|
await finishCleanup(() => input.return(undefined), failed);
|
|
1146
1219
|
}
|
|
1147
1220
|
}
|
|
1221
|
+
async *entryBytes(entry, options) {
|
|
1222
|
+
this.permission(entry, 4);
|
|
1223
|
+
const handle = this.activeStages.has(dirname(entry.path)) && entry.backend === this.#upper
|
|
1224
|
+
? await openRetainedReadFile(this.#upper, entry.path, options) : await this.pinRead(entry, options);
|
|
1225
|
+
let failed = true;
|
|
1226
|
+
try {
|
|
1227
|
+
if (compareIdentity(await handle.stat(options), entry.stat) !== "same")
|
|
1228
|
+
fail("EAGAIN", entry.path);
|
|
1229
|
+
yield* this.handleBytes(handle, options);
|
|
1230
|
+
failed = false;
|
|
1231
|
+
}
|
|
1232
|
+
finally {
|
|
1233
|
+
await finishCleanup(() => handle.close(), failed);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1148
1236
|
async writeStream(path, source, options = {}) {
|
|
1149
1237
|
options.signal?.throwIfAborted();
|
|
1150
1238
|
this.writeOptions(path, options);
|
|
1239
|
+
if (this.capabilities.streamingWrite === false)
|
|
1240
|
+
fail("ENOTSUP", path, "streaming requires retained reads and a streaming upper");
|
|
1151
1241
|
await this.run(options, () => this.writeLocation(path, options));
|
|
1152
|
-
|
|
1153
|
-
await this.
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1242
|
+
await this.staged(options, async (incoming) => {
|
|
1243
|
+
await this.streamToUpper(incoming, this.bounded(source, path, options), { ...options, flag: "wx", mode: 0o600 });
|
|
1244
|
+
const incomingEntry = { path: incoming, backend: this.#upper, stat: await this.#upper.lstat(incoming, options) };
|
|
1245
|
+
await this.run(options, async () => {
|
|
1246
|
+
const location = await this.writeLocation(path, options);
|
|
1247
|
+
const append = options.flag === "a" || options.flag === "ax";
|
|
1248
|
+
if (append && (location.entry?.stat.size ?? 0) + incomingEntry.stat.size > this.maxBufferBytes)
|
|
1249
|
+
fail("EFBIG", path);
|
|
1250
|
+
if (this.publication.supported() && location.entry?.backend === this.#upper) {
|
|
1251
|
+
const parent = await this.required(dirname(location.path), options);
|
|
1252
|
+
const controls = { ...options, parent: this.publication.stat(parent.path, parent.stat),
|
|
1253
|
+
expected: this.publication.stat(location.path, location.entry.stat) };
|
|
1254
|
+
const guard = this.publication.prepareWrite(location.path, controls);
|
|
1255
|
+
if (append) {
|
|
1256
|
+
await this.staged(options, async (temporary) => {
|
|
1257
|
+
const previous = this.entryBytes(location.entry, options);
|
|
1258
|
+
const added = this.entryBytes(incomingEntry, options);
|
|
1259
|
+
await this.streamToUpper(temporary, (async function* () { yield* previous; yield* added; })(), { ...options, flag: "wx", mode: 0o600 });
|
|
1260
|
+
this.publication.replaceContents(temporary, location.path, guard);
|
|
1261
|
+
});
|
|
1169
1262
|
}
|
|
1170
|
-
else
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1263
|
+
else
|
|
1264
|
+
this.publication.replaceContents(incoming, location.path, guard);
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
if (location.entry && ((location.entry.stat.nlink ?? 1) > 1
|
|
1268
|
+
|| (location.entry.backend.capabilities.hardlinks === true && location.entry.stat.nlink === undefined))) {
|
|
1269
|
+
fail("ENOTSUP", path, "copy-up cannot preserve hardlink identity");
|
|
1174
1270
|
}
|
|
1175
|
-
|
|
1271
|
+
await this.parent(location.path, options);
|
|
1272
|
+
await this.staged(options, async (temporary) => {
|
|
1273
|
+
const previous = append && location.entry ? this.entryBytes(location.entry, options) : undefined;
|
|
1274
|
+
const added = this.entryBytes(incomingEntry, options);
|
|
1275
|
+
await this.streamToUpper(temporary, (async function* () { if (previous)
|
|
1276
|
+
yield* previous; yield* added; })(), { ...options, flag: "wx", mode: location.entry ? 0o600 : options.mode ?? 0o666 });
|
|
1277
|
+
if (location.entry) {
|
|
1278
|
+
await this.preserve(temporary, location.entry.stat, options);
|
|
1279
|
+
await this.#upper.utimes(temporary, location.entry.stat.atimeMs, Date.now(), options);
|
|
1280
|
+
}
|
|
1281
|
+
options.signal?.throwIfAborted();
|
|
1282
|
+
await this.#upper.rename(temporary, location.path, options);
|
|
1283
|
+
this.linkMetadata.delete(location.path);
|
|
1284
|
+
this.linkOrigins.delete(location.path);
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1176
1287
|
});
|
|
1177
|
-
|
|
1178
|
-
}
|
|
1179
|
-
const bytes = await collectBytes(source, { ...(this.maxBufferBytes === Infinity ? {} : { maxBytes: this.maxBufferBytes }), ...(options.signal ? { signal: options.signal } : {}) });
|
|
1180
|
-
await this.writeFile(path, bytes, options);
|
|
1288
|
+
});
|
|
1181
1289
|
}
|
|
1182
1290
|
}
|
|
1183
1291
|
export function createOverlayFileSystem(options) {
|
|
@@ -29,6 +29,7 @@ export declare class OverlayMemoryPublication {
|
|
|
29
29
|
private check;
|
|
30
30
|
prepareChmod(path: string, options: ChmodOptions): (() => true) | undefined;
|
|
31
31
|
prepareWrite(path: string, options: ConditionalWriteFileOptions): () => true;
|
|
32
|
+
replaceContents(source: string, destination: string, guard: () => true): void;
|
|
32
33
|
writeFile(path: string, data: Uint8Array, options: ConditionalWriteFileOptions, maxBytes: number, guard?: () => void): Promise<FileStat>;
|
|
33
34
|
removeFile(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
|
|
34
35
|
prepareDirectory(path: string, options: PrepareDirectoryOptions, guard?: () => void): Promise<FileStat>;
|
|
@@ -203,6 +203,11 @@ export class OverlayMemoryPublication {
|
|
|
203
203
|
return true;
|
|
204
204
|
};
|
|
205
205
|
}
|
|
206
|
+
replaceContents(source, destination, guard) {
|
|
207
|
+
const { upper } = this.stores();
|
|
208
|
+
guard();
|
|
209
|
+
upper.replaceContents(source, destination);
|
|
210
|
+
}
|
|
206
211
|
async writeFile(path, data, options, maxBytes, guard) {
|
|
207
212
|
const check = this.prepareWrite(path, options);
|
|
208
213
|
check();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FsError } from "../../contracts/errors.js";
|
|
2
2
|
import { finishCleanup } from "../../contracts/cleanup.js";
|
|
3
|
+
import { readBytes } from "../../contracts/io.js";
|
|
3
4
|
import { openRetainedReadFile, openRetainedResizeFile, quotaCapabilities, retainedReadCapabilities, retainedResizeCapabilities } from "../capabilities.js";
|
|
4
5
|
import { admitDirectoryEntries } from "../directory-admission.js";
|
|
5
6
|
import { forwardFileDescriptor, openFileDescriptor } from "../descriptor.js";
|
|
@@ -485,12 +486,16 @@ export function withFileSystemQuota(fs, options) {
|
|
|
485
486
|
},
|
|
486
487
|
writeStream(path, source, writeOptions) {
|
|
487
488
|
return mutate(async () => {
|
|
489
|
+
if (fs.capabilities.streamingAppend !== true || !fs.writeStream)
|
|
490
|
+
throw new FsError("ENOTSUP", { syscall: "writeStream", path });
|
|
488
491
|
const append = writeOptions?.flag === "a" || writeOptions?.flag === "ax";
|
|
489
492
|
if (!append)
|
|
490
493
|
await fs.writeFile(path, new Uint8Array(), writeOptions);
|
|
491
|
-
|
|
494
|
+
else
|
|
495
|
+
await fs.writeStream(path, (async function* () { })(), writeOptions);
|
|
496
|
+
for await (const chunk of readBytes(source, writeOptions?.signal)) {
|
|
492
497
|
await assertDelta(path, await existingBytes(fs, path, writeOptions) + chunk.length, writeOptions);
|
|
493
|
-
await fs.
|
|
498
|
+
await fs.writeStream(path, (async function* () { yield chunk; })(), { ...writeOptions, flag: "a" });
|
|
494
499
|
}
|
|
495
500
|
});
|
|
496
501
|
},
|
|
@@ -502,11 +507,11 @@ export function withFileSystemQuota(fs, options) {
|
|
|
502
507
|
if (property === "publishStagedFileSet" || property === "prepareStagingResolution" || property === "prepareDirectoryAncestry" || property === "publishFileConditional" || property === "removeEntryConditional" || property === "writeFileConditional" || property === "removeFileConditional" || property === "resizeFile" || property === "canonicalizeMissingTarget" || property === "createStagedFile" || property === "publishStagedFile" || property === "removeStagedFile" || property === "prepareDirectory")
|
|
503
508
|
return undefined;
|
|
504
509
|
if (property === "capabilities")
|
|
505
|
-
return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs)), ...(typeof fs.open === "function" ? {} : { open: false }) });
|
|
510
|
+
return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs)), ...(typeof fs.open === "function" ? {} : { open: false }), ...(typeof fs.writeStream === "function" ? {} : { streamingAppend: false }) });
|
|
506
511
|
if (property === "capabilitiesFor")
|
|
507
512
|
return async (path, fsOptions) => {
|
|
508
513
|
const capabilities = await fs.capabilitiesFor?.(path, fsOptions) ?? fs.capabilities;
|
|
509
|
-
return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs, capabilities)), ...(typeof fs.open === "function" ? {} : { open: false }) });
|
|
514
|
+
return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs, capabilities)), ...(typeof fs.open === "function" ? {} : { open: false }), ...(typeof fs.writeStream === "function" ? {} : { streamingAppend: false }) });
|
|
510
515
|
};
|
|
511
516
|
if (property === "openReadFile")
|
|
512
517
|
return (path, fsOptions = {}) => openRetainedReadFile(fs, path, fsOptions);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FileSystem, ReadStreamOptions } from "../contracts/filesystem.js";
|
|
2
|
+
/** Sequential payload access without a whole-file fallback. The injected backend
|
|
3
|
+
* must supply streaming reads or retained range reads; no private storage is used.
|
|
4
|
+
* Chunks are borrowed until the consumer advances the iterator. */
|
|
5
|
+
export declare function readFileStream(fs: FileSystem, path: string, options?: ReadStreamOptions & {
|
|
6
|
+
readonly skipStream?: boolean;
|
|
7
|
+
}): AsyncGenerator<Uint8Array>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { FsError } from "../contracts/errors.js";
|
|
2
|
+
import { finishCleanup } from "../contracts/cleanup.js";
|
|
3
|
+
import { openRetainedReadFile } from "./capabilities.js";
|
|
4
|
+
const byteLength = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Uint8Array.prototype), "byteLength").get;
|
|
5
|
+
const byteKind = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Uint8Array.prototype), Symbol.toStringTag).get;
|
|
6
|
+
/** Sequential payload access without a whole-file fallback. The injected backend
|
|
7
|
+
* must supply streaming reads or retained range reads; no private storage is used.
|
|
8
|
+
* Chunks are borrowed until the consumer advances the iterator. */
|
|
9
|
+
export function readFileStream(fs, path, options = {}) {
|
|
10
|
+
let activeCleanup;
|
|
11
|
+
const generator = (async function* () {
|
|
12
|
+
const { signal } = options;
|
|
13
|
+
signal?.throwIfAborted();
|
|
14
|
+
const chunkSize = options.chunkSize ?? 65536;
|
|
15
|
+
let position = options.start ?? 0;
|
|
16
|
+
const end = options.endExclusive ?? Number.MAX_SAFE_INTEGER;
|
|
17
|
+
if (!Number.isSafeInteger(chunkSize) || chunkSize <= 0 || !Number.isSafeInteger(position) || position < 0
|
|
18
|
+
|| !Number.isSafeInteger(end) || end < position)
|
|
19
|
+
throw new FsError("EINVAL", { path, syscall: "readStream" });
|
|
20
|
+
const capabilities = await fs.capabilitiesFor?.(path, options) ?? fs.capabilities;
|
|
21
|
+
signal?.throwIfAborted();
|
|
22
|
+
if (!options.skipStream && fs.readStream && fs.capabilities?.streamingRead !== false && capabilities?.streamingRead !== false) {
|
|
23
|
+
let emitted = false;
|
|
24
|
+
let reading = true;
|
|
25
|
+
try {
|
|
26
|
+
const source = fs.readStream(path, { ...options, chunkSize });
|
|
27
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
28
|
+
let done = false, failed = true;
|
|
29
|
+
let returned;
|
|
30
|
+
const closeIterator = () => returned ??= Promise.resolve().then(() => iterator.return?.());
|
|
31
|
+
activeCleanup = () => done ? Promise.resolve() : closeIterator();
|
|
32
|
+
try {
|
|
33
|
+
while (true) {
|
|
34
|
+
signal?.throwIfAborted();
|
|
35
|
+
const item = await iterator.next();
|
|
36
|
+
signal?.throwIfAborted();
|
|
37
|
+
if (item.done) {
|
|
38
|
+
done = true;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
reading = false;
|
|
42
|
+
if (byteLength.call(item.value))
|
|
43
|
+
emitted = true;
|
|
44
|
+
yield item.value;
|
|
45
|
+
reading = true;
|
|
46
|
+
}
|
|
47
|
+
failed = false;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
activeCleanup = undefined;
|
|
51
|
+
if (!done)
|
|
52
|
+
await finishCleanup(closeIterator, failed);
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
signal?.throwIfAborted();
|
|
58
|
+
if (!reading || emitted || !(error instanceof FsError) || error.code !== "ENOTSUP")
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const handle = await openRetainedReadFile(fs, path, options);
|
|
63
|
+
let failed = true;
|
|
64
|
+
let closed;
|
|
65
|
+
const closeHandle = () => closed ??= Promise.resolve().then(() => handle.close());
|
|
66
|
+
activeCleanup = closeHandle;
|
|
67
|
+
try {
|
|
68
|
+
while (position < end) {
|
|
69
|
+
signal?.throwIfAborted();
|
|
70
|
+
const maximum = Math.min(chunkSize, end - position);
|
|
71
|
+
const bytes = await handle.read(position, maximum, options);
|
|
72
|
+
signal?.throwIfAborted();
|
|
73
|
+
if (byteKind.call(bytes) !== "Uint8Array" || byteLength.call(bytes) > maximum)
|
|
74
|
+
throw new FsError("EIO", { path, syscall: "read" });
|
|
75
|
+
const length = byteLength.call(bytes);
|
|
76
|
+
if (!length)
|
|
77
|
+
break;
|
|
78
|
+
position += length;
|
|
79
|
+
yield bytes;
|
|
80
|
+
}
|
|
81
|
+
failed = false;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
activeCleanup = undefined;
|
|
85
|
+
await finishCleanup(closeHandle, failed);
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
const originalReturn = generator.return.bind(generator);
|
|
89
|
+
generator.return = async (value) => {
|
|
90
|
+
let cleanupError;
|
|
91
|
+
try {
|
|
92
|
+
await activeCleanup?.();
|
|
93
|
+
}
|
|
94
|
+
catch (reason) {
|
|
95
|
+
cleanupError = { reason };
|
|
96
|
+
}
|
|
97
|
+
const result = await originalReturn(value);
|
|
98
|
+
if (cleanupError)
|
|
99
|
+
throw cleanupError.reason;
|
|
100
|
+
return result;
|
|
101
|
+
};
|
|
102
|
+
return generator;
|
|
103
|
+
}
|
|
@@ -13,6 +13,7 @@ export declare class ReadOnlyFileSystem implements FileSystem {
|
|
|
13
13
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
14
14
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
15
15
|
compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").EntryComparison>;
|
|
16
|
+
iterateDirectory(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
|
|
16
17
|
readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
17
18
|
realpath(path: string, options?: FsOptions): Promise<string>;
|
|
18
19
|
access(path: string, mode?: number, options?: FsOptions): Promise<void>;
|
|
@@ -122,6 +122,16 @@ export class ReadOnlyFileSystem {
|
|
|
122
122
|
compareEntry(path, peer, peerPath, options = {}) {
|
|
123
123
|
return compareEntries(this, path, peer, peerPath, options);
|
|
124
124
|
}
|
|
125
|
+
async *iterateDirectory(path, options = {}) {
|
|
126
|
+
options.signal?.throwIfAborted();
|
|
127
|
+
if (!this.#filesystem.iterateDirectory)
|
|
128
|
+
throw new FsError("ENOTSUP", { syscall: "iterateDirectory", path });
|
|
129
|
+
for await (const entry of this.#filesystem.iterateDirectory(path, options)) {
|
|
130
|
+
options.signal?.throwIfAborted();
|
|
131
|
+
yield { name: entry.name, type: entry.type };
|
|
132
|
+
}
|
|
133
|
+
options.signal?.throwIfAborted();
|
|
134
|
+
}
|
|
125
135
|
async readdir(path, options) {
|
|
126
136
|
const limit = options?.maxEntries === undefined ? undefined : directoryEntryLimit(options, path);
|
|
127
137
|
const entries = await this.#filesystem.readdir(path, options);
|
|
@@ -81,6 +81,7 @@ export declare class RealFileSystem implements FileSystem {
|
|
|
81
81
|
appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
|
|
82
82
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
83
83
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
84
|
+
iterateDirectory(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
|
|
84
85
|
readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
85
86
|
mkdir(path: string, options?: MkdirOptions): Promise<void>;
|
|
86
87
|
rmdir(path: string, options?: FsOptions): Promise<void>;
|
|
@@ -638,6 +638,33 @@ export class RealFileSystem {
|
|
|
638
638
|
return fileStat(await native.lstat(target, { bigint: true }));
|
|
639
639
|
});
|
|
640
640
|
}
|
|
641
|
+
async *iterateDirectory(path, options = {}) {
|
|
642
|
+
let handle, failed = false;
|
|
643
|
+
try {
|
|
644
|
+
// Preserve the acquired handle even if cancellation arrives during open:
|
|
645
|
+
// the following read admission will observe it and finally will close it.
|
|
646
|
+
handle = await this.operation("iterateDirectory", path, options, async () => {
|
|
647
|
+
const target = await this.path(path, options);
|
|
648
|
+
options.signal?.throwIfAborted();
|
|
649
|
+
return native.opendir(target, { bufferSize: 1 });
|
|
650
|
+
}, undefined, true);
|
|
651
|
+
while (true) {
|
|
652
|
+
const entry = await this.operation("iterateDirectory", path, options, () => handle.read());
|
|
653
|
+
if (entry === null)
|
|
654
|
+
break;
|
|
655
|
+
yield { name: entry.name, type: fileType(entry) };
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
failed = true;
|
|
660
|
+
options.signal?.throwIfAborted();
|
|
661
|
+
throw error;
|
|
662
|
+
}
|
|
663
|
+
finally {
|
|
664
|
+
if (handle)
|
|
665
|
+
await finishCleanup(() => handle.close(), failed);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
641
668
|
async readdir(path, options = {}) {
|
|
642
669
|
const limit = directoryEntryLimit(options, path);
|
|
643
670
|
return this.operation("readdir", path, options, async () => {
|
|
@@ -53,7 +53,7 @@ export declare class S3FileSystem implements FileSystem {
|
|
|
53
53
|
truncate: boolean;
|
|
54
54
|
atomicRenameNoReplace: false;
|
|
55
55
|
rename: boolean;
|
|
56
|
-
streamingAppend:
|
|
56
|
+
streamingAppend: false;
|
|
57
57
|
randomAccessWrite: false;
|
|
58
58
|
readOnly: boolean;
|
|
59
59
|
symlinks: false;
|
|
@@ -109,6 +109,7 @@ export declare class S3FileSystem implements FileSystem {
|
|
|
109
109
|
appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
|
|
110
110
|
mkdir(input: string, options?: MkdirOptions): Promise<void>;
|
|
111
111
|
readdir(input: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
112
|
+
iterateDirectory(input: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
|
|
112
113
|
private tree;
|
|
113
114
|
rmdir(input: string, options?: FsOptions): Promise<void>;
|
|
114
115
|
rm(input: string, options?: RemoveOptions): Promise<void>;
|