@poe-platform/safe-fs 0.1.807 → 0.1.809

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 (41) hide show
  1. package/README.md +28 -1
  2. package/dist/safe-fs/contracts/filesystem.d.ts +20 -1
  3. package/dist/safe-fs/core.d.ts +3 -1
  4. package/dist/safe-fs/core.js +3 -1
  5. package/dist/safe-fs/fs/capabilities.d.ts +1 -1
  6. package/dist/safe-fs/fs/capabilities.js +9 -5
  7. package/dist/safe-fs/fs/devices/index.d.ts +3 -2
  8. package/dist/safe-fs/fs/devices/index.js +53 -9
  9. package/dist/safe-fs/fs/memory/atomic-view.d.ts +1 -0
  10. package/dist/safe-fs/fs/memory/index.d.ts +6 -3
  11. package/dist/safe-fs/fs/memory/index.js +101 -13
  12. package/dist/safe-fs/fs/mount/index.d.ts +4 -2
  13. package/dist/safe-fs/fs/mount/index.js +169 -10
  14. package/dist/safe-fs/fs/overlay/index.d.ts +3 -0
  15. package/dist/safe-fs/fs/overlay/index.js +177 -59
  16. package/dist/safe-fs/fs/overlay/memory-publication.d.ts +1 -0
  17. package/dist/safe-fs/fs/overlay/memory-publication.js +25 -0
  18. package/dist/safe-fs/fs/quota/index.js +9 -4
  19. package/dist/safe-fs/fs/read-file-stream.d.ts +7 -0
  20. package/dist/safe-fs/fs/read-file-stream.js +103 -0
  21. package/dist/safe-fs/fs/readonly/index.d.ts +1 -0
  22. package/dist/safe-fs/fs/readonly/index.js +10 -0
  23. package/dist/safe-fs/fs/real/index.d.ts +1 -0
  24. package/dist/safe-fs/fs/real/index.js +29 -0
  25. package/dist/safe-fs/fs/s3/filesystem.d.ts +2 -1
  26. package/dist/safe-fs/fs/s3/filesystem.js +55 -16
  27. package/dist/safe-fs/fs/scoped.js +13 -5
  28. package/dist/safe-fs/index.d.ts +1 -0
  29. package/dist/safe-fs/index.js +1 -0
  30. package/dist/safe-fs/runtime-core.d.ts +1 -0
  31. package/dist/safe-fs/runtime-core.js +1 -0
  32. package/dist/safe-fs/storage.d.ts +57 -0
  33. package/dist/safe-fs/storage.js +364 -0
  34. package/dist/safe-fs/testing/object-io-metrics.js +1 -0
  35. package/dist/xml-ast/errors.d.ts +4 -0
  36. package/dist/xml-ast/errors.js +7 -0
  37. package/dist/xml-ast/index.d.ts +2 -4
  38. package/dist/xml-ast/index.js +3 -7
  39. package/dist/xml-ast/stream.d.ts +31 -0
  40. package/dist/xml-ast/stream.js +179 -0
  41. package/package.json +10 -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));
