UNPKG

@adonisjs/bodyparser

Version:

AdonisJs body parser to read and parse HTTP request bodies

305 lines (304 loc) 10.5 kB
"use strict"; /* * @adonisjs/bodyparser * * (c) Harminder Virk <virk@adonisjs.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Multipart = void 0; /// <reference path="../../adonis-typings/bodyparser.ts" /> const bytes_1 = __importDefault(require("bytes")); const utils_1 = require("@poppinss/utils"); const multiparty_1 = __importDefault(require("@poppinss/multiparty")); const FormFields_1 = require("../FormFields"); const PartHandler_1 = require("./PartHandler"); /** * Multipart class offers a low level API to interact the incoming * HTTP request data as a stream. This makes it super easy to * write files to s3 without saving them to the disk first. */ class Multipart { constructor(ctx, config = {}, drive) { this.ctx = ctx; this.config = config; this.drive = drive; /** * The registered handlers to handle the file uploads */ this.handlers = {}; /** * Collected fields from the multipart stream */ this.fields = new FormFields_1.FormFields({ convertEmptyStringsToNull: this.config.convertEmptyStringsToNull === true, }); /** * Collected files from the multipart stream. Files are only collected * when there is an attached listener for a given file. */ this.files = new FormFields_1.FormFields({ convertEmptyStringsToNull: this.config.convertEmptyStringsToNull === true, }); /** * We track the finishing of `this.onFile` async handlers * to make sure that `process` promise resolves for all * handlers to finish. */ this.pendingHandlers = 0; /** * A track of total number of file bytes processed so far */ this.processedBytes = 0; /** * The current state of the multipart form handler */ this.state = 'idle'; } /** * Returns a boolean telling whether all streams have been * consumed along with all handlers execution */ isClosed() { return this.form['flushing'] <= 0 && this.pendingHandlers <= 0; } /** * Removes array like expression from the part name to * find the handler */ getHandlerName(name) { return name.replace(/\[\d*\]/, ''); } /** * Validates and returns an error when upper limit is defined and * processed bytes is over the upper limit */ validateProcessedBytes(chunkLength) { if (!this.upperLimit) { return; } this.processedBytes += chunkLength; if (this.processedBytes > this.upperLimit) { return new utils_1.Exception('request entity too large', 413, 'E_REQUEST_ENTITY_TOO_LARGE'); } } /** * Handles a given part by invoking it's handler or * by resuming the part, if there is no defined * handler */ async handlePart(part) { /** * Skip parts with empty name or empty filenames. The empty * filenames takes place when user doesn't upload a file * and empty name is more of a bad client scanerio. */ if (!part.name || !part.filename) { part.resume(); return; } const name = this.getHandlerName(part.name); /** * Skip, if their is no handler to consume the part. */ const handler = this.handlers[name] || this.handlers['*']; if (!handler) { part.resume(); return; } this.pendingHandlers++; /** * Instantiate the part handler */ const partHandler = new PartHandler_1.PartHandler(part, handler.options, this.drive); partHandler.begin(); /** * Track the file instance created by the part handler. The end user * must be able to access these files. */ this.files.add(partHandler.file.fieldName, partHandler.file); part.file = partHandler.file; try { const response = await handler.handler(part, async (line) => { if (this.state !== 'processing') { return; } const lineLength = line.length; /** * Keeping an eye on total bytes processed so far and shortcircuit * request when more than expected bytes have been received. */ const error = this.validateProcessedBytes(lineLength); if (error) { part.emit('error', error); this.abort(error); return; } try { await partHandler.reportProgress(line, lineLength); } catch (err) { this.ctx.logger.fatal('Unhandled multipart stream error. Make sure to handle "error" events for all manually processed streams'); } }); /** * Stream consumed successfully */ await partHandler.reportSuccess(response || {}); } catch (error) { /** * The stream handler reported an exception */ await partHandler.reportError(error); } this.pendingHandlers--; } /** * Record the fields inside multipart contract */ handleField(key, value) { if (!key) { return; } this.fields.add(key, value); } /** * Processes the user config and computes the `upperLimit` value from * it. */ processConfig(config) { this.config = Object.assign(this.config, config); /** * Getting bytes from the `config.fieldsLimit` option, which can * also be a string. */ this.maxFieldsSize = typeof this.config.fieldsLimit === 'string' ? (0, bytes_1.default)(this.config.fieldsLimit) : this.config.fieldsLimit; /** * Getting bytes from the `config.limit` option, which can * also be a string */ this.upperLimit = typeof this.config.limit === 'string' ? (0, bytes_1.default)(this.config.limit) : this.config.limit; } /** * Mark the process as finished */ finish(newState) { if (this.state === 'idle' || this.state === 'processing') { this.state = newState; this.ctx.request['__raw_files'] = this.files.get(); this.ctx.request.setInitialBody(this.fields.get()); } } /** * Attach handler for a given file. To handle all files, you * can attach a wildcard handler. * * @example * ```ts * multipart.onFile('package', {}, async (stream) => { * }) * * multipart.onFile('*', {}, async (stream) => { * }) * ``` */ onFile(name, options, handler) { this.handlers[name] = { handler, options }; return this; } /** * Abort request by emitting error */ abort(error) { this.form.emit('error', error); } /** * Process the request by going all the file and field * streams. */ process(config) { return new Promise((resolve, reject) => { if (this.state !== 'idle') { reject(new utils_1.Exception('multipart stream has already been consumed', 500, 'E_RUNTIME_EXCEPTION')); return; } this.state = 'processing'; this.processConfig(config); this.form = new multiparty_1.default.Form({ maxFields: this.config.maxFields, maxFieldsSize: this.maxFieldsSize, }); /** * Raise error when form encounters an * error */ this.form.on('error', (error) => { this.finish('error'); process.nextTick(() => { if (this.ctx.request.request.readable) { this.ctx.request.request.resume(); } if (error.message.match(/maxFields [0-9]+ exceeded/)) { reject(new utils_1.Exception('Fields length limit exceeded', 413, 'E_REQUEST_ENTITY_TOO_LARGE')); } else if (error.message.match(/maxFieldsSize [0-9]+ exceeded/)) { reject(new utils_1.Exception('Fields size in bytes exceeded', 413, 'E_REQUEST_ENTITY_TOO_LARGE')); } else { reject(error); } }); }); /** * Process each part at a time and also resolve the * promise when all parts are consumed and processed * by their handlers */ this.form.on('part', async (part) => { await this.handlePart(part); /** * When a stream finishes before the handler, the close `event` * will not resolve the current Promise. So in that case, we * check and resolve from here */ if (this.isClosed()) { this.finish('success'); resolve(); } }); /** * Listen for fields */ this.form.on('field', (key, value) => { try { this.handleField(key, value); } catch (error) { this.abort(error); } }); /** * Resolve promise on close, when all internal * file handlers are done processing files */ this.form.on('close', () => { if (this.isClosed()) { this.finish('success'); resolve(); } }); this.form.parse(this.ctx.request.request); }); } } exports.Multipart = Multipart;