@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.
- package/README.md +28 -1
- package/dist/safe-fs/contracts/filesystem.d.ts +20 -1
- package/dist/safe-fs/core.d.ts +3 -1
- package/dist/safe-fs/core.js +3 -1
- package/dist/safe-fs/fs/capabilities.d.ts +1 -1
- package/dist/safe-fs/fs/capabilities.js +9 -5
- package/dist/safe-fs/fs/devices/index.d.ts +3 -2
- package/dist/safe-fs/fs/devices/index.js +53 -9
- package/dist/safe-fs/fs/memory/atomic-view.d.ts +1 -0
- package/dist/safe-fs/fs/memory/index.d.ts +6 -3
- package/dist/safe-fs/fs/memory/index.js +101 -13
- package/dist/safe-fs/fs/mount/index.d.ts +4 -2
- package/dist/safe-fs/fs/mount/index.js +169 -10
- package/dist/safe-fs/fs/overlay/index.d.ts +3 -0
- package/dist/safe-fs/fs/overlay/index.js +177 -59
- package/dist/safe-fs/fs/overlay/memory-publication.d.ts +1 -0
- package/dist/safe-fs/fs/overlay/memory-publication.js +25 -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 +29 -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 +13 -5
- 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 +57 -0
- package/dist/safe-fs/storage.js +364 -0
- package/dist/safe-fs/testing/object-io-metrics.js +1 -0
- package/dist/xml-ast/errors.d.ts +4 -0
- package/dist/xml-ast/errors.js +7 -0
- package/dist/xml-ast/index.d.ts +2 -4
- package/dist/xml-ast/index.js +3 -7
- package/dist/xml-ast/stream.d.ts +31 -0
- package/dist/xml-ast/stream.js +179 -0
- 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.
|
|
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));
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
1053
|
+
if (original.stat.size > this.maxBufferBytes)
|
|
1054
|
+
fail("EFBIG", source);
|
|
987
1055
|
await this.replace(target, options, async (temporary) => {
|
|
988
|
-
await this
|
|
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),
|
|
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
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
-
|
|
1153
|
-
await this.
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>;
|
|
@@ -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 () => {
|