@ultimat3/storage 30.8.0 → 31.0.0

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.
@@ -0,0 +1,125 @@
1
+ // Single responsibility: a signed upload taken as a STREAM — the head read and sniffed, the rest
2
+ // piped into `disk.putStream`, so memory per upload is the sniff window, not the body. A body that
3
+ // ends inside the window, an XML type or a head opening with `<` (a script can sit anywhere in
4
+ // either), and a required checksum nobody declared are read whole by `acceptSignedUpload`'s rule.
5
+
6
+ import { type AcceptSignedUploadInput, acceptSignedUpload, signedObjectPut } from './accept';
7
+ import type { StorageObject } from './driver';
8
+ import { checksumMismatch, tooLarge } from './errors';
9
+ import {
10
+ isXmlType,
11
+ opensWithMarkup,
12
+ SNIFF_WINDOW_BYTES,
13
+ uploadPolicy,
14
+ validateStoredHead,
15
+ } from './upload';
16
+
17
+ export interface AcceptSignedUploadStreamInput extends Omit<AcceptSignedUploadInput, 'bytes'> {
18
+ /** The request's body; `null` is an empty one. Read once, never buffered past the window. */
19
+ readonly body: ReadableStream<Uint8Array> | null;
20
+ }
21
+
22
+ /** Chunks read until `window` bytes are in hand or the body ends; past `ceiling` it is refused. */
23
+ async function readHead(
24
+ reader: ReadableStreamDefaultReader<Uint8Array>,
25
+ key: string,
26
+ window: number,
27
+ ceiling: number,
28
+ ): Promise<{ readonly chunks: Uint8Array[]; readonly size: number; readonly done: boolean }> {
29
+ const chunks: Uint8Array[] = [];
30
+ let size = 0;
31
+ while (size < window) {
32
+ const next = await reader.read();
33
+ if (next.done) return { chunks, size, done: true };
34
+ chunks.push(next.value);
35
+ size += next.value.byteLength;
36
+ if (size > ceiling) throw tooLarge(key, size, ceiling);
37
+ }
38
+ return { chunks, size, done: false };
39
+ }
40
+
41
+ /** The uploader's body, no longer wanted: cancelled so the socket stops, whatever cancel answers. */
42
+ async function stopReading(reader: ReadableStreamDefaultReader<Uint8Array>): Promise<void> {
43
+ try {
44
+ await reader.cancel();
45
+ } catch {
46
+ // An already-errored body cannot be cancelled, and has stopped anyway.
47
+ }
48
+ }
49
+
50
+ const concat = (chunks: readonly Uint8Array[]): Uint8Array =>
51
+ new Uint8Array(Bun.concatArrayBuffers([...chunks]));
52
+
53
+ /**
54
+ * `acceptSignedUpload` for a body that may not fit in memory: the same gates, in the same order,
55
+ * with the magic bytes judged on the head. Past the ceiling, or a declared checksum the bytes do
56
+ * not hash to, the stream errors and `putStream` abandons the write: the key is as it was.
57
+ */
58
+ export async function acceptSignedUploadStream(
59
+ input: AcceptSignedUploadStreamInput,
60
+ ): Promise<StorageObject> {
61
+ const { key, contentType, maxBytes } = await signedObjectPut(input);
62
+ const policy = input.policy ?? uploadPolicy();
63
+ const ceiling = Math.min(policy.maxBytes, maxBytes ?? policy.maxBytes);
64
+ const reader = (input.body ?? new Blob([]).stream()).getReader();
65
+ let head: Awaited<ReturnType<typeof readHead>>;
66
+ try {
67
+ head = await readHead(reader, key, SNIFF_WINDOW_BYTES, ceiling);
68
+ } catch (error) {
69
+ await stopReading(reader);
70
+ throw error;
71
+ }
72
+ const whole =
73
+ head.done ||
74
+ isXmlType(contentType) ||
75
+ opensWithMarkup(concat(head.chunks)) ||
76
+ (policy.requireChecksum && input.checksum === undefined);
77
+ if (whole) {
78
+ const chunks = [...head.chunks];
79
+ let size = head.size;
80
+ try {
81
+ for (
82
+ let next = head.done ? undefined : await reader.read();
83
+ next !== undefined && !next.done;
84
+ next = await reader.read()
85
+ ) {
86
+ size += next.value.byteLength;
87
+ if (size > ceiling) throw tooLarge(key, size, ceiling);
88
+ chunks.push(next.value);
89
+ }
90
+ } finally {
91
+ await stopReading(reader);
92
+ }
93
+ return acceptSignedUpload({ ...input, bytes: concat(chunks) });
94
+ }
95
+ // The head says more follows: `size + 1` is "at least one byte past it", so an XML or a cut
96
+ // character is judged as a head, never as a whole body.
97
+ validateStoredHead(
98
+ { key, declaredContentType: contentType, head: concat(head.chunks), size: head.size + 1 },
99
+ policy,
100
+ );
101
+ const hasher = input.checksum === undefined ? undefined : new Bun.CryptoHasher('sha256');
102
+ let size = 0;
103
+ const counted = (chunk: Uint8Array): Uint8Array => {
104
+ size += chunk.byteLength;
105
+ if (size > ceiling) throw tooLarge(key, size, ceiling);
106
+ hasher?.update(chunk);
107
+ return chunk;
108
+ };
109
+ const queued = head.chunks.slice();
110
+ const body = new ReadableStream<Uint8Array>({
111
+ async pull(controller) {
112
+ const first = queued.shift();
113
+ if (first !== undefined) return controller.enqueue(counted(first));
114
+ const next = await reader.read();
115
+ if (!next.done) return controller.enqueue(counted(next.value));
116
+ const actual = hasher?.digest('base64');
117
+ if (actual !== undefined && actual !== input.checksum) {
118
+ throw checksumMismatch(key, input.checksum ?? 'none', actual);
119
+ }
120
+ controller.close();
121
+ },
122
+ cancel: (reason) => reader.cancel(reason),
123
+ });
124
+ return input.disk.putStream(key, body, { maxBytes: ceiling, contentType });
125
+ }
package/src/accept.ts CHANGED
@@ -133,13 +133,21 @@ export async function signedUploadConstraints(
133
133
  return constraintsFor(input, 'PUT');
134
134
  }
