@poe-platform/safe-fs 0.1.806 → 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.
Files changed (34) hide show
  1. package/README.md +19 -0
  2. package/dist/safe-fs/contracts/filesystem.d.ts +5 -0
  3. package/dist/safe-fs/core.d.ts +2 -0
  4. package/dist/safe-fs/core.js +2 -0
  5. package/dist/safe-fs/fs/capabilities.js +2 -2
  6. package/dist/safe-fs/fs/devices/index.d.ts +1 -0
  7. package/dist/safe-fs/fs/devices/index.js +40 -2
  8. package/dist/safe-fs/fs/memory/atomic-view.d.ts +1 -0
  9. package/dist/safe-fs/fs/memory/index.d.ts +4 -1
  10. package/dist/safe-fs/fs/memory/index.js +56 -9
  11. package/dist/safe-fs/fs/mount/index.d.ts +3 -1
  12. package/dist/safe-fs/fs/mount/index.js +154 -3
  13. package/dist/safe-fs/fs/overlay/index.d.ts +2 -0
  14. package/dist/safe-fs/fs/overlay/index.js +167 -59
  15. package/dist/safe-fs/fs/overlay/memory-publication.d.ts +1 -0
  16. package/dist/safe-fs/fs/overlay/memory-publication.js +5 -0
  17. package/dist/safe-fs/fs/quota/index.js +9 -4
  18. package/dist/safe-fs/fs/read-file-stream.d.ts +7 -0
  19. package/dist/safe-fs/fs/read-file-stream.js +103 -0
  20. package/dist/safe-fs/fs/readonly/index.d.ts +1 -0
  21. package/dist/safe-fs/fs/readonly/index.js +10 -0
  22. package/dist/safe-fs/fs/real/index.d.ts +1 -0
  23. package/dist/safe-fs/fs/real/index.js +27 -0
  24. package/dist/safe-fs/fs/s3/filesystem.d.ts +2 -1
  25. package/dist/safe-fs/fs/s3/filesystem.js +55 -16
  26. package/dist/safe-fs/fs/scoped.js +10 -4
  27. package/dist/safe-fs/index.d.ts +1 -0
  28. package/dist/safe-fs/index.js +1 -0
  29. package/dist/safe-fs/runtime-core.d.ts +1 -0
  30. package/dist/safe-fs/runtime-core.js +1 -0
  31. package/dist/safe-fs/storage.d.ts +49 -0
  32. package/dist/safe-fs/storage.js +265 -0
  33. package/dist/safe-fs/testing/object-io-metrics.js +1 -0
  34. 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.capabilities.streamingRead === true
99
- && typeof this.#upper.writeStream === "function" && typeof this.#upper.readStream === "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, upper.truncate)],
121
- ["streamingAppend", requireCapabilities(mutation, streamingWrite, upper.streamingAppend)],
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, streaming = false) {
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 if (streaming && entry.backend.readStream && entry.backend.capabilities.streamingRead !== false && this.#upper.writeStream) {
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
- const handle = await this.pinRead(entry, options);
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, streaming = false, commitGuard) {
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, streaming);
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
- const bytes = await this.bytes(original, options);
1043
+ if (original.stat.size > this.maxBufferBytes)
1044
+ fail("EFBIG", source);
987
1045
  await this.replace(target, options, async (temporary) => {
988
- await this.#upper.writeFile(temporary, bytes, { ...options, mode: original.stat.mode & 0o7777, flag: "w" });
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), false, guard);
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
- await this.replace({ path: entry.path, entry }, options, async (temporary) => {
1083
- if (this.#upper.truncate)
1084
- await this.#upper.truncate(temporary, length, options);
1085
- else {
1086
- const previous = await this.#upper.readFile(temporary, { ...options, ...(this.maxBufferBytes === Infinity ? {} : { maxBytes: this.maxBufferBytes }) });
1087
- const bytes = new Uint8Array(length);
1088
- bytes.set(previous.subarray(0, length));
1089
- await this.#upper.writeFile(temporary, bytes, options);
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
- if (this.capabilities.streamingWrite !== false) {
1153
- await this.staged(options, async (incoming) => {
1154
- const bounded = this.bounded(source, path, options);
1155
- await this.streamToUpper(incoming, bounded, { ...options, flag: "wx", mode: 0o600 });
1156
- const size = (await this.#upper.stat(incoming, options)).size;
1157
- await this.run(options, async () => {
1158
- const location = await this.writeLocation(path, options);
1159
- const append = options.flag === "a" || options.flag === "ax";
1160
- if (append && (location.entry?.stat.size ?? 0) + size > this.maxBufferBytes)
1161
- fail("EFBIG", path);
1162
- if (this.publication.supported() && location.entry?.backend === this.#upper) {
1163
- const bytes = await this.#upper.readFile(incoming, options);
1164
- const parent = await this.required(dirname(location.path), options);
1165
- await this.publication.writeFile(location.path, bytes, {
1166
- ...options, parent: this.publication.stat(parent.path, parent.stat),
1167
- expected: this.publication.stat(location.path, location.entry.stat), append,
1168
- }, this.maxBufferBytes);
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
- await this.replace(location, options, async (temporary) => {
1172
- await this.streamToUpper(temporary, this.#upper.readStream(incoming, options), { ...options, flag: append ? "a" : "w" });
1173
- }, true);
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
- return;
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
- for await (const chunk of source) {
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.appendFile(path, chunk, writeOptions);
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: boolean;
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>;