@adonisjs/bodyparser
Version:
AdonisJs body parser to read and parse HTTP request bodies
305 lines (304 loc) • 10.5 kB
JavaScript
"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;