135
135
 
136
+ /** What a verified object PUT may store: the key, the type the grant signed, its ceiling. */
137
+ export interface SignedObjectPut {
138
+ readonly key: string;
139
+ readonly contentType: string;
140
+ readonly maxBytes: number | undefined;
141
+ }
142
+
136
143
  /**
137
- * Take a presigned PUT and write it, or refuse. Four gates the client cannot move because all
138
- * four are inside the signature or inside the bytes: the signature itself, the expiry, the byte
139
- * count against the signed ceiling, and the declared type against both the signature and the
140
- * magic bytes. `validateUpload` runs last because it is the only one that reads the whole body.
144
+ * Every gate a PUT passes before its body is read: the signature, the expiry, the tenant, the
145
+ * method, an OBJECT url (never a part's), and the declared type against the signed one. Shared by
146
+ * `acceptSignedUpload` and the streamed `acceptSignedUploadStream`, so the two are one rule.
141
147
  */
142
- export async function acceptSignedUpload(input: AcceptSignedUploadInput): Promise<StorageObject> {
148
+ export async function signedObjectPut(
149
+ input: SignedRequestInput & { readonly declaredContentType?: string | undefined },
150
+ ): Promise<SignedObjectPut> {
143
151
  const constraints = await constraintsFor(input, 'PUT');
144
152
  const key = constraints.key;
145
153
  // A part URL writes one part of an open upload, never the object: `acceptSignedPart` takes it.
@@ -169,9 +177,19 @@ export async function acceptSignedUpload(input: AcceptSignedUploadInput): Promis
169
177
  );
170
178
  }
171
179
 
