hono-upload
Version:
A memory efficient upload handler for hono.
1 lines • 5.66 kB
Source Map (JSON)
{"version":3,"sources":["../../src/index.ts"],"sourcesContent":["import type { FileInfo } from 'busboy';\nimport type { Context } from 'hono';\nimport type { Readable } from 'stream';\n\nimport createBusboy from 'busboy';\n\ntype UploadFileInfo = {\n filename: string;\n encoding: string;\n mimeType: string;\n field: string;\n};\n\nexport type UploadParams<T, Ctx extends Context> = {\n onFile: (file: Readable, fileInfo: UploadFileInfo) => T | Promise<T>;\n queuingStrategy?: QueuingStrategy<Uint8Array>;\n ctx: Ctx;\n maxFileSize?: number;\n};\n\nexport const uploadErrors = {\n MISSING_BODY: 'MISSING_BODY',\n INVALID_CONTENT_TYPE: 'INVALID_CONTENT_TYPE',\n CONTENT_LENGTH_MISSING: 'CONTENT_LENGTH_MISSING',\n MAX_FILE_SIZE_EXCEEDED: 'MAX_FILE_SIZE_EXCEEDED',\n MAX_ONE_FILE_ALLOWED: 'MAX_ONE_FILE_ALLOWED',\n};\n\nexport async function uploadHandler<T, C extends Context>({\n queuingStrategy,\n onFile,\n ctx,\n ...params\n}: UploadParams<T, C>) {\n const request = ctx.req.raw;\n const stream = request.body;\n\n if (!stream) {\n throw new Error(uploadErrors.MISSING_BODY);\n }\n\n const contentType = request.headers.get('Content-Type') || '';\n if (!contentType.includes('multipart/form-data')) {\n throw new Error(uploadErrors.INVALID_CONTENT_TYPE);\n }\n\n // maxFileSize + 2MB\n const maxFileSize = (params.maxFileSize || Infinity) + 2 * 1024 * 1024;\n\n const requestSize = parseInt(request.headers.get('Content-Length') || '0', 10);\n if (requestSize === 0) {\n throw new Error(uploadErrors.CONTENT_LENGTH_MISSING);\n }\n if (maxFileSize && requestSize > maxFileSize) {\n throw new Error(uploadErrors.MAX_FILE_SIZE_EXCEEDED, {\n cause: new Error(requestSize.toString()),\n });\n }\n\n const headers = Object.fromEntries(ctx.req.raw.headers.entries());\n\n const queueStrategy = {\n highWaterMark: 3,\n size: () => 1,\n ...queuingStrategy,\n };\n\n return new Promise<T>((resolve, reject) => {\n const busboy = createBusboy({\n headers,\n highWaterMark: queueStrategy.highWaterMark,\n limits: {\n fields: 0,\n files: 1,\n },\n });\n\n const writableStream = new WritableStream<Uint8Array>(\n {\n abort(reason) {\n reject(reason);\n },\n write(chunk) {\n busboy.write(chunk);\n },\n close() {\n busboy.end();\n },\n },\n queueStrategy,\n );\n\n function cleanup() {\n busboy.removeListener('error', onBusboyError); // eslint-disable-line no-use-before-define\n busboy.removeListener('file', onBusboyFile); // eslint-disable-line no-use-before-define\n }\n\n let result: T | undefined;\n\n function onBusboyError() {\n result = undefined;\n cleanup();\n }\n\n function onBusboyFile(field: string, file: Readable, fileInfo: FileInfo) {\n try {\n const writeFile = onFile(file, {\n ...fileInfo,\n field,\n });\n if (writeFile instanceof Promise) {\n writeFile\n .then((v) => {\n result = v;\n busboy.emit('close');\n })\n .catch((err) => {\n reject(err);\n });\n } else {\n result = writeFile;\n }\n } catch (e) {\n reject(e);\n }\n }\n\n busboy.on('file', onBusboyFile);\n busboy.on('error', onBusboyError);\n busboy.on('close', () => {\n writableStream.close().catch(() => {});\n cleanup();\n if (result) resolve(result);\n });\n\n stream.pipeTo(writableStream).catch((e) => {\n reject(e);\n });\n });\n}\n"],"mappings":"AAIA,OAAO,kBAAkB;AAgBlB,MAAM,eAAe;AAAA,EAC1B,cAAc;AAAA,EACd,sBAAsB;AAAA,EACtB,wBAAwB;AAAA,EACxB,wBAAwB;AAAA,EACxB,sBAAsB;AACxB;AAEA,eAAsB,cAAoC;AAAA,EACxD;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAuB;AACrB,QAAM,UAAU,IAAI,IAAI;AACxB,QAAM,SAAS,QAAQ;AAEvB,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,aAAa,YAAY;AAAA,EAC3C;AAEA,QAAM,cAAc,QAAQ,QAAQ,IAAI,cAAc,KAAK;AAC3D,MAAI,CAAC,YAAY,SAAS,qBAAqB,GAAG;AAChD,UAAM,IAAI,MAAM,aAAa,oBAAoB;AAAA,EACnD;AAGA,QAAM,eAAe,OAAO,eAAe,YAAY,IAAI,OAAO;AAElE,QAAM,cAAc,SAAS,QAAQ,QAAQ,IAAI,gBAAgB,KAAK,KAAK,EAAE;AAC7E,MAAI,gBAAgB,GAAG;AACrB,UAAM,IAAI,MAAM,aAAa,sBAAsB;AAAA,EACrD;AACA,MAAI,eAAe,cAAc,aAAa;AAC5C,UAAM,IAAI,MAAM,aAAa,wBAAwB;AAAA,MACnD,OAAO,IAAI,MAAM,YAAY,SAAS,CAAC;AAAA,IACzC,CAAC;AAAA,EACH;AAEA,QAAM,UAAU,OAAO,YAAY,IAAI,IAAI,IAAI,QAAQ,QAAQ,CAAC;AAEhE,QAAM,gBAAgB;AAAA,IACpB,eAAe;AAAA,IACf,MAAM,MAAM;AAAA,IACZ,GAAG;AAAA,EACL;AAEA,SAAO,IAAI,QAAW,CAAC,SAAS,WAAW;AACzC,UAAM,SAAS,aAAa;AAAA,MAC1B;AAAA,MACA,eAAe,cAAc;AAAA,MAC7B,QAAQ;AAAA,QACN,QAAQ;AAAA,QACR,OAAO;AAAA,MACT;AAAA,IACF,CAAC;AAED,UAAM,iBAAiB,IAAI;AAAA,MACzB;AAAA,QACE,MAAM,QAAQ;AACZ,iBAAO,MAAM;AAAA,QACf;AAAA,QACA,MAAM,OAAO;AACX,iBAAO,MAAM,KAAK;AAAA,QACpB;AAAA,QACA,QAAQ;AACN,iBAAO,IAAI;AAAA,QACb;AAAA,MACF;AAAA,MACA;AAAA,IACF;AAEA,aAAS,UAAU;AACjB,aAAO,eAAe,SAAS,aAAa;AAC5C,aAAO,eAAe,QAAQ,YAAY;AAAA,IAC5C;AAEA,QAAI;AAEJ,aAAS,gBAAgB;AACvB,eAAS;AACT,cAAQ;AAAA,IACV;AAEA,aAAS,aAAa,OAAe,MAAgB,UAAoB;AACvE,UAAI;AACF,cAAM,YAAY,OAAO,MAAM;AAAA,UAC7B,GAAG;AAAA,UACH;AAAA,QACF,CAAC;AACD,YAAI,qBAAqB,SAAS;AAChC,oBACG,KAAK,CAAC,MAAM;AACX,qBAAS;AACT,mBAAO,KAAK,OAAO;AAAA,UACrB,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,mBAAO,GAAG;AAAA,UACZ,CAAC;AAAA,QACL,OAAO;AACL,mBAAS;AAAA,QACX;AAAA,MACF,SAAS,GAAG;AACV,eAAO,CAAC;AAAA,MACV;AAAA,IACF;AAEA,WAAO,GAAG,QAAQ,YAAY;AAC9B,WAAO,GAAG,SAAS,aAAa;AAChC,WAAO,GAAG,SAAS,MAAM;AACvB,qBAAe,MAAM,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AACrC,cAAQ;AACR,UAAI;AAAQ,gBAAQ,MAAM;AAAA,IAC5B,CAAC;AAED,WAAO,OAAO,cAAc,EAAE,MAAM,CAAC,MAAM;AACzC,aAAO,CAAC;AAAA,IACV,CAAC;AAAA,EACH,CAAC;AACH;","names":[]}