toolcraft 0.0.759 → 0.0.760
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/composition.json +2 -2
- package/dist/composition.json +2 -2
- package/node_modules/@poe-code/safe-fs/README.md +8 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/capabilities.js +2 -2
- package/node_modules/@poe-code/safe-fs/dist/fs/memory/atomic-view.d.ts +1 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/memory/index.js +20 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/overlay/index.d.ts +1 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/overlay/index.js +101 -59
- package/node_modules/@poe-code/safe-fs/dist/fs/overlay/memory-publication.d.ts +1 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/overlay/memory-publication.js +5 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/quota/index.js +9 -4
- package/node_modules/@poe-code/safe-fs/dist/fs/s3/filesystem.d.ts +1 -1
- package/node_modules/@poe-code/safe-fs/dist/fs/s3/filesystem.js +2 -2
- package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +2 -2
package/composition.json
CHANGED
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"name": "toolcraft",
|
|
136
|
-
"version": "0.0.
|
|
136
|
+
"version": "0.0.760",
|
|
137
137
|
"license": "MIT"
|
|
138
138
|
},
|
|
139
139
|
{
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
"name": "toolcraft-schema",
|
|
146
|
-
"version": "0.0.
|
|
146
|
+
"version": "0.0.760",
|
|
147
147
|
"license": "MIT"
|
|
148
148
|
},
|
|
149
149
|
{
|
package/dist/composition.json
CHANGED
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"name": "toolcraft",
|
|
136
|
-
"version": "0.0.
|
|
136
|
+
"version": "0.0.760",
|
|
137
137
|
"license": "MIT"
|
|
138
138
|
},
|
|
139
139
|
{
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
"name": "toolcraft-schema",
|
|
146
|
-
"version": "0.0.
|
|
146
|
+
"version": "0.0.760",
|
|
147
147
|
"license": "MIT"
|
|
148
148
|
},
|
|
149
149
|
{
|
|
@@ -295,6 +295,14 @@ Every raw filesystem operation accepts an optional `signal`. Additional fields a
|
|
|
295
295
|
|
|
296
296
|
`access` takes a separate mode bitmask from `ACCESS_MODES`. `chmod` takes a mode, `utimes` takes millisecond timestamps, and `truncate` takes a byte length (default 0). For conditional chmod, check `capabilitiesFor(path, { conditionalChmod: true })` (or `capabilities`) and require `conditionalChmod: true`; supply `parent`, `expected`, and complete root-to-parent `ancestors` together. An optional mutation-free `commitGuard` must return literal `true` synchronously. Memory validates at its metadata commit; mount and supported Memory overlays preserve wrapper ancestry. Real uses its existing externally isolated host-tree boundary and does not prevent races with other processes. Backend limits still apply. Node-shaped bridge methods translate their own options rather than accepting these raw option objects; see the [bridge signatures](src/bridge/filesystem.ts).
|
|
297
297
|
|
|
298
|
+
Overlay copy-up, copy, stream replacement/append and resize transfer retained ranges in 64 KiB chunks through the injected upper's stream writer. Resize streams the retained prefix and bounded zero chunks. Stream writes stage input before publication; cancellation or transfer failure leaves the destination unchanged and cleans staging. Stock Memory overlays transfer staged storage into existing upper inodes, preserving hardlinks without an additional whole-file buffer. `readFile` and `writeFile` remain buffering convenience APIs; use `readStream`/`writeStream` for large payloads. `maxBufferBytes` retains its existing optional total-byte admission semantics, not a working-memory guarantee.
|
|
299
|
+
|
|
300
|
+
A streaming overlay needs an upper with `atomicRename`, `streamingWrite`, retained reads (`retainedRead` plus `openReadFile`), exclusive create, directory mutation, permissions and timestamps. Both layers must provide retained, bounded range reads with stable file and directory identities. For example, inject these host-authorized adapters with `createOverlayFileSystem({ upper: externalSpool, lower: retainedSource })`, then use `overlay.writeStream(path, source)`. The upper must await each consumed chunk, keep transport queues bounded, and preserve ownership until publication. Supply bounded chunks at the input boundary; the overlay cannot undo a payload-wide allocation already made by its caller. Whole-file-only uppers explicitly reject transfer operations with `ENOTSUP`; they do not silently collect streams.
|
|
301
|
+
|
|
302
|
+
For Workers, `externalSpool` must store payloads outside isolate RAM and satisfy that contract, including atomic rename and retained identity. Memory and the S3 mock store payloads in RAM and are not external spools. Stock S3/WebDAV do not provide the atomic rename/retained-read contract needed for an overlay upper; setting a capability flag does not add it. A host must inject a qualified external adapter. On Node, rooted Real adapters provide the streaming overlay configuration under their documented external-tree isolation requirement. Mount and read-only wrappers forward the selected backend's capabilities; quota streaming additionally requires a real streaming-append route. S3's buffered append convenience path reports `streamingAppend: false`.
|
|
303
|
+
|
|
304
|
+
The reusable metadata-only fixture in [`tests/fixtures/bounded-filesystem.ts`](tests/fixtures/bounded-filesystem.ts) generates file ranges and consumes slow writes without storing payload bytes. [`tests/overlay-bounded.test.ts`](tests/overlay-bounded.test.ts) checks fixed transfer sizes, source backpressure, cancellation/error cleanup, byte results, and Memory publication allocation/identity. These deterministic conformance checks do not qualify a deployed Worker backend; follow the [Worker QA plan](../../docs/plans/safe-fs-bounded-overlay-qa.md) for runtime measurements.
|
|
305
|
+
|
|
298
306
|
`collectBytes(source, { maxBytes, maxMemoryBytes, signal })` snapshots streamed chunks into one growing buffer. `maxBytes` and `maxMemoryBytes` accept explicit `Infinity`. `maxMemoryBytes` limits owned capacity, the current input's full backing buffer, and overlapping allocations during growth; exhaustion throws `EFBIG`. Collection limits are disabled by default on Node, browser, and Worker targets. Each call owns its budget; concurrent collections do not consume one another’s allowance. The returned view may retain geometric spare capacity. Configured budgets cover collection, not caller-retained results, transport buffering, archive decoding, strings, or the rest of the runtime; use streaming APIs and limit concurrent workloads for larger inputs.
|
|
299
307
|
|
|
300
308
|
<details>
|
|
@@ -141,8 +141,8 @@ export function readOnlyCapabilities(capabilities) {
|
|
|
141
141
|
});
|
|
142
142
|
}
|
|
143
143
|
export function quotaCapabilities(capabilities) {
|
|
144
|
-
const streamingWrite = requireCapabilities(capabilities.write, capabilities.append, !capabilities.readOnly);
|
|
145
|
-
const streamingAppend = requireCapabilities(capabilities.append, !capabilities.readOnly);
|
|
144
|
+
const streamingWrite = requireCapabilities(capabilities.write, capabilities.append, capabilities.streamingAppend, !capabilities.readOnly);
|
|
145
|
+
const streamingAppend = requireCapabilities(capabilities.append, capabilities.streamingAppend, !capabilities.readOnly);
|
|
146
146
|
const { streamingWrite: ignoredWrite, streamingAppend: ignoredAppend, ...rest } = capabilities;
|
|
147
147
|
return Object.freeze({ ...rest, synchronousStagingResolution: false, retainedStagingCleanup: false, retainedStagingWrite: false, atomicStagingAncestry: false, synchronousDirectoryValidation: false, guardedStagingPublication: false, atomicFilePublication: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
148
148
|
...(streamingWrite === undefined ? {} : { streamingWrite }),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FileStat, FileSystem } from "../../contracts/filesystem.js";
|
|
2
2
|
/** Internal authority: inspections and stock mutations share one JavaScript turn. */
|
|
3
3
|
export interface MemoryAtomicView {
|
|
4
|
+
replaceContents(source: string, destination: string): void;
|
|
4
5
|
stat(path: string): FileStat | undefined;
|
|
5
6
|
names(path: string): readonly string[];
|
|
6
7
|
}
|
|
@@ -656,6 +656,26 @@ export class MemoryFileSystem {
|
|
|
656
656
|
}
|
|
657
657
|
}
|
|
658
658
|
return owner.atomicView ??= {
|
|
659
|
+
replaceContents: (source, destination) => {
|
|
660
|
+
const from = this.entry(source, "overlayAtomicView").node;
|
|
661
|
+
const to = this.entry(destination, "overlayAtomicView").node;
|
|
662
|
+
if (from?.type !== "file" || to?.type !== "file" || from === to || from.nlink !== 1)
|
|
663
|
+
this.fail("EINVAL", "overlayAtomicView", destination);
|
|
664
|
+
this.permission(to, 2, "overlayAtomicView", destination);
|
|
665
|
+
// Exchange owned storage, preserving destination inode/hardlinks and
|
|
666
|
+
// retained allocation snapshots. Stage cleanup releases the old contents.
|
|
667
|
+
const allocation = to.allocation, length = to.byteLength;
|
|
668
|
+
to.allocation = from.allocation;
|
|
669
|
+
to.byteLength = from.byteLength;
|
|
670
|
+
from.allocation = allocation;
|
|
671
|
+
from.byteLength = length;
|
|
672
|
+
to.view = undefined;
|
|
673
|
+
from.view = undefined;
|
|
674
|
+
to.sourceRef = undefined;
|
|
675
|
+
from.sourceRef = undefined;
|
|
676
|
+
this.changed(to);
|
|
677
|
+
this.changed(from);
|
|
678
|
+
},
|
|
659
679
|
stat: (path) => {
|
|
660
680
|
this.validatePath(path, "overlayAtomicView");
|
|
661
681
|
let node = this.root;
|
|
@@ -82,6 +82,7 @@ export declare class OverlayFileSystem implements FileSystem {
|
|
|
82
82
|
readStream(path: string, options?: ReadStreamOptions): ByteSource;
|
|
83
83
|
private bounded;
|
|
84
84
|
private streamToUpper;
|
|
85
|
+
private entryBytes;
|
|
85
86
|
writeStream(path: string, source: ByteSource, options?: WriteFileOptions): Promise<void>;
|
|
86
87
|
}
|
|
87
88
|
export declare function createOverlayFileSystem(options: OverlayFileSystemOptions): OverlayFileSystem;
|
|
@@ -95,8 +95,8 @@ export class OverlayFileSystem {
|
|
|
95
95
|
: readable.every((capability) => capability === false) ? false : undefined;
|
|
96
96
|
const retainedRead = streamingRead;
|
|
97
97
|
const streamingWrite = writable && this.#upper.capabilities.streamingWrite === true
|
|
98
|
-
&& this.#upper.
|
|
99
|
-
&& typeof this.#upper.writeStream === "function"
|
|
98
|
+
&& retainedReadCapabilities(this.#upper).retainedRead === true
|
|
99
|
+
&& typeof this.#upper.writeStream === "function"
|
|
100
100
|
? readable.every((capability) => capability === true) ? true : undefined : false;
|
|
101
101
|
const append = !writable || this.#upper.capabilities.append === false ? false
|
|
102
102
|
: this.#upper.capabilities.append === true ? true : undefined;
|
|
@@ -117,8 +117,8 @@ export class OverlayFileSystem {
|
|
|
117
117
|
["rename", requireCapabilities(mutation, upper.rename, upper.write)],
|
|
118
118
|
["copy", requireCapabilities(mutation, upper.write)],
|
|
119
119
|
["exclusiveCopy", requireCapabilities(mutation, upper.exclusiveCreate)],
|
|
120
|
-
["truncate", requireCapabilities(mutation,
|
|
121
|
-
["streamingAppend", requireCapabilities(mutation, streamingWrite
|
|
120
|
+
["truncate", requireCapabilities(mutation, streamingWrite)],
|
|
121
|
+
["streamingAppend", requireCapabilities(mutation, streamingWrite)],
|
|
122
122
|
["randomAccessWrite", requireCapabilities(mutation, upper.randomAccessWrite)],
|
|
123
123
|
["explicitDirectories", requireCapabilities(upper.explicitDirectories, this.#lower.capabilities.explicitDirectories)],
|
|
124
124
|
].filter(([, value]) => value !== undefined));
|
|
@@ -578,7 +578,7 @@ export class OverlayFileSystem {
|
|
|
578
578
|
fail("ENOTSUP", path, "upper cannot preserve timestamps during copy-up");
|
|
579
579
|
await this.#upper.utimes(path, stat.atimeMs, stat.mtimeMs, options);
|
|
580
580
|
}
|
|
581
|
-
async clone(entry, destination, options
|
|
581
|
+
async clone(entry, destination, options) {
|
|
582
582
|
if (entry.stat.type !== "directory" && ((entry.stat.nlink ?? 1) > 1
|
|
583
583
|
|| (entry.backend.capabilities.hardlinks === true && entry.stat.nlink === undefined))) {
|
|
584
584
|
fail("ENOTSUP", entry.path, "copy-up cannot preserve hardlink identity");
|
|
@@ -591,23 +591,14 @@ export class OverlayFileSystem {
|
|
|
591
591
|
}
|
|
592
592
|
if (entry.stat.type === "directory")
|
|
593
593
|
await this.#upper.mkdir(destination, { ...options, mode: entry.stat.mode & 0o7777 });
|
|
594
|
-
else
|
|
594
|
+
else {
|
|
595
|
+
if (this.#upper.capabilities.streamingWrite !== true || !this.#upper.writeStream)
|
|
596
|
+
fail("ENOTSUP", destination, "copy-up requires a streaming upper");
|
|
595
597
|
this.permission(entry, 4);
|
|
596
598
|
if (entry.stat.size > this.maxBufferBytes)
|
|
597
599
|
fail("EFBIG", entry.path);
|
|
598
|
-
|
|
599
|
-
let failed = true;
|
|
600
|
-
try {
|
|
601
|
-
const source = this.bounded(this.handleBytes(handle, options), entry.path, options);
|
|
602
|
-
await this.streamToUpper(destination, source, { ...options, mode: 0o600, flag: "wx" });
|
|
603
|
-
failed = false;
|
|
604
|
-
}
|
|
605
|
-
finally {
|
|
606
|
-
await finishCleanup(() => handle.close(), failed);
|
|
607
|
-
}
|
|
600
|
+
await this.streamToUpper(destination, this.bounded(this.entryBytes(entry, options), entry.path, options), { ...options, mode: 0o600, flag: "wx" });
|
|
608
601
|
}
|
|
609
|
-
else
|
|
610
|
-
await this.#upper.writeFile(destination, await this.bytes(entry, options), { ...options, mode: 0o600, flag: "wx" });
|
|
611
602
|
await this.preserve(destination, entry.stat, options);
|
|
612
603
|
}
|
|
613
604
|
rememberLink(entry, destination) {
|
|
@@ -686,11 +677,11 @@ export class OverlayFileSystem {
|
|
|
686
677
|
}
|
|
687
678
|
return location;
|
|
688
679
|
}
|
|
689
|
-
async replace(location, options, operation,
|
|
680
|
+
async replace(location, options, operation, commitGuard) {
|
|
690
681
|
await this.parent(location.path, options);
|
|
691
682
|
await this.staged(options, async (temporary) => {
|
|
692
683
|
if (location.entry)
|
|
693
|
-
await this.clone(location.entry, temporary, options
|
|
684
|
+
await this.clone(location.entry, temporary, options);
|
|
694
685
|
await operation(temporary);
|
|
695
686
|
options.signal?.throwIfAborted();
|
|
696
687
|
commitGuard?.();
|
|
@@ -983,9 +974,10 @@ export class OverlayFileSystem {
|
|
|
983
974
|
}
|
|
984
975
|
}
|
|
985
976
|
await this.cleanGarbage(false);
|
|
986
|
-
|
|
977
|
+
if (original.stat.size > this.maxBufferBytes)
|
|
978
|
+
fail("EFBIG", source);
|
|
987
979
|
await this.replace(target, options, async (temporary) => {
|
|
988
|
-
await this
|
|
980
|
+
await this.streamToUpper(temporary, this.entryBytes(original, options), { ...options, mode: original.stat.mode & 0o7777, flag: "w" });
|
|
989
981
|
if (this.#upper.chmod)
|
|
990
982
|
await this.#upper.chmod(temporary, original.stat.mode & 0o7777, options);
|
|
991
983
|
else if (((await this.#upper.lstat(temporary, options)).mode & 0o7777) !== (original.stat.mode & 0o7777))
|
|
@@ -1057,7 +1049,7 @@ export class OverlayFileSystem {
|
|
|
1057
1049
|
await this.#upper.chmod(entry.path, permissions, controls);
|
|
1058
1050
|
}
|
|
1059
1051
|
else {
|
|
1060
|
-
await this.replace({ path: entry.path, entry }, controls, temporary => this.#upper.chmod(temporary, permissions, controls),
|
|
1052
|
+
await this.replace({ path: entry.path, entry }, controls, temporary => this.#upper.chmod(temporary, permissions, controls), guard);
|
|
1061
1053
|
}
|
|
1062
1054
|
});
|
|
1063
1055
|
}
|
|
@@ -1079,15 +1071,30 @@ export class OverlayFileSystem {
|
|
|
1079
1071
|
if (entry.stat.type !== "file")
|
|
1080
1072
|
fail("EISDIR", path);
|
|
1081
1073
|
this.permission(entry, 2);
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1074
|
+
if ((entry.stat.nlink ?? 1) > 1 || (entry.backend.capabilities.hardlinks === true && entry.stat.nlink === undefined)) {
|
|
1075
|
+
fail("ENOTSUP", path, "copy-up cannot preserve hardlink identity");
|
|
1076
|
+
}
|
|
1077
|
+
await this.parent(entry.path, options);
|
|
1078
|
+
await this.staged(options, async (temporary) => {
|
|
1079
|
+
const prefix = this.entryBytes(entry, { ...options, endExclusive: length });
|
|
1080
|
+
const source = async function* () {
|
|
1081
|
+
let written = 0;
|
|
1082
|
+
for await (const chunk of prefix) {
|
|
1083
|
+
written += chunk.byteLength;
|
|
1084
|
+
yield chunk;
|
|
1085
|
+
}
|
|
1086
|
+
while (written < length) {
|
|
1087
|
+
options.signal?.throwIfAborted();
|
|
1088
|
+
const zero = new Uint8Array(Math.min(64 * 1024, length - written));
|
|
1089
|
+
written += zero.byteLength;
|
|
1090
|
+
yield zero;
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
await this.streamToUpper(temporary, source(), { ...options, mode: 0o600, flag: "wx" });
|
|
1094
|
+
await this.preserve(temporary, entry.stat, options);
|
|
1095
|
+
await this.#upper.utimes(temporary, entry.stat.atimeMs, Date.now(), options);
|
|
1096
|
+
options.signal?.throwIfAborted();
|
|
1097
|
+
await this.#upper.rename(temporary, entry.path, options);
|
|
1091
1098
|
});
|
|
1092
1099
|
});
|
|
1093
1100
|
}
|
|
@@ -1130,7 +1137,7 @@ export class OverlayFileSystem {
|
|
|
1130
1137
|
}
|
|
1131
1138
|
}
|
|
1132
1139
|
async streamToUpper(path, source, options) {
|
|
1133
|
-
if (!this.#upper.writeStream)
|
|
1140
|
+
if (!this.#upper.writeStream || this.#upper.capabilities.streamingWrite !== true)
|
|
1134
1141
|
fail("ENOTSUP", path);
|
|
1135
1142
|
const input = readBytes(source, options.signal);
|
|
1136
1143
|
let failed = false;
|
|
@@ -1145,39 +1152,74 @@ export class OverlayFileSystem {
|
|
|
1145
1152
|
await finishCleanup(() => input.return(undefined), failed);
|
|
1146
1153
|
}
|
|
1147
1154
|
}
|
|
1155
|
+
async *entryBytes(entry, options) {
|
|
1156
|
+
this.permission(entry, 4);
|
|
1157
|
+
const handle = this.activeStages.has(dirname(entry.path)) && entry.backend === this.#upper
|
|
1158
|
+
? await openRetainedReadFile(this.#upper, entry.path, options) : await this.pinRead(entry, options);
|
|
1159
|
+
let failed = true;
|
|
1160
|
+
try {
|
|
1161
|
+
if (compareIdentity(await handle.stat(options), entry.stat) !== "same")
|
|
1162
|
+
fail("EAGAIN", entry.path);
|
|
1163
|
+
yield* this.handleBytes(handle, options);
|
|
1164
|
+
failed = false;
|
|
1165
|
+
}
|
|
1166
|
+
finally {
|
|
1167
|
+
await finishCleanup(() => handle.close(), failed);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1148
1170
|
async writeStream(path, source, options = {}) {
|
|
1149
1171
|
options.signal?.throwIfAborted();
|
|
1150
1172
|
this.writeOptions(path, options);
|
|
1173
|
+
if (this.capabilities.streamingWrite === false)
|
|
1174
|
+
fail("ENOTSUP", path, "streaming requires retained reads and a streaming upper");
|
|
1151
1175
|
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
|
-
|
|
1176
|
+
await this.staged(options, async (incoming) => {
|
|
1177
|
+
await this.streamToUpper(incoming, this.bounded(source, path, options), { ...options, flag: "wx", mode: 0o600 });
|
|
1178
|
+
const incomingEntry = { path: incoming, backend: this.#upper, stat: await this.#upper.lstat(incoming, options) };
|
|
1179
|
+
await this.run(options, async () => {
|
|
1180
|
+
const location = await this.writeLocation(path, options);
|
|
1181
|
+
const append = options.flag === "a" || options.flag === "ax";
|
|
1182
|
+
if (append && (location.entry?.stat.size ?? 0) + incomingEntry.stat.size > this.maxBufferBytes)
|
|
1183
|
+
fail("EFBIG", path);
|
|
1184
|
+
if (this.publication.supported() && location.entry?.backend === this.#upper) {
|
|
1185
|
+
const parent = await this.required(dirname(location.path), options);
|
|
1186
|
+
const controls = { ...options, parent: this.publication.stat(parent.path, parent.stat),
|
|
1187
|
+
expected: this.publication.stat(location.path, location.entry.stat) };
|
|
1188
|
+
const guard = this.publication.prepareWrite(location.path, controls);
|
|
1189
|
+
if (append) {
|
|
1190
|
+
await this.staged(options, async (temporary) => {
|
|
1191
|
+
const previous = this.entryBytes(location.entry, options);
|
|
1192
|
+
const added = this.entryBytes(incomingEntry, options);
|
|
1193
|
+
await this.streamToUpper(temporary, (async function* () { yield* previous; yield* added; })(), { ...options, flag: "wx", mode: 0o600 });
|
|
1194
|
+
this.publication.replaceContents(temporary, location.path, guard);
|
|
1195
|
+
});
|
|
1169
1196
|
}
|
|
1170
|
-
else
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1197
|
+
else
|
|
1198
|
+
this.publication.replaceContents(incoming, location.path, guard);
|
|
1199
|
+
}
|
|
1200
|
+
else {
|
|
1201
|
+
if (location.entry && ((location.entry.stat.nlink ?? 1) > 1
|
|
1202
|
+
|| (location.entry.backend.capabilities.hardlinks === true && location.entry.stat.nlink === undefined))) {
|
|
1203
|
+
fail("ENOTSUP", path, "copy-up cannot preserve hardlink identity");
|
|
1174
1204
|
}
|
|
1175
|
-
|
|
1205
|
+
await this.parent(location.path, options);
|
|
1206
|
+
await this.staged(options, async (temporary) => {
|
|
1207
|
+
const previous = append && location.entry ? this.entryBytes(location.entry, options) : undefined;
|
|
1208
|
+
const added = this.entryBytes(incomingEntry, options);
|
|
1209
|
+
await this.streamToUpper(temporary, (async function* () { if (previous)
|
|
1210
|
+
yield* previous; yield* added; })(), { ...options, flag: "wx", mode: location.entry ? 0o600 : options.mode ?? 0o666 });
|
|
1211
|
+
if (location.entry) {
|
|
1212
|
+
await this.preserve(temporary, location.entry.stat, options);
|
|
1213
|
+
await this.#upper.utimes(temporary, location.entry.stat.atimeMs, Date.now(), options);
|
|
1214
|
+
}
|
|
1215
|
+
options.signal?.throwIfAborted();
|
|
1216
|
+
await this.#upper.rename(temporary, location.path, options);
|
|
1217
|
+
this.linkMetadata.delete(location.path);
|
|
1218
|
+
this.linkOrigins.delete(location.path);
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1176
1221
|
});
|
|
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);
|
|
1222
|
+
});
|
|
1181
1223
|
}
|
|
1182
1224
|
}
|
|
1183
1225
|
export function createOverlayFileSystem(options) {
|
|
@@ -29,6 +29,7 @@ export declare class OverlayMemoryPublication {
|
|
|
29
29
|
private check;
|
|
30
30
|
prepareChmod(path: string, options: ChmodOptions): (() => true) | undefined;
|
|
31
31
|
prepareWrite(path: string, options: ConditionalWriteFileOptions): () => true;
|
|
32
|
+
replaceContents(source: string, destination: string, guard: () => true): void;
|
|
32
33
|
writeFile(path: string, data: Uint8Array, options: ConditionalWriteFileOptions, maxBytes: number, guard?: () => void): Promise<FileStat>;
|
|
33
34
|
removeFile(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
|
|
34
35
|
prepareDirectory(path: string, options: PrepareDirectoryOptions, guard?: () => void): Promise<FileStat>;
|
|
@@ -203,6 +203,11 @@ export class OverlayMemoryPublication {
|
|
|
203
203
|
return true;
|
|
204
204
|
};
|
|
205
205
|
}
|
|
206
|
+
replaceContents(source, destination, guard) {
|
|
207
|
+
const { upper } = this.stores();
|
|
208
|
+
guard();
|
|
209
|
+
upper.replaceContents(source, destination);
|
|
210
|
+
}
|
|
206
211
|
async writeFile(path, data, options, maxBytes, guard) {
|
|
207
212
|
const check = this.prepareWrite(path, options);
|
|
208
213
|
check();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FsError } from "../../contracts/errors.js";
|
|
2
2
|
import { finishCleanup } from "../../contracts/cleanup.js";
|
|
3
|
+
import { readBytes } from "../../contracts/io.js";
|
|
3
4
|
import { openRetainedReadFile, openRetainedResizeFile, quotaCapabilities, retainedReadCapabilities, retainedResizeCapabilities } from "../capabilities.js";
|
|
4
5
|
import { admitDirectoryEntries } from "../directory-admission.js";
|
|
5
6
|
import { forwardFileDescriptor, openFileDescriptor } from "../descriptor.js";
|
|
@@ -485,12 +486,16 @@ export function withFileSystemQuota(fs, options) {
|
|
|
485
486
|
},
|
|
486
487
|
writeStream(path, source, writeOptions) {
|
|
487
488
|
return mutate(async () => {
|
|
489
|
+
if (fs.capabilities.streamingAppend !== true || !fs.writeStream)
|
|
490
|
+
throw new FsError("ENOTSUP", { syscall: "writeStream", path });
|
|
488
491
|
const append = writeOptions?.flag === "a" || writeOptions?.flag === "ax";
|
|
489
492
|
if (!append)
|
|
490
493
|
await fs.writeFile(path, new Uint8Array(), writeOptions);
|
|
491
|
-
|
|
494
|
+
else
|
|
495
|
+
await fs.writeStream(path, (async function* () { })(), writeOptions);
|
|
496
|
+
for await (const chunk of readBytes(source, writeOptions?.signal)) {
|
|
492
497
|
await assertDelta(path, await existingBytes(fs, path, writeOptions) + chunk.length, writeOptions);
|
|
493
|
-
await fs.
|
|
498
|
+
await fs.writeStream(path, (async function* () { yield chunk; })(), { ...writeOptions, flag: "a" });
|
|
494
499
|
}
|
|
495
500
|
});
|
|
496
501
|
},
|
|
@@ -502,11 +507,11 @@ export function withFileSystemQuota(fs, options) {
|
|
|
502
507
|
if (property === "publishStagedFileSet" || property === "prepareStagingResolution" || property === "prepareDirectoryAncestry" || property === "publishFileConditional" || property === "removeEntryConditional" || property === "writeFileConditional" || property === "removeFileConditional" || property === "resizeFile" || property === "canonicalizeMissingTarget" || property === "createStagedFile" || property === "publishStagedFile" || property === "removeStagedFile" || property === "prepareDirectory")
|
|
503
508
|
return undefined;
|
|
504
509
|
if (property === "capabilities")
|
|
505
|
-
return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs)), ...(typeof fs.open === "function" ? {} : { open: false }) });
|
|
510
|
+
return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs)), ...(typeof fs.open === "function" ? {} : { open: false }), ...(typeof fs.writeStream === "function" ? {} : { streamingAppend: false }) });
|
|
506
511
|
if (property === "capabilitiesFor")
|
|
507
512
|
return async (path, fsOptions) => {
|
|
508
513
|
const capabilities = await fs.capabilitiesFor?.(path, fsOptions) ?? fs.capabilities;
|
|
509
|
-
return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs, capabilities)), ...(typeof fs.open === "function" ? {} : { open: false }) });
|
|
514
|
+
return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs, capabilities)), ...(typeof fs.open === "function" ? {} : { open: false }), ...(typeof fs.writeStream === "function" ? {} : { streamingAppend: false }) });
|
|
510
515
|
};
|
|
511
516
|
if (property === "openReadFile")
|
|
512
517
|
return (path, fsOptions = {}) => openRetainedReadFile(fs, path, fsOptions);
|
|
@@ -152,8 +152,8 @@ export class S3FileSystem {
|
|
|
152
152
|
atomicRenameNoReplace: false,
|
|
153
153
|
rename: this.allowRename && options.transport.capabilities?.conditionalDelete === true
|
|
154
154
|
&& (options.transport.capabilities?.conditionalCopy === true || options.transport.capabilities?.conditionalPut === true),
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
// Conditional append currently collects the old and incoming bodies.
|
|
156
|
+
streamingAppend: false,
|
|
157
157
|
randomAccessWrite: false,
|
|
158
158
|
readOnly: options.readOnly ?? false,
|
|
159
159
|
symlinks: false, hardlinks: false, permissions: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.760",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
"@poe-code/terminal-text"
|
|
176
176
|
],
|
|
177
177
|
"optionalDependencies": {
|
|
178
|
-
"toolcraft-schema": "0.0.
|
|
178
|
+
"toolcraft-schema": "0.0.760",
|
|
179
179
|
"toolcraft-design": "*",
|
|
180
180
|
"@poe-code/frontmatter": "*",
|
|
181
181
|
"@poe-code/agent-mcp-config": "*",
|