@adonisjs/bodyparser
Version:
AdonisJs body parser to read and parse HTTP request bodies
208 lines (207 loc) • 7.07 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PartHandler = void 0;
/// <reference path="../../adonis-typings/index.ts" />
const path_1 = require("path");
const utils_1 = require("@poppinss/utils");
const File_1 = require("./File");
const utils_2 = require("../utils");
/**
* Part handler handles the progress of a stream and also internally validates
* it's size and extension.
*
* This class offloads the task of validating a file stream, regardless of how
* the stream is consumed. For example:
*
* In classic scanerio, we will process the file stream and write files to the
* tmp directory and in more advanced cases, the end user can handle the
* stream by themselves and report each chunk to this class.
*/
class PartHandler {
constructor(part, options, drive) {
this.part = part;
this.options = options;
this.drive = drive;
/**
* Creating a new file object for each part inside the multipart
* form data
*/
this.file = new File_1.File({
clientName: this.part.filename,
fieldName: this.part.name,
headers: this.part.headers,
}, {
size: this.options.size,
extnames: this.options.extnames,
}, this.drive);
/**
* A boolean to know, if we have emitted the error event after one or
* more validation errors. We need this flag, since the race conditions
* between `data` and `error` events will trigger multiple `error`
* emit.
*/
this.emittedValidationError = false;
}
/**
* A boolean to know if we can use the magic number to detect the file type. This is how it
* works.
*
* - We begin by extracting the file extension from the file name
* - If the file has no extension, we try to inspect the buffer
* - If the extension is something we support via magic numbers, then we ignore the extension
* and inspect the buffer
* - Otherwise, we have no other option than to trust the extension
*
* Think of this as using the optimal way for validating the file type
*/
get canFileTypeBeDetected() {
const fileExtension = (0, path_1.extname)(this.part.filename).replace(/^\./, '');
return fileExtension ? utils_2.supportMagicFileTypes.has(fileExtension) : true;
}
/**
* Detects the file type and extension and also validates it when validations
* are not deferred.
*/
async detectFileTypeAndExtension() {
if (!this.buff) {
return;
}
const fileType = this.canFileTypeBeDetected
? await (0, utils_2.getFileType)(this.buff)
: (0, utils_2.computeFileTypeFromName)(this.file.clientName, this.file.headers);
if (fileType) {
this.file.extname = fileType.ext;
this.file.type = fileType.type;
this.file.subtype = fileType.subtype;
}
}
/**
* Skip the stream or end it forcefully. This is invoked when the
* streaming consumer reports an error
*/
skipEndStream() {
this.part.emit('close');
}
/**
* Finish the process of listening for any more events and mark the
* file state as consumed.
*/
finish() {
this.file.state = 'consumed';
if (!this.options.deferValidations) {
this.file.validate();
}
}
/**
* Start the process the updating the file state
* to streaming mode.
*/
begin() {
this.file.state = 'streaming';
}
/**
* Handles the file upload progress by validating the file size and
* extension.
*/
async reportProgress(line, bufferLength) {
/**
* Do not consume stream data when file state is not `streaming`. Stream
* events race conditions may emit the `data` event after the `error`
* event in some cases, so we have to restrict it here.
*/
if (this.file.state !== 'streaming') {
return;
}
/**
* Detect the file type and extension when extname is null, otherwise
* empty out the buffer. We only need the buffer to find the
* file extension from it's content.
*/
if (this.file.extname === undefined) {
this.buff = this.buff ? Buffer.concat([this.buff, line]) : line;
await this.detectFileTypeAndExtension();
}
else {
this.buff = undefined;
}
/**
* The length of stream buffer
*/
this.file.size = this.file.size + bufferLength;
/**
* Validate the file on every chunk, unless validations have been deferred.
*/
if (this.options.deferValidations) {
return;
}
/**
* Attempt to validate the file after every chunk and report error
* when it has one or more failures. After this the consumer must
* call `reportError`.
*/
this.file.validate();
if (!this.file.isValid && !this.emittedValidationError) {
this.emittedValidationError = true;
this.part.emit('error', new utils_1.Exception('one or more validations failed', 400, 'E_STREAM_VALIDATION_FAILURE'));
}
}
/**
* Report errors encountered while processing the stream. These can be errors
* apart from the one reported by this class. For example: The `s3` failure
* due to some bad credentails.
*/
async reportError(error) {
if (this.file.state !== 'streaming') {
return;
}
this.skipEndStream();
this.finish();
if (error.code === 'E_STREAM_VALIDATION_FAILURE') {
return;
}
/**
* Push to the array of file errors
*/
this.file.errors.push({
fieldName: this.file.fieldName,
clientName: this.file.clientName,
type: 'fatal',
message: error.message,
});
}
/**
* Report success data about the file.
*/
async reportSuccess(data) {
if (this.file.state !== 'streaming') {
return;
}
/**
* Re-attempt to detect the file extension after we are done
* consuming the stream
*/
if (this.file.extname === undefined) {
await this.detectFileTypeAndExtension();
}
if (data) {
const { filePath, tmpPath, ...meta } = data;
if (filePath) {
this.file.filePath = filePath;
}
if (tmpPath) {
this.file.tmpPath = tmpPath;
}
this.file.meta = meta || {};
}
this.finish();
}
}
exports.PartHandler = PartHandler;