Skip to content

Commit 4fc4980

Browse files
committed
update multi-part to support streaming
1 parent 1149255 commit 4fc4980

File tree

2 files changed

+70
-38
lines changed

2 files changed

+70
-38
lines changed

packages/multipart-parser/src/lib/multipart.node.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ import { getMultipartBoundary } from './multipart-request.ts'
1919
* @param options Options for the parser
2020
* @return A generator yielding `MultipartPart` objects
2121
*/
22-
export function* parseMultipart(
22+
export async function* parseMultipart(
2323
message: Buffer | Iterable<Buffer>,
2424
options: ParseMultipartOptions,
25-
): Generator<MultipartPart, void, unknown> {
25+
): AsyncGenerator<MultipartPart, void, unknown> {
2626
yield* parseMultipartWeb(message as Uint8Array | Iterable<Uint8Array>, options)
2727
}
2828

packages/multipart-parser/src/lib/multipart.ts

Lines changed: 68 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ export interface ParseMultipartOptions {
5454
* Default: 2 MiB
5555
*/
5656
maxFileSize?: number
57+
58+
useContentPart?: boolean
59+
onCreatePart?(part: MultipartPart): Promise<void> | void
5760
}
5861

5962
/**
@@ -66,10 +69,10 @@ export interface ParseMultipartOptions {
6669
* @param options Options for the parser
6770
* @return A generator that yields `MultipartPart` objects
6871
*/
69-
export function* parseMultipart(
72+
export async function* parseMultipart(
7073
message: Uint8Array | Iterable<Uint8Array>,
7174
options: ParseMultipartOptions,
72-
): Generator<MultipartPart, void, unknown> {
75+
): AsyncGenerator<MultipartPart, void, unknown> {
7376
let parser = new MultipartParser(options.boundary, {
7477
maxHeaderSize: options.maxHeaderSize,
7578
maxFileSize: options.maxFileSize,
@@ -152,6 +155,9 @@ export class MultipartParser {
152155
#currentPart: MultipartPart | null = null
153156
#contentLength = 0
154157

158+
#useContentPart: MultipartParserOptions['useContentPart']
159+
#onCreatePart: MultipartParserOptions['onCreatePart']
160+
155161
constructor(boundary: string, options?: MultipartParserOptions) {
156162
this.boundary = boundary
157163
this.maxHeaderSize = options?.maxHeaderSize ?? 8 * oneKb
@@ -162,6 +168,9 @@ export class MultipartParser {
162168
this.#findBoundary = createSearch(`\r\n--${boundary}`)
163169
this.#findPartialTailBoundary = createPartialTailSearch(`\r\n--${boundary}`)
164170
this.#boundaryLength = 4 + boundary.length // length of '\r\n--' + boundary
171+
172+
this.#onCreatePart = options?.onCreatePart
173+
this.#useContentPart = options?.useContentPart ?? true
165174
}
166175

167176
/**
@@ -170,7 +179,7 @@ export class MultipartParser {
170179
* @param chunk A chunk of data to write to the parser
171180
* @return A generator yielding `MultipartPart` objects as they are parsed
172181
*/
173-
*write(chunk: Uint8Array): Generator<MultipartPart, void, unknown> {
182+
async *write(chunk: Uint8Array): AsyncGenerator<MultipartPart, void, unknown> {
174183
if (this.#state === MultipartParserStateDone) {
175184
throw new MultipartParseError('Unexpected data after end of stream')
176185
}
@@ -201,16 +210,16 @@ export class MultipartParser {
201210
let partialTailIndex = this.#findPartialTailBoundary(chunk)
202211

203212
if (partialTailIndex === -1) {
204-
this.#append(index === 0 ? chunk : chunk.subarray(index))
213+
await this.#append(index === 0 ? chunk : chunk.subarray(index))
205214
} else {
206-
this.#append(chunk.subarray(index, partialTailIndex))
215+
await this.#append(chunk.subarray(index, partialTailIndex))
207216
this.#buffer = chunk.subarray(partialTailIndex)
208217
}
209218

210219
break
211220
}
212221

213-
this.#append(chunk.subarray(index, boundaryIndex))
222+
await this.#append(chunk.subarray(index, boundaryIndex))
214223

215224
yield this.#currentPart!
216225

@@ -256,13 +265,19 @@ export class MultipartParser {
256265
throw new MaxHeaderSizeExceededError(this.maxHeaderSize)
257266
}
258267

259-
this.#currentPart = new MultipartPart(chunk.subarray(index, headerEndIndex), [])
268+
const header = chunk.subarray(index, headerEndIndex);
269+
this.#currentPart = this.#useContentPart
270+
? new MultipartContentPart(header, [])
271+
: new MultipartPart(header)
272+
260273
this.#contentLength = 0
261274

262275
index = headerEndIndex + 4 // Skip header + \r\n\r\n
263276

264277
this.#state = MultipartParserStateBody
265278

279+
await this.#onCreatePart?.(this.#currentPart)
280+
266281
continue
267282
}
268283

@@ -283,12 +298,12 @@ export class MultipartParser {
283298
}
284299
}
285300

286-
#append(chunk: Uint8Array): void {
301+
async #append(chunk: Uint8Array): Promise<void> {
287302
if (this.#contentLength + chunk.length > this.maxFileSize) {
288303
throw new MaxFileSizeExceededError(this.maxFileSize)
289304
}
290305

291-
this.#currentPart!.content.push(chunk)
306+
await this.#currentPart!.append(chunk)
292307
this.#contentLength += chunk.length
293308
}
294309

@@ -313,40 +328,16 @@ const decoder = new TextDecoder('utf-8', { fatal: true })
313328
* A part of a `multipart/*` HTTP message.
314329
*/
315330
export class MultipartPart {
316-
/**
317-
* The raw content of this part as an array of `Uint8Array` chunks.
318-
*/
319-
readonly content: Uint8Array[]
320331

321332
#header: Uint8Array
322333
#headers?: Headers
323334

324-
constructor(header: Uint8Array, content: Uint8Array[]) {
335+
constructor(header: Uint8Array) {
325336
this.#header = header
326-
this.content = content
327337
}
328338

329-
/**
330-
* The content of this part as an `ArrayBuffer`.
331-
*/
332-
get arrayBuffer(): ArrayBuffer {
333-
return this.bytes.buffer as ArrayBuffer
334-
}
335-
336-
/**
337-
* The content of this part as a single `Uint8Array`. In `multipart/form-data` messages, this is useful
338-
* for reading the value of files that were uploaded using `<input type="file">` fields.
339-
*/
340-
get bytes(): Uint8Array {
341-
let buffer = new Uint8Array(this.size)
342-
343-
let offset = 0
344-
for (let chunk of this.content) {
345-
buffer.set(chunk, offset)
346-
offset += chunk.length
347-
}
348-
349-
return buffer
339+
async append(chunk: Uint8Array) {
340+
throw new Error("Not implemented. Please assign or override this method.");
350341
}
351342

352343
/**
@@ -395,6 +386,47 @@ export class MultipartPart {
395386
return this.headers.contentDisposition.name
396387
}
397388

389+
}
390+
391+
export class MultipartContentPart extends MultipartPart {
392+
393+
/**
394+
* The raw content of this part as an array of `Uint8Array` chunks.
395+
*/
396+
readonly content: Uint8Array[]
397+
398+
async append(chunk: Uint8Array): Promise<void> {
399+
this.content.push(chunk)
400+
}
401+
402+
constructor(header: Uint8Array, content: Uint8Array[]) {
403+
super(header);
404+
this.content = content
405+
}
406+
407+
/**
408+
* The content of this part as an `ArrayBuffer`.
409+
*/
410+
get arrayBuffer(): ArrayBuffer {
411+
return this.bytes.buffer as ArrayBuffer
412+
}
413+
414+
/**
415+
* The content of this part as a single `Uint8Array`. In `multipart/form-data` messages, this is useful
416+
* for reading the value of files that were uploaded using `<input type="file">` fields.
417+
*/
418+
get bytes(): Uint8Array {
419+
let buffer = new Uint8Array(this.size)
420+
421+
let offset = 0
422+
for (let chunk of this.content) {
423+
buffer.set(chunk, offset)
424+
offset += chunk.length
425+
}
426+
427+
return buffer
428+
}
429+
398430
/**
399431
* The size of the content in bytes.
400432
*/

0 commit comments

Comments
 (0)