180
+ return { key, contentType: signed, maxBytes: constraints.maxBytes };
181
+ }
182
+
183
+ /**
184
+ * Take a presigned PUT and write it, or refuse. Four gates the client cannot move because all
185
+ * four are inside the signature or inside the bytes: the signature itself, the expiry, the byte
186
+ * count against the signed ceiling, and the declared type against both the signature and the
187
+ * magic bytes. `validateUpload` runs last because it is the only one that reads the whole body.
188
+ */
189
+ export async function acceptSignedUpload(input: AcceptSignedUploadInput): Promise<StorageObject> {
190
+ const { key, contentType: signed, maxBytes } = await signedObjectPut(input);
172
191
  // The signed ceiling, checked before the policy's: the two can differ once a policy is edited
173
192
  // between the grant and the upload, and the narrower one is the one that was granted.
174
- const maxBytes = constraints.maxBytes;
175
193
  const size = input.bytes.byteLength;
176
194
  if (maxBytes !== undefined && size > maxBytes) throw tooLarge(key, size, maxBytes);
177
195
 
package/src/adapter.ts ADDED
@@ -0,0 +1,46 @@
1
+ // `@ultimat3/storage/adapter`: two ports as an adapter author needs them. `storage`: `StorageDriver`,
2
+ // its shapes, errors and shared refusal helpers — no installer, the boot READS `runtime.ts`'s
3
+ // driver as its default disk (`@ultimat3/cli`'s `runtime-storage-boot.ts`). `uploadScanner`:
4
+ // `UploadScanner`, what it is asked and answers, and the boot's installer.
5
+
6
+ export type {
7
+ ByteLimit,
8
+ CopyOptions,
9
+ ListOptions,
10
+ PutOptions,
11
+ ServerSideEncryption,
12
+ SignedUrlMethod,
13
+ SignedUrlOptions,
14
+ StorageBody,
15
+ StorageDriver,
16
+ StorageListEntry,
17
+ StorageObject,
18
+ StorageRead,
19
+ StreamOptions,
20
+ } from './driver';
21
+ export { assertPutOptions, etagOf, resolveListLimit, toBytes } from './driver';
22
+ export type { LocalDriverOptions } from './driver-local';
23
+ export type { MemoryStorageDriverOptions } from './driver-memory';
24
+ export type { S3DriverOptions } from './driver-s3';
25
+ export type { StorageErrorCode, StorageErrorInit } from './errors';
26
+ export {
27
+ checksumMismatch,
28
+ deleteFailed,
29
+ getTooLarge,
30
+ isStorageError,
31
+ keyConflict,
32
+ listFailed,
33
+ objectNotFound,
34
+ pathUnsafe,
35
+ putFailed,
36
+ putTooLarge,
37
+ readFailed,
38
+ StorageError,
39
+ } from './errors';
40
+ export type { MultipartCapability } from './multipart';
41
+ export type { ObjectLock, ObjectLockOptions } from './object-lock';
42
+ export type { PutStreamOptions } from './put-stream';
43
+ export type { ByteRange, GetOptions } from './range';
44
+ export type { SignedPart, SignedUrlVerification } from './signed-url';
45
+ export type { ScanInput, ScanVerdict, UploadScanner } from './upload-scanner';
46
+ export { installUploadScanner, resetUploadScanner } from './upload-scanner';
package/src/attachment.ts CHANGED
@@ -5,31 +5,18 @@
5
5
  // so the tenant prefix is a construction, not a check somebody remembered to write.
6
6
 
7
7
  import type { Clock } from '@ultimat3/core';
8
- import { assert, finiteCount, renderThrowable, systemClock } from '@ultimat3/core';
8
+ import { assert, finiteCount, nanoid, renderThrowable, systemClock } from '@ultimat3/core';
9
9
  import type { StorageDriver, StorageListEntry, StorageObject } from './driver';
10
10
  import { objectNotFound, orgMismatch, tooLarge } from './errors';
11
- import { alreadyPromoted, notPending, objectChanged, quarantined } from './errors-promote';
11
+ import { alreadyPromoted, notPending, objectChanged } from './errors-promote';
12
12
  import { assertSafeKey, isWithinOrg, orgPrefix, scopedKey } from './path';
13
13
  import type { UploadPolicy } from './upload';
14
14
  import { SNIFF_WINDOW_BYTES, validateStoredHead } from './upload';
15
+ import { SCAN_SEGMENT, scanAttempt, scanning } from './upload-scan';
15
16
 
16
17
  /** The one segment an unattached upload lives under. `sweepOrphans` reads only this prefix. */
17
18
  export const PENDING_SEGMENT = 'pending';
18
19
 
19
- /**
20
- * One more segment inside `pending/`, and the whole of Ultimate's content-scanning mechanism.
21
- *
22
- * Magic-byte sniffing closes stored XSS; it is not malware scanning, and `application/zip` is
23
- * deliberately accepted as the OOXML container, so a macro-laden `.docx` passes `validateUpload`
24
- * exactly as a clean one does. Scanning is the APP's — a scanner is a business decision with a
25
- * vendor, a licence and a latency budget (axiom 8) — so what ships is the place to put one: an
26
- * upload granted with `quarantine: true` lands under this prefix, `promoteAttachment` REFUSES a
27
- * key that is still there (`X_STORAGE_QUARANTINED`), and the app's scan job calls
28
- * `releaseQuarantine` on a verdict of clean. Inside `pending/` on purpose: an upload nobody ever
29
- * scanned is still an orphan, so `sweepOrphans` collects it with no second prefix to walk.
30
- */
31
- export const QUARANTINE_SEGMENT = 'quarantine';
32
-
33
20
  export interface AttachmentTarget {
34
21
  /** Entity name as the app declares it — `post`, `user`. Exactly one key segment. */
35
22
  readonly entity: string;
@@ -78,48 +65,9 @@ export function attachmentKey(orgId: string, target: AttachmentTarget, name: str
78
65
  return scopedKey(orgId, target.entity, target.id, target.field, name);
79
66
  }
80
67
 
81
- /** `org/<orgId>/pending/quarantine/<name>` — uploaded, validated, and not yet cleared to use. */
82
- export function quarantineKey(orgId: string, name: string): string {
83
- return scopedKey(orgId, PENDING_SEGMENT, QUARANTINE_SEGMENT, name);
84
- }
85
-
86
- export function quarantinePrefix(orgId: string): string {
87
- return `${pendingPrefix(orgId)}${QUARANTINE_SEGMENT}/`;
88
- }
89
-
90
68
  export const isPendingKey = (key: string, orgId: string): boolean =>
91
69
  key.startsWith(pendingPrefix(orgId));
92
70
 
93
- /** True while nothing has cleared the key. `promoteAttachment` refuses exactly this. */
94
- export const isQuarantinedKey = (key: string, orgId: string): boolean =>
95
- key.startsWith(quarantinePrefix(orgId));
96
-
97
- export interface ReleaseQuarantineInput {
98
- readonly disk: StorageDriver;
99
- /** A key under `quarantinePrefix(orgId)`. */
100
- readonly key: string;
101
- readonly orgId: string;
102
- }
103
-
104
- /**
105
- * The app's scan said clean: move the object out of quarantine and onto the ordinary `pending/`
106
- * key, which `promoteAttachment` will accept. Copy then delete, for the reason promotion does —
107
- * a delete that ran first loses the bytes on a failed write.
108
- *
109
- * Releasing a key that is not quarantined returns it untouched rather than copying it onto
110
- * itself: a scan job that retries after a crash must not be a second round trip, and a `pending/`
111
- * key is already the released state.
112
- */
113
- export async function releaseQuarantine(input: ReleaseQuarantineInput): Promise<string> {
114
- if (!isWithinOrg(input.key, input.orgId)) throw orgMismatch(input.key, input.orgId);
115
- if (!isQuarantinedKey(input.key, input.orgId)) return input.key;
116
- const name = input.key.slice(input.key.lastIndexOf('/') + 1);
117
- const released = pendingKey(input.orgId, name);
118
- await input.disk.copy(input.key, released);
119
- await input.disk.delete(input.key);
120
- return released;
121
- }
122
-
123
71
  export interface PromoteAttachmentInput {
124
72
  readonly disk: StorageDriver;
125
73
  /** A key `grantUpload` minted with no `target`. */
@@ -197,6 +145,14 @@ async function assertCopyIsWhatWasJudged(
197
145
  * pending too. A ranged read because `maxGetBytes` bounds the range, never the object. An overwrite
198
146
  * after that read is `X_STORAGE_OBJECT_CHANGED`, or the copy itself is judged, and never lands.
199
147
  *
148
+ * SCANNED before it attaches, when `runtime.ts` installs an `uploadScanner`: on a per-attempt copy
149
+ * no presigned URL can rewrite (`scannedAttempt`), which is then what is attached. Infected is
150
+ * `X_STORAGE_UPLOAD_INFECTED` with attempt and upload deleted; no verdict is
151
+ * `X_STORAGE_SCAN_UNAVAILABLE` with the attempt deleted and the upload still pending, unless
152
+ * `policy.onScanUnavailable` accepts; a `hold` verdict (a review, an async vendor scan) is
153
+ * `X_STORAGE_QUARANTINED`, the upload pending, and no policy overrides it. None ever touches the
154
+ * row's key. This is the ONE scan path: the v30 `quarantine: true` segment is retired.
155
+ *
200
156
  * A source that is gone with the destination already there is `X_STORAGE_ALREADY_PROMOTED`
201
157
  * (409, `meta.attachedKey`), never the destination answered as if this call had moved it. 27.1.0
202
158
  * answered it: a confirm sent twice then inserted a second row for one key, that insert failed,
@@ -213,7 +169,6 @@ export async function promoteAttachment(input: PromoteAttachmentInput): Promise<
213
169
  // Pending, not merely in-org: an attached key a client sent back belongs to another row, and the
214
170
  // copy-then-delete below would move that row's file here and delete theirs.
215
171
  if (!isPendingKey(input.key, input.orgId)) throw notPending(input.key, input.orgId);
216
- if (isQuarantinedKey(input.key, input.orgId)) throw quarantined(input.key, input.orgId);
217
172
  // The type says required; an untyped caller upgrading from the four-field call does not read
218
173
  // types, and `input.policy.maxBytes` on `undefined` is a bare `TypeError` two lines down.
219
174
  assert(
@@ -235,16 +190,43 @@ export async function promoteAttachment(input: PromoteAttachmentInput): Promise<
235
190
  throw tooLarge(input.key, pending.size, input.policy.maxBytes);
236
191
  }
237
192
  await judge(disk, pending, input.key, input.policy);
193
+ const scanned = scanning() ? await scannedAttempt(disk, pending, input, name) : undefined;
194
+ const source = scanned ?? pending;
238
195
  const object = await disk.copy(
239
- input.key,
196
+ scanned?.key ?? input.key,
240
197
  attached,
241
- pending.etag === '' ? undefined : { ifMatch: pending.etag },
198
+ source.etag === '' ? undefined : { ifMatch: source.etag },
242
199
  );
243
- await assertCopyIsWhatWasJudged(disk, pending, object, input.policy);
200
+ await assertCopyIsWhatWasJudged(disk, source, object, input.policy);
201
+ // The attempt is this call's scratch: a delete that fails leaves an orphan the sweep collects,
202
+ // never a refusal of a promotion that already happened.
203
+ if (scanned !== undefined) await disk.delete(scanned.key).catch(() => undefined);
244
204
  await disk.delete(input.key);
245
205
  return object;
246
206
  }
247
207
 
208
+ /**
209
+ * With an `uploadScanner` installed, what is attached is a copy THIS call made and scanned: under
210
+ * `pending/scan/`, server-named (no presigned URL rewrites it after the scan) and its own (a
211
+ * refusal deletes it and nothing a doubled confirm attached). Judged like any copy first.
212
+ */
213
+ async function scannedAttempt(
214
+ disk: StorageDriver,
215
+ pending: StorageObject,
216
+ input: PromoteAttachmentInput,
217
+ name: string,
218
+ ): Promise<StorageObject> {
219
+ const key = scopedKey(input.orgId, PENDING_SEGMENT, SCAN_SEGMENT, `${nanoid()}-${name}`);
220
+ const attempt = await disk.copy(
221
+ input.key,
222
+ key,
223
+ pending.etag === '' ? undefined : { ifMatch: pending.etag },
224
+ );
225
+ await assertCopyIsWhatWasJudged(disk, pending, attempt, input.policy);
226
+ await scanAttempt({ disk, pendingKey: input.key, attempt, policy: input.policy });
227
+ return attempt;
228
+ }
229
+
248
230
  export interface SweepOrphansInput {
249
231
  readonly disk: StorageDriver;
250
232
  readonly orgId: string;
@@ -277,8 +259,8 @@ export interface SweepResult {
277
259
 
278
260
  /**
279
261
  * Delete every unattached upload past the window, and answer with what went and what did not.
280
- * Scoped to one org and to the `pending/` prefix — quarantine included, since it lives inside
281
- * that prefix: a sweep that could reach an attached key is a job that deletes production data
262
+ * Scoped to one org and to the `pending/` prefix — a `hold` included, since a held upload is still
263
+ * pending: a sweep that could reach an attached key is a job that deletes production data
282
264
  * the first time an app forgets to promote something.
283
265
  *
284
266
  * A refusal is recorded and the sweep continues. Stopping at the first one would leave the caller
@@ -57,7 +57,7 @@ export async function listLocalPage(input: LocalListInput): Promise<Page<Storage
57
57
  'local',
58
58
  prefix,
59
59
  error,
60
- `make the disk root readable by this process, then retry: ls -ld ${renderFixShellArg(root, "'<the disk root>'")}`,
60
+ `ls -ld ${renderFixShellArg(root, "'<the disk root>'")} # make the disk root readable by this process, then retry`,
61
61
  );
62
62
  }
63
63
  keys.sort();
@@ -0,0 +1,112 @@
1
+ // Single responsibility: the local disk's PINNED read — the file opened and its revision checked
2
+ // against the etag, at the call and again at the first pull, so the bytes streamed are that
3
+ // revision's or the read refuses.
4
+
5
+ // why: Bun has no handle API — `Bun.file(path)` reopens the path at every read, which is the race
6
+ // this file exists to close, and `Bun.file(fd)` has no positioned read nor a close.
7
+ import { type FileHandle, open } from 'node:fs/promises';
8
+ import { etagOfFile, headObject, refusedRead } from './driver-local-read';
9
+ import { objectNotFound } from './errors';
10
+ import { revisionChanged } from './errors-stream';
11
+ import { type ByteRange, streamedRange } from './range';
12
+
13
+ /** What one pull reads: big enough to amortise a syscall, small enough to never matter. */
14
+ const CHUNK_BYTES = 256 * 1024;
15
+
16
+ export interface PinnedRead {
17
+ readonly root: string;
18
+ readonly key: string;
19
+ readonly disk: string;
20
+ readonly etag: string;
21
+ readonly range?: ByteRange | undefined;
22
+ }
23
+
24
+ /** Closed, whatever the close says: a read that has its answer does not trade it for a close error. */
25
+ const closeQuietly = async (handle: FileHandle | undefined): Promise<void> => {
26
+ try {
27
+ await handle?.close();
28
+ } catch {
29
+ // Already closed, or the OS refused: either way the descriptor is no longer this read's.
30
+ }
31
+ };
32
+
33
+ /**
34
+ * One open file, proved to be revision `etag`: opened FIRST, then described by path. A write landing
35
+ * in between leaves the handle on the old bytes and the description on the new ones, so it refuses
36
+ * — never serves a mismatch. A commit writes the sidecar before it renames the bytes, so a handle
37
+ * on new bytes always meets a description at least that new.
38
+ */
39
+ async function openChecked(read: PinnedRead): Promise<{ handle: FileHandle; size: number }> {
40
+ const { root, key, disk, etag } = read;
41
+ const handle = await open(`${root}/${key}`, 'r').catch(refusedRead(root, key, disk));
42
+ try {
43
+ const entry = await headObject(root, key, () => etagOfFile(root, key, disk));
44
+ if (entry === undefined) throw objectNotFound(disk, key);
45
+ if (entry.etag !== etag) throw revisionChanged(disk, key, etag);
46
+ return { handle, size: (await handle.stat()).size };
47
+ } catch (error) {
48
+ await closeQuietly(handle);
49
+ throw error;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * The object at `key` as revision `etag`, or `X_STORAGE_OBJECT_CHANGED`; `range` as `streamRange`.
55
+ * Checked NOW and closed again, so a stream nobody reads holds no descriptor; the first pull reopens
56
+ * and checks the ETAG again. Never the inode alone: ext4 hands a freed inode straight back, so two
57
+ * puts left v3 at v1's inode number and it was served under v1's etag.
58
+ */
59
+ export async function streamPinned(read: PinnedRead): Promise<ReadableStream<Uint8Array>> {
60
+ const { key, range } = read;
61
+ const checked = await openChecked(read);
62
+ await closeQuietly(checked.handle);
63
+ const { size } = checked;
64
+ const [first, last] =
65
+ range === undefined ? [0, size - 1] : streamedRange('local', key, range, size);
66
+ // The same etag is the same bytes, so the same size: the etag check is the whole proof.
67
+ const reopen = async (): Promise<FileHandle> => (await openChecked(read)).handle;
68
+ return streamOf(reopen, first, last + 1);
69
+ }
70
+
71
+ /** `[first, end)` off one handle opened at the first pull, closed on the last byte, a cancel or a failure. */
72
+ function streamOf(
73
+ opener: () => Promise<FileHandle>,
74
+ first: number,
75
+ end: number,
76
+ ): ReadableStream<Uint8Array> {
77
+ let position = first;
78
+ let handle: FileHandle | undefined;
79
+ const close = async (): Promise<void> => {
80
+ await closeQuietly(handle);
81
+ handle = undefined;
82
+ };
83
+ return new ReadableStream<Uint8Array>(
84
+ {
85
+ async pull(controller) {
86
+ try {
87
+ const want = Math.min(CHUNK_BYTES, end - position);
88
+ if (want <= 0) {
89
+ await close();
90
+ return controller.close();
91
+ }
92
+ handle ??= await opener();
93
+ const buffer = new Uint8Array(want);
94
+ const { bytesRead } = await handle.read(buffer, 0, want, position);
95
+ if (bytesRead === 0) {
96
+ await close();
97
+ return controller.close();
98
+ }
99
+ position += bytesRead;
100
+ controller.enqueue(bytesRead === want ? buffer : buffer.subarray(0, bytesRead));
101
+ } catch (error) {
102
+ await close();
103
+ controller.error(error);
104
+ }
105
+ },
106
+ cancel: close,
107
+ },
108
+ // Pulled only when read: the default high-water mark of 1 pulls once at construction, which
109
+ // opened a descriptor for a body nobody would ever read (a 304, a dropped client).
110
+ { highWaterMark: 0 },
111
+ );
112
+ }
@@ -33,7 +33,7 @@ async function pendingEtag(root: string, key: string): Promise<string | undefine
33
33
  * that is simply gone — deleted between the `exists()` and the read — is "not found", as it
34
34
  * would have been one call earlier.
35
35
  */
36
- const refusedRead =
36
+ export const refusedRead =
37
37
  (root: string, key: string, disk: string) =>
38
38
  (error: unknown): never => {
39
39
  if (isStorageError(error)) throw error;
@@ -76,6 +76,13 @@ export const discardStaged = async (path: string): Promise<void> => {
76
76
  .catch(() => undefined);
77
77
  };
78
78
 
79
+ /**
80
+ * What the writer holds before a flush: one per MiB, not one per chunk, so a 64 KiB-chunk sender
81
+ * pays a sixteenth of the syscalls and backpressure still holds at a MiB of heap. Measured
82
+ * 2026-11-09, 512 MiB in 64 KiB chunks: 2.72 s → 2.53 s (the hash is most of the rest).
83
+ */
84
+ export const LOCAL_FLUSH_BYTES = 1024 * 1024;
85
+
79
86
  /** Backpressure is the `flush`: awaiting it is what keeps a fast sender from filling the heap. */
80
87
  export async function stageStream(input: StageStreamInput): Promise<StagedStream> {
81
88
  const { root, key, disk, body, maxBytes } = input;
@@ -84,12 +91,16 @@ export async function stageStream(input: StageStreamInput): Promise<StagedStream
84
91
  let sink: ReturnType<ReturnType<typeof Bun.file>['writer']> | undefined;
85
92
  try {
86
93
  await mkdir(`${root}/${STAGING_DIR}`, { recursive: true });
87
- const open = Bun.file(path).writer();
94
+ const open = Bun.file(path).writer({ highWaterMark: LOCAL_FLUSH_BYTES });
88
95
  sink = open;
89
- const size = await pumpBounded(body, streamLimit('local', key, maxBytes), async (chunk) => {
96
+ let buffered = 0;
97
+ const size = await pumpBounded(body, streamLimit('local', key, maxBytes), (chunk) => {
90
98
  hasher.update(chunk);
91
99
  open.write(chunk);
92
- await open.flush();
100
+ buffered += chunk.byteLength;
101
+ if (buffered < LOCAL_FLUSH_BYTES) return;
102
+ buffered = 0;
103
+ return Promise.resolve(open.flush()).then(() => undefined);
93
104
  });
94
105
  await open.end();
95
106
  sink = undefined;