@mjackson/form-data-parser
Version:
A request.formData() wrapper with streaming file upload handling
132 lines (115 loc) • 4 kB
text/typescript
import type { MultipartParserOptions } from '@mjackson/multipart-parser';
import {
isMultipartRequest,
parseMultipartRequest,
MultipartPart,
} from '@mjackson/multipart-parser';
/**
* The base class for errors thrown by the form data parser.
*/
export class FormDataParseError extends Error {
constructor(message: string) {
super(message);
this.name = 'FormDataParseError';
}
}
/**
* An error thrown when the maximum number of files allowed in a request is exceeded.
*/
export class MaxFilesExceededError extends FormDataParseError {
constructor(maxFiles: number) {
super(`Maximum number of files exceeded: ${maxFiles}`);
this.name = 'MaxFilesExceededError';
}
}
/**
* A file that was uploaded as part of a `multipart/form-data` request.
*/
export class FileUpload extends File {
/**
* The name of the <input> field used to upload the file.
*/
readonly fieldName: string;
constructor(part: MultipartPart, fieldName: string) {
super(part.content, part.filename ?? 'file-upload', {
type: part.mediaType ?? 'application/octet-stream',
});
this.fieldName = fieldName;
}
}
/**
* A function used for handling file uploads.
*/
export interface FileUploadHandler {
(file: FileUpload): void | null | string | Blob | Promise<void | null | string | Blob>;
}
async function defaultFileUploadHandler(file: FileUpload): Promise<File> {
// By default just keep the file around in memory.
return file;
}
export interface ParseFormDataOptions extends MultipartParserOptions {
/**
* The maximum number of files that can be uploaded in a single request.
* If this limit is exceeded, a `MaxFilesExceededError` will be thrown.
*
* Default: 20
*/
maxFiles?: number;
}
/**
* Parses a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) body into a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
* object. This is useful when accessing the data contained in a HTTP `multipart/form-data` request
* generated by a HTML `<form>` element.
*
* This is a drop-in replacement for [the built-in `request.formData()` API](https://developer.mozilla.org/en-US/docs/Web/API/Request/formData)
* with the main difference being the ability to customize the handling of file uploads. Instead of
* keeping all files in memory, the `uploadHandler` allows you to store the file on disk or a
* cloud storage service.
*
* @param request The `Request` object to parse
* @param uploadHandler A function that handles file uploads. It receives a `File` object and may return any value that is valid in a `FormData` object
* @return A `Promise` that resolves to a `FormData` object containing the parsed data
*/
export async function parseFormData(
request: Request,
uploadHandler?: FileUploadHandler,
): Promise<FormData>;
export async function parseFormData(
request: Request,
options: ParseFormDataOptions,
uploadHandler?: FileUploadHandler,
): Promise<FormData>;
export async function parseFormData(
request: Request,
options?: ParseFormDataOptions | FileUploadHandler,
uploadHandler: FileUploadHandler = defaultFileUploadHandler,
): Promise<FormData> {
if (typeof options === 'function') {
uploadHandler = options;
options = {};
} else if (options == null) {
options = {};
}
if (!isMultipartRequest(request)) {
return request.formData();
}
let { maxFiles = 20, ...parserOptions } = options;
let formData = new FormData();
let fileCount = 0;
for await (let part of parseMultipartRequest(request, parserOptions)) {
let fieldName = part.name;
if (!fieldName) continue;
if (part.isFile) {
if (++fileCount > maxFiles) {
throw new MaxFilesExceededError(maxFiles);
}
let value = await uploadHandler(new FileUpload(part, fieldName));
if (value != null) {
formData.append(fieldName, value);
}
} else {
formData.append(fieldName, part.text);
}
}
return formData;
}