@@ -554,7 +554,12 @@ export class OverlayFileSystem {
554
554
  async cleanup(options = {}) {
555
555
  return this.run(options, () => this.cleanGarbage(true));
556
556
  }
557
+ // Receipts describe overlay paths and identities, never backing-layer staging paths.
558
+ stagingOptions({ signal }) {
559
+ return signal === undefined ? {} : { signal };
560
+ }
557
561
  async staged(options, operation) {
562
+ options = this.stagingOptions(options);
558
563
  const root = `/.virtual-bash-overlay-${platform.randomUUID()}`;
559
564
  if (await this.maybeStat(this.#upper, root, options) || await this.maybeStat(this.#lower, root, options))
560
565
  fail("EEXIST", root);
@@ -570,6 +575,7 @@ export class OverlayFileSystem {
570
575
  }
571
576
  }
572
577
  async preserve(path, stat, options) {
578
+ options = this.stagingOptions(options);
573
579
  if (this.#upper.chmod)
574
580
  await this.#upper.chmod(path, stat.mode & 0o7777, options);
575
581
  else if (((await this.#upper.lstat(path, options)).mode & 0o7777) !== (stat.mode & 0o7777))
@@ -578,7 +584,8 @@ export class OverlayFileSystem {
578
584
  fail("ENOTSUP", path, "upper cannot preserve timestamps during copy-up");
579
585
  await this.#upper.utimes(path, stat.atimeMs, stat.mtimeMs, options);
580
586
  }
581
- async clone(entry, destination, options, streaming = false) {
587
+ async clone(entry, destination, options) {
588
+ options = this.stagingOptions(options);
582
589
  if (entry.stat.type !== "directory" && ((entry.stat.nlink ?? 1) > 1
583
590
  || (entry.backend.capabilities.hardlinks === true && entry.stat.nlink === undefined))) {
584
591
  fail("ENOTSUP", entry.path, "copy-up cannot preserve hardlink identity");
@@ -591,23 +598,14 @@ export class OverlayFileSystem {
591
598
  }
592
599
  if (entry.stat.type === "directory")
593
600
  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) {
601
+ else {
602
+ if (this.#upper.capabilities.streamingWrite !== true || !this.#upper.writeStream)
603
+ fail("ENOTSUP", destination, "copy-up requires a streaming upper");
595
604
  this.permission(entry, 4);
596
605
  if (entry.stat.size > this.maxBufferBytes)
597
606
  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
- }
607
+ await this.streamToUpper(destination, this.bounded(this.entryBytes(entry, options), entry.path, options), { ...options, mode: 0o600, flag: "wx" });
608
608
  }
609
- else
610
- await this.#upper.writeFile(destination, await this.bytes(entry, options), { ...options, mode: 0o600, flag: "wx" });
611
609
  await this.preserve(destination, entry.stat, options);
612
610
  }
613
611
  rememberLink(entry, destination) {
@@ -617,6 +615,7 @@ export class OverlayFileSystem {
617
615
  }
618
616
  }
619
617
  async ensureDirectory(path, options) {
618
+ options = this.stagingOptions(options);
620
619
  const entry = await this.lookup(path, options);
621
620
  if (!entry)
622
621
  fail("ENOENT", path);
@@ -627,6 +626,7 @@ export class OverlayFileSystem {
627
626
  await this.copyUp(entry, options);
628
627
  }
629
628
  async copyUp(entry, options) {
629
+ options = this.stagingOptions(options);
630
630
  if (entry.backend === this.#upper)
631
631
  return;
632
632
  if (entry.path === "/")
@@ -649,6 +649,7 @@ export class OverlayFileSystem {
649
649
  });
650
650
  }
651
651
  async parent(path, options) {
652
+ options = this.stagingOptions(options);
652
653
  const parent = await this.lookup(dirname(path), options);
653
654
  if (!parent)
654
655
  fail("ENOENT", dirname(path));
@@ -686,11 +687,11 @@ export class OverlayFileSystem {
686
687
  }
687
688
  return location;
688
689
  }
689
- async replace(location, options, operation, streaming = false, commitGuard) {
690
+ async replace(location, options, operation, commitGuard) {
690
691
  await this.parent(location.path, options);
691
692
  await this.staged(options, async (temporary) => {
692
693
  if (location.entry)
693
- await this.clone(location.entry, temporary, options, streaming);
694
+ await this.clone(location.entry, temporary, options);
694
695
  await operation(temporary);
695
696
  options.signal?.throwIfAborted();
696
697
  commitGuard?.();
@@ -769,6 +770,72 @@ export class OverlayFileSystem {
769
770
  const limit = directoryEntryLimit(options, path);
770
771
  return this.run(options, async () => this.listing(await this.required(path, options), options, limit), false);
771
772
  }
773
+ async *iterateDirectory(path, options = {}) {
774
+ const directory = await this.run(options, async () => {
775
+ const entry = await this.required(path, options);
776
+ if (entry.stat.type !== "directory")
777
+ fail("ENOTDIR", path);
778
+ this.permission(entry, 4);
779
+ return entry.path;
780
+ }, false);
781
+ const admit = async () => {
782
+ const current = await this.required(path, options);
783
+ if (current.path !== directory || current.stat.type !== "directory")
784
+ fail("EBUSY", path);
785
+ this.permission(current, 4);
786
+ };
787
+ for (const backend of [this.#upper, this.#lower]) {
788
+ const iterator = await this.run(options, async () => {
789
+ await admit();
790
+ if (backend === this.#lower && (!await this.lowerVisible(directory, options) || this.opaque.has(directory)))
791
+ return undefined;
792
+ if ((await this.maybeStat(backend, directory, options))?.type !== "directory")
793
+ return undefined;
794
+ if (!backend.iterateDirectory)
795
+ fail("ENOTSUP", directory);
796
+ return backend.iterateDirectory(directory, options)[Symbol.asyncIterator]();
797
+ }, false);
798
+ if (!iterator)
799
+ continue;
800
+ let complete = false, failed = false;
801
+ try {
802
+ while (true) {
803
+ const result = await this.run(options, async () => {
804
+ await admit();
805
+ if (backend === this.#lower && (!await this.lowerVisible(directory, options) || this.opaque.has(directory)))
806
+ fail("EBUSY", path);
807
+ const next = await iterator.next();
808
+ options.signal?.throwIfAborted();
809
+ if (next.done)
810
+ return { done: true };
811
+ const name = next.value.name;
812
+ if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0"))
813
+ fail("EIO", directory, "invalid backend directory entry");
814
+ const child = await this.lookup(directory === "/" ? `/${name}` : `${directory}/${name}`, options);
815
+ options.signal?.throwIfAborted();
816
+ // A lower name shadowed by upper storage is skipped by lookup,
817
+ // without retaining a directory-sized set of previously seen names.
818
+ return { done: false, entry: child?.backend === backend ? { name, type: child.stat.type } : undefined };
819
+ }, false);
820
+ if (result.done) {
821
+ complete = true;
822
+ break;
823
+ }
824
+ if (result.entry)
825
+ yield result.entry;
826
+ }
827
+ }
828
+ catch (error) {
829
+ failed = true;
830
+ throw error;
831
+ }
832
+ finally {
833
+ if (!complete)
834
+ await finishCleanup(() => iterator.return?.(), failed);
835
+ }
836
+ }
837
+ options.signal?.throwIfAborted();
838
+ }
772
839
  async realpath(path, options = {}) {
773
840
  return this.run(options, async () => (await this.required(path, options)).path, false);
774
841
  }
@@ -983,9 +1050,10 @@ export class OverlayFileSystem {
983
1050
  }
984
1051
  }
985
1052
  await this.cleanGarbage(false);
986
- const bytes = await this.bytes(original, options);
1053
+ if (original.stat.size > this.maxBufferBytes)
1054
+ fail("EFBIG", source);
987
1055
  await this.replace(target, options, async (temporary) => {
988
- await this.#upper.writeFile(temporary, bytes, { ...options, mode: original.stat.mode & 0o7777, flag: "w" });
1056
+ await this.streamToUpper(temporary, this.entryBytes(original, options), { ...options, mode: original.stat.mode & 0o7777, flag: "w" });
989
1057
  if (this.#upper.chmod)
990
1058
  await this.#upper.chmod(temporary, original.stat.mode & 0o7777, options);
991
1059
  else if (((await this.#upper.lstat(temporary, options)).mode & 0o7777) !== (original.stat.mode & 0o7777))
@@ -1057,7 +1125,7 @@ export class OverlayFileSystem {
1057
1125
  await this.#upper.chmod(entry.path, permissions, controls);
1058
1126
  }
1059
1127
  else {
1060
- await this.replace({ path: entry.path, entry }, controls, temporary => this.#upper.chmod(temporary, permissions, controls), false, guard);
1128
+ await this.replace({ path: entry.path, entry }, controls, temporary => this.#upper.chmod(temporary, permissions, controls), guard);
1061
1129
  }
1062
1130
  });
1063
1131
  }
@@ -1079,15 +1147,30 @@ export class OverlayFileSystem {
1079
1147
  if (entry.stat.type !== "file")
1080
1148
  fail("EISDIR", path);
1081
1149
  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
- }
1150
+ if ((entry.stat.nlink ?? 1) > 1 || (entry.backend.capabilities.hardlinks === true && entry.stat.nlink === undefined)) {
1151
+ fail("ENOTSUP", path, "copy-up cannot preserve hardlink identity");
1152
+ }
1153
+ await this.parent(entry.path, options);
1154
+ await this.staged(options, async (temporary) => {
1155
+ const prefix = this.entryBytes(entry, { ...options, endExclusive: length });
1156
+ const source = async function* () {
1157
+ let written = 0;
1158
+ for await (const chunk of prefix) {
1159
+ written += chunk.byteLength;
1160
+ yield chunk;
1161
+ }
1162
+ while (written < length) {
1163
+ options.signal?.throwIfAborted();
1164
+ const zero = new Uint8Array(Math.min(64 * 1024, length - written));
1165
+ written += zero.byteLength;
1166
+ yield zero;
1167
+ }
1168
+ };
1169
+ await this.streamToUpper(temporary, source(), { ...options, mode: 0o600, flag: "wx" });
1170
+ await this.preserve(temporary, entry.stat, options);
1171
+ await this.#upper.utimes(temporary, entry.stat.atimeMs, Date.now(), options);
1172
+ options.signal?.throwIfAborted();
1173
+ await this.#upper.rename(temporary, entry.path, options);
1091
1174
  });
1092
1175
  });
1093
1176
  }
@@ -1130,7 +1213,7 @@ export class OverlayFileSystem {
1130
1213
  }
1131
1214
  }
1132
1215
  async streamToUpper(path, source, options) {
1133
- if (!this.#upper.writeStream)
1216
+ if (!this.#upper.writeStream || this.#upper.capabilities.streamingWrite !== true)
1134
1217
  fail("ENOTSUP", path);
1135
1218
  const input = readBytes(source, options.signal);
1136
1219
  let failed = false;
@@ -1145,39 +1228,74 @@ export class OverlayFileSystem {
1145
1228
  await finishCleanup(() => input.return(undefined), failed);
1146
1229
  }
1147
1230
  }
1231
+ async *entryBytes(entry, options) {
1232
+ this.permission(entry, 4);
1233
+ const handle = this.activeStages.has(dirname(entry.path)) && entry.backend === this.#upper
1234
+ ? await openRetainedReadFile(this.#upper, entry.path, options) : await this.pinRead(entry, options);
1235
+ let failed = true;
1236
+ try {
1237
+ if (compareIdentity(await handle.stat(options), entry.stat) !== "same")
1238
+ fail("EAGAIN", entry.path);
1239
+ yield* this.handleBytes(handle, options);
1240
+ failed = false;
1241
+ }
1242
+ finally {
1243
+ await finishCleanup(() => handle.close(), failed);
1244
+ }
1245
+ }
1148
1246
  async writeStream(path, source, options = {}) {
1149
1247
  options.signal?.throwIfAborted();
1150
1248
  this.writeOptions(path, options);
1249
+ if (this.capabilities.streamingWrite === false)
1250
+ fail("ENOTSUP", path, "streaming requires retained reads and a streaming upper");
1151
1251
  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);
1252
+ await this.staged(options, async (incoming) => {
1253
+ await this.streamToUpper(incoming, this.bounded(source, path, options), { ...options, flag: "wx", mode: 0o600 });
1254
+ const incomingEntry = { path: incoming, backend: this.#upper, stat: await this.#upper.lstat(incoming, options) };
1255
+ await this.run(options, async () => {
1256
+ const location = await this.writeLocation(path, options);
1257
+ const append = options.flag === "a" || options.flag === "ax";
1258
+ if (append && (location.entry?.stat.size ?? 0) + incomingEntry.stat.size > this.maxBufferBytes)
1259
+ fail("EFBIG", path);
1260
+ if (this.publication.supported() && location.entry?.backend === this.#upper) {
1261
+ const parent = await this.required(dirname(location.path), options);
1262
+ const controls = { ...options, parent: this.publication.stat(parent.path, parent.stat),
1263
+ expected: this.publication.stat(location.path, location.entry.stat) };
1264
+ const guard = this.publication.prepareWrite(location.path, controls);
1265
+ if (append) {
1266
+ await this.staged(options, async (temporary) => {
1267
+ const previous = this.entryBytes(location.entry, options);
1268
+ const added = this.entryBytes(incomingEntry, options);
1269
+ await this.streamToUpper(temporary, (async function* () { yield* previous; yield* added; })(), { ...options, flag: "wx", mode: 0o600 });
1270
+ this.publication.replaceContents(temporary, location.path, guard);
1271
+ });
1169
1272
  }
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);
1273
+ else
1274
+ this.publication.replaceContents(incoming, location.path, guard);
1275
+ }
1276
+ else {
1277
+ if (location.entry && ((location.entry.stat.nlink ?? 1) > 1
1278
+ || (location.entry.backend.capabilities.hardlinks === true && location.entry.stat.nlink === undefined))) {
1279
+ fail("ENOTSUP", path, "copy-up cannot preserve hardlink identity");
1174
1280
  }
1175
- });
1281
+ await this.parent(location.path, options);
1282
+ await this.staged(options, async (temporary) => {
1283
+ const previous = append && location.entry ? this.entryBytes(location.entry, options) : undefined;
1284
+ const added = this.entryBytes(incomingEntry, options);
1285
+ await this.streamToUpper(temporary, (async function* () { if (previous)
1286
+ yield* previous; yield* added; })(), { ...options, flag: "wx", mode: location.entry ? 0o600 : options.mode ?? 0o666 });
1287
+ if (location.entry) {
1288
+ await this.preserve(temporary, location.entry.stat, options);
1289
+ await this.#upper.utimes(temporary, location.entry.stat.atimeMs, Date.now(), options);
1290
+ }
1291
+ options.signal?.throwIfAborted();
1292
+ await this.#upper.rename(temporary, location.path, options);
1293
+ this.linkMetadata.delete(location.path);
1294
+ this.linkOrigins.delete(location.path);
1295
+ });
1296
+ }
1176
1297
  });
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);
1298
+ });
1181
1299
  }
1182
1300
  }
1183
1301
  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();
@@ -371,6 +376,24 @@ export class OverlayMemoryPublication {
371
376
  const owned = await this.upper.createStagedFile(directory, name, content, { ...options, parent });
372
377
  const staging = Object.freeze({
373
378
  parent: owned.parent, directory: owned.directory, file: owned.file,
379
+ ...(owned.writer ? { writer: Object.freeze({
380
+ write: async (bytes, controls = {}) => {
381
+ controls.signal?.throwIfAborted();
382
+ this.stores();
383
+ guard?.();
384
+ this.check(ancestors);
385
+ await owned.writer.write(bytes, controls);
386
+ },
387
+ finish: async (controls = {}) => {
388
+ controls.signal?.throwIfAborted();
389
+ this.stores();
390
+ guard?.();
391
+ this.check(ancestors);
392
+ const stat = await owned.writer.finish(controls);
393
+ this.stages.set(staging.directory, { ...owned, file: { ...owned.file, stat } });
394
+ return stat;
395
+ },
396
+ }) } : {}),
374
397
  ...(owned.cleanup ? { cleanup: createStagingCleanup(directory, controls => this.cleanup(staging, controls, true), () => owned.cleanup.close()) } : {}),
375
398
  });
376
399
  this.stages.set(staging.directory, owned);
@@ -379,6 +402,8 @@ export class OverlayMemoryPublication {
379
402
  return staging;
380
403
  }
381
404
  async publish(staging, path, options) {
405
+ if (options.preserveIdentity)
406
+ throw new FsError("ENOTSUP", { path });
382
407
  options.signal?.throwIfAborted();
383
408
  this.path(path);
384
409
  const owned = this.stages.get(staging.directory);
@@ -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>;
@@ -413,6 +413,8 @@ export class RealFileSystem {
413
413
  throw new FsError("EAGAIN");
414
414
  }
415
415
  async publishStagedFile(staging, destination, options) {
416
+ if (options.preserveIdentity)
417
+ throw new FsError("ENOTSUP", { path: destination });
416
418
  return this.operation("publishStagedFile", staging.file.path, options, async () => {
417
419
  options.signal?.throwIfAborted();
418
420
  if (options.ancestors !== undefined || options.commitGuard !== undefined)
@@ -638,6 +640,33 @@ export class RealFileSystem {
638
640
  return fileStat(await native.lstat(target, { bigint: true }));
639
641
  });
640
642
  }
643
+ async *iterateDirectory(path, options = {}) {
644
+ let handle, failed = false;
645
+ try {
646
+ // Preserve the acquired handle even if cancellation arrives during open:
647
+ // the following read admission will observe it and finally will close it.
648
+ handle = await this.operation("iterateDirectory", path, options, async () => {
649
+ const target = await this.path(path, options);
650
+ options.signal?.throwIfAborted();
651
+ return native.opendir(target, { bufferSize: 1 });
652
+ }, undefined, true);
653
+ while (true) {
654
+ const entry = await this.operation("iterateDirectory", path, options, () => handle.read());
655
+ if (entry === null)
656
+ break;
657
+ yield { name: entry.name, type: fileType(entry) };
658
+ }
659
+ }
660
+ catch (error) {
661
+ failed = true;
662
+ options.signal?.throwIfAborted();
663
+ throw error;
664
+ }
665
+ finally {
666
+ if (handle)
667
+ await finishCleanup(() => handle.close(), failed);
668
+ }
669
+ }
641
670
  async readdir(path, options = {}) {
642
671
  const limit = directoryEntryLimit(options, path);
643
672
  return this.operation("readdir", path, options, async () => {