This commit is contained in:
Tutur33
2023-11-24 22:35:41 +01:00
parent 3c0b507a93
commit 7644b2a0f7
45165 changed files with 4803356 additions and 3 deletions
+9
View File
@@ -0,0 +1,9 @@
# The MIT License
Copyright 2022 Harminder Virk, contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+55
View File
@@ -0,0 +1,55 @@
<div align="center">
<img src="https://res.cloudinary.com/adonisjs/image/upload/q_100/v1558612869/adonis-readme_zscycu.jpg" width="600px">
</div>
<br />
<div align="center">
<h3>BodyParser</h3>
<p>
BodyParser Middleware For AdonisJS with first class support for <strong>file uploads</strong>, <strong>JSON payloads</strong>, <strong>raw body</strong> and standard <strong>form submissions</strong>
</p>
</div>
<br />
<div align="center">
[![gh-workflow-image]][gh-workflow-url] [![typescript-image]][typescript-url] [![npm-image]][npm-url] [![license-image]][license-url] [![synk-image]][synk-url]
</div>
<div align="center">
<h3>
<a href="https://adonisjs.com">
Website
</a>
<span> | </span>
<a href="https://docs.adonisjs.com/guides/request#request-body">
Guides
</a>
<span> | </span>
<a href="CONTRIBUTING.md">
Contributing
</a>
</h3>
</div>
<div align="center">
<sub>Built with ❤︎ by <a href="https://twitter.com/AmanVirk1">Harminder Virk</a>
</div>
[gh-workflow-image]: https://img.shields.io/github/workflow/status/adonisjs/bodyparser/test?style=for-the-badge
[gh-workflow-url]: https://github.com/adonisjs/bodyparser/actions/workflows/test.yml "Github action"
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
[typescript-url]: "typescript"
[npm-image]: https://img.shields.io/npm/v/@adonisjs/bodyparser/alpha.svg?style=for-the-badge&logo=npm
[npm-url]: https://www.npmjs.com/package/@adonisjs/bodyparser/v/alpha "npm"
[license-image]: https://img.shields.io/npm/l/@adonisjs/bodyparser?color=blueviolet&style=for-the-badge
[license-url]: LICENSE.md "license"
[synk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/bodyparser?label=Synk%20Vulnerabilities&style=for-the-badge
[synk-url]: https://snyk.io/test/github/adonisjs/bodyparser?targetFile=package.json "synk"
@@ -0,0 +1,217 @@
/// <reference types="node" />
/// <reference types="node" />
declare module '@ioc:Adonis/Core/BodyParser' {
import { Readable } from 'stream';
import { DisksList, WriteOptions } from '@ioc:Adonis/Core/Drive';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
/**
* Qs module config
*/
type QueryStringConfig = {
depth?: number;
allowPrototypes?: boolean;
plainObjects?: boolean;
parameterLimit?: number;
arrayLimit?: number;
ignoreQueryPrefix?: boolean;
delimiter?: RegExp | string;
allowDots?: boolean;
charset?: string;
charsetSentinel?: boolean;
interpretNumericEntities?: boolean;
parseArrays?: boolean;
comma?: boolean;
};
/**
* Base config used by all types
*/
type BodyParserBaseConfig = {
encoding: string;
limit: string | number;
types: string[];
};
/**
* Body parser config for parsing JSON requests
*/
export type BodyParserJSONConfig = BodyParserBaseConfig & {
strict: boolean;
};
/**
* Parser config for parsing form data
*/
export type BodyParserFormConfig = BodyParserBaseConfig & {
queryString: QueryStringConfig;
convertEmptyStringsToNull: boolean;
};
/**
* Parser config for parsing raw body (untouched)
*/
export type BodyParserRawConfig = BodyParserBaseConfig & {
queryString: QueryStringConfig;
};
/**
* Parser config for parsing multipart requests
*/
export type BodyParserMultipartConfig = BodyParserBaseConfig & {
autoProcess: boolean;
maxFields: number;
processManually: string[];
convertEmptyStringsToNull: boolean;
fieldsLimit?: number | string;
tmpFileName?(): string;
};
/**
* Body parser config for all different types
*/
export type BodyParserConfig = {
whitelistedMethods: string[];
json: BodyParserJSONConfig;
form: BodyParserFormConfig;
raw: BodyParserRawConfig;
multipart: BodyParserMultipartConfig;
};
/**
* ------------------------------------
* Multipart related options
* ------------------------------------
*/
/**
* Readable stream along with some extra data. This is what
* is passed to `onFile` handlers.
*/
export type MultipartStream = Readable & {
headers: {
[key: string]: string;
};
name: string;
filename: string;
bytes: number;
file: MultipartFileContract;
};
/**
* The callback handler for a given file part
*/
export type PartHandler = (part: MultipartStream, reportChunk: (chunk: Buffer) => void) => Promise<({
filePath?: string;
tmpPath?: string;
} & {
[key: string]: any;
}) | void>;
/**
* Multipart class contract, since it is exposed on the request object,
* we need the interface to extend typings.
*/
export interface MultipartContract {
state: 'idle' | 'processing' | 'error' | 'success';
abort(error: any): void;
onFile(name: string, options: Partial<FileValidationOptions & {
deferValidations: boolean;
}>, callback: PartHandler): this;
process(config?: Partial<{
limit: string | number;
maxFields: number;
}>): Promise<void>;
}
/**
* ------------------------------------
* Multipart file related options
* ------------------------------------
*/
/**
* The options that can be used to validate a given
* file
*/
export type FileValidationOptions = {
size: string | number;
extnames: string[];
};
/**
* Error shape for file upload errors
*/
export type FileUploadError = {
fieldName: string;
clientName: string;
message: string;
type: 'size' | 'extname' | 'fatal';
};
/**
* Shape of file.toJSON return value
*/
export type FileJSON = {
fieldName: string;
clientName: string;
size: number;
filePath?: string;
fileName?: string;
type?: string;
extname?: string;
subtype?: string;
state: 'idle' | 'streaming' | 'consumed' | 'moved';
isValid: boolean;
validated: boolean;
errors: FileUploadError[];
meta: any;
};
/**
* Multipart file interface
*/
export interface MultipartFileContract {
isMultipartFile: true;
fieldName: string;
clientName: string;
size: number;
headers: {
[key: string]: string;
};
tmpPath?: string;
filePath?: string;
fileName?: string;
type?: string;
extname?: string;
subtype?: string;
state: 'idle' | 'streaming' | 'consumed' | 'moved';
isValid: boolean;
hasErrors: boolean;
validated: boolean;
errors: FileUploadError[];
sizeLimit?: number | string;
allowedExtensions?: string[];
meta: any;
/**
* Run validations on the file
*/
validate(): void;
/**
* Mark file as moved
*/
markAsMoved(fileName: string, filePath: string): void;
/**
* Move file from temporary path to a different location. Self consumed
* streams cannot be moved unless `tmpPath` is defined explicitly.
*/
move(location: string, options?: {
name?: string;
overwrite?: boolean;
}): Promise<void>;
/**
* Move file to a pre-registered drive disk
*/
moveToDisk(location: string, options?: WriteOptions & {
name?: string;
}, diskName?: keyof DisksList): Promise<void>;
/**
* Get JSON representation of file
*/
toJSON(): FileJSON;
}
/**
* Shape of the bodyparser middleware class constructor
*/
export interface BodyParserMiddlewareContract {
new (config: BodyParserConfig): {
handle(ctx: HttpContextContract, next: () => void): any;
};
}
const BodyParserMiddleware: BodyParserMiddlewareContract;
export default BodyParserMiddleware;
}
@@ -0,0 +1,8 @@
/*
* @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.
*/
@@ -0,0 +1,2 @@
/// <reference path="bodyparser.d.ts" />
/// <reference path="request.d.ts" />
+10
View File
@@ -0,0 +1,10 @@
/*
* @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.
*/
/// <reference path="./bodyparser.ts" />
/// <reference path="./request.ts" />
@@ -0,0 +1,14 @@
/**
* Extending the `request` interface on the core module
*/
declare module '@ioc:Adonis/Core/Request' {
import { MultipartContract, FileValidationOptions, MultipartFileContract } from '@ioc:Adonis/Core/BodyParser';
interface RequestContract {
file(key: string, options?: Partial<FileValidationOptions>): MultipartFileContract | null;
files(key: string, options?: Partial<FileValidationOptions>): MultipartFileContract[];
allFiles(): {
[field: string]: MultipartFileContract | MultipartFileContract[];
};
multipart: MultipartContract;
}
}
@@ -0,0 +1,8 @@
/*
* @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.
*/
@@ -0,0 +1,14 @@
import { ApplicationContract } from '@ioc:Adonis/Core/Application';
export default class BodyParserProvider {
protected app: ApplicationContract;
constructor(app: ApplicationContract);
static needsApplication: boolean;
/**
* Registers the bodyparser middleware namespace to the container.
*/
register(): void;
/**
* Adding the `file` macro to add support for reading request files.
*/
boot(): void;
}
@@ -0,0 +1,33 @@
"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 });
class BodyParserProvider {
constructor(app) {
this.app = app;
}
/**
* Registers the bodyparser middleware namespace to the container.
*/
register() {
this.app.container.bind('Adonis/Core/BodyParser', () => {
const { BodyParserMiddleware } = require('../src/BodyParser/index');
return BodyParserMiddleware;
});
}
/**
* Adding the `file` macro to add support for reading request files.
*/
boot() {
const extendRequest = require('../src/Bindings/Request').default;
extendRequest(this.app.container.resolveBinding('Adonis/Core/Request'));
}
}
exports.default = BodyParserProvider;
BodyParserProvider.needsApplication = true;
@@ -0,0 +1,7 @@
/// <reference path="../../adonis-typings/bodyparser.d.ts" />
import { RequestConstructorContract } from '@ioc:Adonis/Core/Request';
/**
* Extend the Request class by adding `file` and `files` macro to read processed
* files
*/
export default function extendRequest(Request: RequestConstructorContract): void;
+69
View File
@@ -0,0 +1,69 @@
"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 });
/// <reference path="../../adonis-typings/bodyparser.ts" />
const utils_1 = require("@poppinss/utils");
const File_1 = require("../Multipart/File");
/**
* Updates the validation options on the file instance
*/
function setFileOptions(file, options) {
if (file.sizeLimit === undefined && options && options.size) {
file.sizeLimit = options.size;
}
if (file.allowedExtensions === undefined && options && options.extnames) {
file.allowedExtensions = options.extnames;
}
}
/**
* A boolean to know if file is an instance of multipart
* file class
*/
function isInstanceOfFile(file) {
return file && file instanceof File_1.File;
}
/**
* Extend the Request class by adding `file` and `files` macro to read processed
* files
*/
function extendRequest(Request) {
/**
* Fetch a single file
*/
Request.macro('file', function getFile(key, options) {
let file = utils_1.lodash.get(this.allFiles(), key);
file = Array.isArray(file) ? file[0] : file;
if (!isInstanceOfFile(file)) {
return null;
}
setFileOptions(file, options);
file.validate();
return file;
});
/**
* Fetch an array of files
*/
Request.macro('files', function getFiles(key, options) {
let files = utils_1.lodash.get(this.allFiles(), key);
files = Array.isArray(files) ? files : files ? [files] : [];
return files.filter(isInstanceOfFile).map((file) => {
setFileOptions(file, options);
file.validate();
return file;
});
});
/**
* Fetch all files
*/
Request.macro('allFiles', function allFiles() {
return this['__raw_files'] || {};
});
}
exports.default = extendRequest;
@@ -0,0 +1,44 @@
/// <reference path="../../adonis-typings/bodyparser.d.ts" />
/// <reference types="@adonisjs/config/build/adonis-typings/config" />
import type { ConfigContract } from '@ioc:Adonis/Core/Config';
import type { DriveManagerContract } from '@ioc:Adonis/Core/Drive';
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
/**
* BodyParser middleware parses the incoming request body and set it as
* request body to be read later in the request lifecycle.
*/
export declare class BodyParserMiddleware {
private drive;
/**
* Bodyparser config
*/
private config;
constructor(Config: ConfigContract, drive: DriveManagerContract);
/**
* Returns config for a given type
*/
private getConfigFor;
/**
* Ensures that types exists and have length
*/
private ensureTypes;
/**
* Returns a boolean telling if request `content-type` header
* matches the expected types or not
*/
private isType;
/**
* Returns a proper Adonis style exception for popular error codes
* returned by https://github.com/stream-utils/raw-body#readme.
*/
private getExceptionFor;
/**
* Returns the tmp path for storing the files temporarly
*/
private getTmpPath;
/**
* Handle HTTP request body by parsing it as per the user
* config
*/
handle(ctx: HttpContextContract, next: () => Promise<void>): Promise<void>;
}
+232
View File
@@ -0,0 +1,232 @@
"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 __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BodyParserMiddleware = void 0;
/// <reference path="../../adonis-typings/bodyparser.ts" />
const os_1 = require("os");
const co_body_1 = __importDefault(require("@poppinss/co-body"));
const path_1 = require("path");
const utils_1 = require("@poppinss/utils");
const application_1 = require("@adonisjs/application");
const helpers_1 = require("@poppinss/utils/build/helpers");
const Multipart_1 = require("../Multipart");
const streamFile_1 = require("../Multipart/streamFile");
/**
* BodyParser middleware parses the incoming request body and set it as
* request body to be read later in the request lifecycle.
*/
let BodyParserMiddleware = class BodyParserMiddleware {
constructor(Config, drive) {
this.drive = drive;
this.config = Config.get('bodyparser', {});
}
/**
* Returns config for a given type
*/
getConfigFor(type) {
const config = this.config[type];
config['returnRawBody'] = true;
return config;
}
/**
* Ensures that types exists and have length
*/
ensureTypes(types) {
return !!(types && types.length);
}
/**
* Returns a boolean telling if request `content-type` header
* matches the expected types or not
*/
isType(request, types) {
return !!(this.ensureTypes(types) && request.is(types));
}
/**
* Returns a proper Adonis style exception for popular error codes
* returned by https://github.com/stream-utils/raw-body#readme.
*/
getExceptionFor(error) {
switch (error.type) {
case 'encoding.unsupported':
return new utils_1.Exception(error.message, error.status, 'E_ENCODING_UNSUPPORTED');
case 'entity.too.large':
return new utils_1.Exception(error.message, error.status, 'E_REQUEST_ENTITY_TOO_LARGE');
case 'request.aborted':
return new utils_1.Exception(error.message, error.status, 'E_REQUEST_ABORTED');
default:
return error;
}
}
/**
* Returns the tmp path for storing the files temporarly
*/
getTmpPath(config) {
if (typeof config.tmpFileName === 'function') {
const tmpPath = config.tmpFileName();
return (0, path_1.isAbsolute)(tmpPath) ? tmpPath : (0, path_1.join)((0, os_1.tmpdir)(), tmpPath);
}
return (0, path_1.join)((0, os_1.tmpdir)(), (0, helpers_1.cuid)());
}
/**
* Handle HTTP request body by parsing it as per the user
* config
*/
async handle(ctx, next) {
/**
* Initiating the `__raw_files` private property as an object
*/
ctx.request['__raw_files'] = {};
const requestMethod = ctx.request.method();
/**
* Only process for whitelisted nodes
*/
if (!this.config.whitelistedMethods.includes(requestMethod)) {
ctx.logger.trace(`bodyparser skipping method ${requestMethod}`);
return next();
}
/**
* Return early when request body is empty. Many clients set the `Content-length = 0`
* when request doesn't have any body, which is not handled by the below method.
*
* The main point of `hasBody` is to early return requests with empty body created by
* clients with missing headers.
*/
if (!ctx.request.hasBody()) {
ctx.logger.trace('bodyparser skipping empty body');
return next();
}
/**
* Handle multipart form
*/
const multipartConfig = this.getConfigFor('multipart');
if (this.isType(ctx.request, multipartConfig.types)) {
ctx.logger.trace('bodyparser parsing as multipart body');
ctx.request.multipart = new Multipart_1.Multipart(ctx, {
maxFields: multipartConfig.maxFields,
limit: multipartConfig.limit,
fieldsLimit: multipartConfig.fieldsLimit,
convertEmptyStringsToNull: multipartConfig.convertEmptyStringsToNull,
}, this.drive);
/**
* Skip parsing when `autoProcess` is disabled or route matches one
* of the defined processManually route patterns.
*/
if (!multipartConfig.autoProcess ||
multipartConfig.processManually.indexOf(ctx.route.pattern) > -1) {
return next();
}
/**
* Make sure we are not running any validations on the uploaded files. They are
* deferred for the end user when they will access file using `request.file`
* method.
*/
ctx.request.multipart.onFile('*', { deferValidations: true }, async (part, reporter) => {
/**
* We need to abort the main request when we are unable to process any
* file. Otherwise the error will endup on the file object, which
* is incorrect.
*/
try {
const tmpPath = this.getTmpPath(multipartConfig);
await (0, streamFile_1.streamFile)(part, tmpPath, reporter);
return { tmpPath };
}
catch (error) {
ctx.request.multipart.abort(error);
}
});
const action = ctx.profiler.profile('bodyparser:multipart');
try {
await ctx.request.multipart.process();
action.end();
return next();
}
catch (error) {
action.end({ error });
throw error;
}
}
/**
* Handle url-encoded form data
*/
const formConfig = this.getConfigFor('form');
if (this.isType(ctx.request, formConfig.types)) {
ctx.logger.trace('bodyparser parsing as form request');
const action = ctx.profiler.profile('bodyparser:urlencoded');
try {
const { parsed, raw } = await co_body_1.default.form(ctx.request.request, formConfig);
ctx.request.setInitialBody(parsed);
ctx.request.updateRawBody(raw);
action.end();
return next();
}
catch (error) {
action.end({ error });
throw this.getExceptionFor(error);
}
}
/**
* Handle content with JSON types
*/
const jsonConfig = this.getConfigFor('json');
if (this.isType(ctx.request, jsonConfig.types)) {
ctx.logger.trace('bodyparser parsing as json body');
const action = ctx.profiler.profile('bodyparser:json');
try {
const { parsed, raw } = await co_body_1.default.json(ctx.request.request, jsonConfig);
ctx.request.setInitialBody(parsed);
ctx.request.updateRawBody(raw);
action.end();
return next();
}
catch (error) {
action.end({ error });
throw this.getExceptionFor(error);
}
}
/**
* Handles raw request body
*/
const rawConfig = this.getConfigFor('raw');
if (this.isType(ctx.request, rawConfig.types)) {
ctx.logger.trace('bodyparser parsing as raw body');
const action = ctx.profiler.profile('bodyparser:raw');
try {
const { raw } = await co_body_1.default.text(ctx.request.request, rawConfig);
ctx.request.setInitialBody({});
ctx.request.updateRawBody(raw);
action.end();
return next();
}
catch (error) {
action.end({ error });
throw this.getExceptionFor(error);
}
}
await next();
}
};
BodyParserMiddleware = __decorate([
(0, application_1.inject)(['Adonis/Core/Config', 'Adonis/Core/Drive']),
__metadata("design:paramtypes", [Object, Object])
], BodyParserMiddleware);
exports.BodyParserMiddleware = BodyParserMiddleware;
@@ -0,0 +1,33 @@
/**
* A jar of form fields to store form data by handling
* array gracefully
*/
export declare class FormFields {
private config;
private fields;
constructor(config: {
convertEmptyStringsToNull: boolean;
});
/**
* Add a new key/value pair. The keys with array like
* expressions are handled properly.
*
* @example
* ```
* formfields.add('username', 'virk')
*
* // array
* formfields.add('username[]', 'virk')
* formfields.add('username[]', 'nikk')
*
* // Indexed keys are orderd properly
* formfields.add('username[1]', 'virk')
* formfields.add('username[0]', 'nikk')
* ```
*/
add(key: string, value: any): void;
/**
* Returns the copy of form fields
*/
get(): any;
}
+82
View File
@@ -0,0 +1,82 @@
"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.FormFields = void 0;
const utils_1 = require("@poppinss/utils");
/**
* A jar of form fields to store form data by handling
* array gracefully
*/
class FormFields {
constructor(config) {
this.config = config;
this.fields = {};
}
/**
* Add a new key/value pair. The keys with array like
* expressions are handled properly.
*
* @example
* ```
* formfields.add('username', 'virk')
*
* // array
* formfields.add('username[]', 'virk')
* formfields.add('username[]', 'nikk')
*
* // Indexed keys are orderd properly
* formfields.add('username[1]', 'virk')
* formfields.add('username[0]', 'nikk')
* ```
*/
add(key, value) {
let isArray = false;
/**
* Convert empty strings to null
*/
if (this.config.convertEmptyStringsToNull && value === '') {
value = null;
}
/**
* Drop `[]` without indexes, since lodash `_.set` and
* `_.get` methods needs the index or plain key.
*/
key = key.replace(/\[]$/, () => {
isArray = true;
return '';
});
/**
* Check to see if value exists or set it (if missing)
*/
const existingValue = utils_1.lodash.get(this.fields, key);
if (!existingValue) {
utils_1.lodash.set(this.fields, key, isArray ? [value] : value);
return;
}
/**
* Mutate existing value if it's an array
*/
if (existingValue instanceof Array) {
existingValue.push(value);
return;
}
/**
* Set new value + existing value
*/
utils_1.lodash.set(this.fields, key, [existingValue, value]);
}
/**
* Returns the copy of form fields
*/
get() {
return this.fields;
}
}
exports.FormFields = FormFields;
+123
View File
@@ -0,0 +1,123 @@
/// <reference path="../../adonis-typings/bodyparser.d.ts" />
import { DisksList, WriteOptions, DriveManagerContract } from '@ioc:Adonis/Core/Drive';
import { FileJSON, FileUploadError, FileValidationOptions, MultipartFileContract } from '@ioc:Adonis/Core/BodyParser';
/**
* The file holds the meta/data for an uploaded file, along with
* an errors occurred during the upload process.
*/
export declare class File implements MultipartFileContract {
private data;
private drive;
private sizeValidator;
private extensionValidator;
/**
* A boolean to know if file is an instance of this class
* or not
*/
isMultipartFile: true;
/**
* Field name is the name of the field
*/
fieldName: string;
/**
* Client name is the file name on the user client
*/
clientName: string;
/**
* The headers sent as part of the multipart request
*/
headers: any;
/**
* File size in bytes
*/
size: number;
/**
* The extname for the file.
*/
extname?: string;
/**
* Upload errors
*/
errors: FileUploadError[];
/**
* Type and subtype are extracted from the `content-type`
* header or from the file magic number
*/
type?: string;
subtype?: string;
/**
* File path is only set after the move operation
*/
filePath?: string;
/**
* File name is only set after the move operation. It is the relative
* path of the moved file
*/
fileName?: string;
/**
* Tmp path, only exists when file is uploaded using the
* classic mode.
*/
tmpPath?: string;
/**
* The file meta data
*/
meta: any;
/**
* The state of the file
*/
state: 'idle' | 'streaming' | 'consumed' | 'moved';
/**
* Whether or not the validations have been executed
*/
get validated(): boolean;
/**
* A boolean to know if file has one or more errors
*/
get isValid(): boolean;
/**
* Opposite of [[this.isValid]]
*/
get hasErrors(): boolean;
/**
* The maximum file size limit
*/
get sizeLimit(): number | string | undefined;
set sizeLimit(limit: number | string | undefined);
/**
* Extensions allowed
*/
get allowedExtensions(): string[] | undefined;
set allowedExtensions(extensions: string[] | undefined);
constructor(data: {
fieldName: string;
clientName: string;
headers: any;
}, validationOptions: Partial<FileValidationOptions>, drive: DriveManagerContract);
/**
* Validate the file
*/
validate(): void;
/**
* Mark file as moved
*/
markAsMoved(fileName: string, filePath: string): void;
/**
* Moves the file to a given location. Multiple calls to the `move` method are allowed,
* incase you want to move a file to multiple locations.
*/
move(location: string, options?: {
name?: string;
overwrite?: boolean;
}): Promise<void>;
/**
* Move file to a drive disk
*/
moveToDisk(location: string, options?: WriteOptions & {
name?: string;
}, diskName?: keyof DisksList): Promise<void>;
/**
* Returns file JSON representation
*/
toJSON(): FileJSON;
}
+200
View File
@@ -0,0 +1,200 @@
"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.File = void 0;
/// <reference path="../../adonis-typings/bodyparser.ts" />
const slash_1 = __importDefault(require("slash"));
const path_1 = require("path");
const utils_1 = require("@poppinss/utils");
const fs_extra_1 = require("fs-extra");
const helpers_1 = require("@poppinss/utils/build/helpers");
const Size_1 = require("./Validators/Size");
const Extensions_1 = require("./Validators/Extensions");
/**
* The file holds the meta/data for an uploaded file, along with
* an errors occurred during the upload process.
*/
class File {
/**
* Whether or not the validations have been executed
*/
get validated() {
return this.sizeValidator.validated && this.extensionValidator.validated;
}
/**
* A boolean to know if file has one or more errors
*/
get isValid() {
return this.errors.length === 0;
}
/**
* Opposite of [[this.isValid]]
*/
get hasErrors() {
return !this.isValid;
}
/**
* The maximum file size limit
*/
get sizeLimit() {
return this.sizeValidator.maxLimit;
}
set sizeLimit(limit) {
this.sizeValidator.maxLimit = limit;
}
/**
* Extensions allowed
*/
get allowedExtensions() {
return this.extensionValidator.extensions;
}
set allowedExtensions(extensions) {
this.extensionValidator.extensions = extensions;
}
constructor(data, validationOptions, drive) {
this.data = data;
this.drive = drive;
this.sizeValidator = new Size_1.SizeValidator(this);
this.extensionValidator = new Extensions_1.ExtensionValidator(this);
/**
* A boolean to know if file is an instance of this class
* or not
*/
this.isMultipartFile = true;
/**
* Field name is the name of the field
*/
this.fieldName = this.data.fieldName;
/**
* Client name is the file name on the user client
*/
this.clientName = this.data.clientName;
/**
* The headers sent as part of the multipart request
*/
this.headers = this.data.headers;
/**
* File size in bytes
*/
this.size = 0;
/**
* Upload errors
*/
this.errors = [];
/**
* The file meta data
*/
this.meta = {};
/**
* The state of the file
*/
this.state = 'idle';
this.sizeLimit = validationOptions.size;
this.allowedExtensions = validationOptions.extnames;
}
/**
* Validate the file
*/
validate() {
this.extensionValidator.validate();
this.sizeValidator.validate();
}
/**
* Mark file as moved
*/
markAsMoved(fileName, filePath) {
this.filePath = filePath;
this.fileName = fileName;
this.state = 'moved';
}
/**
* Moves the file to a given location. Multiple calls to the `move` method are allowed,
* incase you want to move a file to multiple locations.
*/
async move(location, options) {
if (!this.tmpPath) {
throw new utils_1.Exception('tmpPath must be set on the file before moving it', 500, 'E_MISSING_FILE_TMP_PATH');
}
options = Object.assign({ name: this.clientName, overwrite: true }, options);
const filePath = (0, path_1.join)(location, options.name);
try {
await (0, fs_extra_1.move)(this.tmpPath, filePath, { overwrite: options.overwrite });
this.markAsMoved(options.name, filePath);
}
catch (error) {
if (error.message.includes('dest already exists')) {
throw new utils_1.Exception(`"${options.name}" already exists at "${location}". Set "overwrite = true" to overwrite it`, 500);
}
throw error;
}
}
/**
* Move file to a drive disk
*/
async moveToDisk(location, options, diskName) {
const driver = diskName ? this.drive.use(diskName) : this.drive.use();
const fileName = driver.name === 'fake'
? this.clientName
: options?.name
? options.name
: `${(0, helpers_1.cuid)()}.${this.extname}`;
/**
* Move file as normal when using the local driver
*/
if (driver.name === 'local') {
await this.move(driver.makePath(location), {
name: fileName,
overwrite: true,
});
return;
}
/**
* Make a unix style key for cloud drivers, since the cloud
* key is not a filesystem path
*/
const key = (0, slash_1.default)((0, path_1.join)(location || './', fileName));
/**
* Set the content type for cloud drivers
*/
options = options || {};
if (this.type && this.subtype && !options.contentType) {
options.contentType = `${this.type}/${this.subtype}`;
}
if (options.contentLength === undefined) {
options.contentLength = this.size;
}
await driver.putStream(key, (0, fs_extra_1.createReadStream)(this.tmpPath), options);
this.markAsMoved(key, await driver.getUrl(key));
}
/**
* Returns file JSON representation
*/
toJSON() {
return {
fieldName: this.fieldName,
clientName: this.clientName,
size: this.size,
filePath: this.filePath,
fileName: this.fileName,
type: this.type,
extname: this.extname,
subtype: this.subtype,
state: this.state,
isValid: this.isValid,
validated: this.validated,
errors: this.errors,
meta: this.meta,
};
}
}
exports.File = File;
@@ -0,0 +1,94 @@
/// <reference path="../../adonis-typings/index.d.ts" />
/// <reference types="node" />
import { DriveManagerContract } from '@ioc:Adonis/Core/Drive';
import { MultipartStream, FileValidationOptions } from '@ioc:Adonis/Core/BodyParser';
import { File } from './File';
/**
* 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.
*/
export declare class PartHandler {
private part;
private options;
private drive;
/**
* The stream buffer reported by the stream consumer. We hold the buffer until are
* able to detect the file extension and then buff memory is released
*/
private buff?;
/**
* 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
*/
private get canFileTypeBeDetected();
/**
* Creating a new file object for each part inside the multipart
* form data
*/
file: File;
/**
* 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.
*/
private emittedValidationError;
constructor(part: MultipartStream, options: Partial<FileValidationOptions & {
deferValidations: boolean;
}>, drive: DriveManagerContract);
/**
* Detects the file type and extension and also validates it when validations
* are not deferred.
*/
private detectFileTypeAndExtension;
/**
* Skip the stream or end it forcefully. This is invoked when the
* streaming consumer reports an error
*/
private skipEndStream;
/**
* Finish the process of listening for any more events and mark the
* file state as consumed.
*/
private finish;
/**
* Start the process the updating the file state
* to streaming mode.
*/
begin(): void;
/**
* Handles the file upload progress by validating the file size and
* extension.
*/
reportProgress(line: Buffer, bufferLength: number): Promise<void>;
/**
* 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.
*/
reportError(error: any): Promise<void>;
/**
* Report success data about the file.
*/
reportSuccess(data?: {
filePath?: string;
tmpPath?: string;
} & {
[key: string]: any;
}): Promise<void>;
}
@@ -0,0 +1,207 @@
"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 {
/**
* 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;
}
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;
}
/**
* 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;
@@ -0,0 +1,33 @@
/// <reference path="../../../adonis-typings/bodyparser.d.ts" />
import { MultipartFileContract } from '@ioc:Adonis/Core/BodyParser';
/**
* Validates the file extension
*/
export declare class ExtensionValidator {
private file;
private allowedExtensions?;
validated: boolean;
/**
* Update the expected file extensions
*/
get extensions(): string[] | undefined;
set extensions(extnames: string[] | undefined);
constructor(file: MultipartFileContract);
/**
* Report error to the file
*/
private reportError;
/**
* Validating the file in the streaming mode. During this mode
* we defer the validation, until we get the file extname.
*/
private validateWhenGettingStreamed;
/**
* Validate the file extension after it has been streamed
*/
private validateAfterConsumed;
/**
* Validate the file
*/
validate(): void;
}
@@ -0,0 +1,109 @@
"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.ExtensionValidator = void 0;
/**
* Validates the file extension
*/
class ExtensionValidator {
/**
* Update the expected file extensions
*/
get extensions() {
return this.allowedExtensions;
}
set extensions(extnames) {
if (this.allowedExtensions && this.allowedExtensions.length) {
throw new Error('Cannot update allowed extension names after file has been validated');
}
this.validated = false;
this.allowedExtensions = extnames;
}
constructor(file) {
this.file = file;
this.allowedExtensions = [];
this.validated = false;
}
/**
* Report error to the file
*/
reportError() {
/**
* File is invalid, so report the error
*/
const suffix = this.allowedExtensions.length === 1 ? 'is' : 'are';
const message = [
`Invalid file extension ${this.file.extname}.`,
`Only ${this.allowedExtensions.join(', ')} ${suffix} allowed`,
].join(' ');
this.file.errors.push({
fieldName: this.file.fieldName,
clientName: this.file.clientName,
message: message,
type: 'extname',
});
}
/**
* Validating the file in the streaming mode. During this mode
* we defer the validation, until we get the file extname.
*/
validateWhenGettingStreamed() {
if (!this.file.extname) {
return;
}
this.validated = true;
/**
* Valid extension type
*/
if (this.allowedExtensions.includes(this.file.extname)) {
return;
}
this.reportError();
}
/**
* Validate the file extension after it has been streamed
*/
validateAfterConsumed() {
this.validated = true;
/**
* Valid extension type
*/
if (this.allowedExtensions.includes(this.file.extname || '')) {
return;
}
this.reportError();
}
/**
* Validate the file
*/
validate() {
/**
* Do not validate if already validated
*/
if (this.validated) {
return;
}
/**
* Do not run validations, when constraints on the extension are not set
*/
if (!Array.isArray(this.allowedExtensions) || this.allowedExtensions.length === 0) {
this.validated = true;
return;
}
if (this.file.state === 'streaming') {
this.validateWhenGettingStreamed();
return;
}
if (this.file.state === 'consumed') {
this.validateAfterConsumed();
}
}
}
exports.ExtensionValidator = ExtensionValidator;
@@ -0,0 +1,36 @@
/// <reference path="../../../adonis-typings/bodyparser.d.ts" />
import { MultipartFileContract } from '@ioc:Adonis/Core/BodyParser';
/**
* Size validator validates the file size
*/
export declare class SizeValidator {
private file;
private maximumAllowedLimit?;
private bytesLimit;
validated: boolean;
/**
* Defining the maximum bytes the file can have
*/
get maxLimit(): number | string | undefined;
set maxLimit(limit: number | string | undefined);
constructor(file: MultipartFileContract);
/**
* Reporting error to the file
*/
private reportError;
/**
* Validating file size while it is getting streamed. We only mark
* the file as `validated` when it's validation fails. Otherwise
* we keep re-validating the file as we receive more data.
*/
private validateWhenGettingStreamed;
/**
* We have the final file size after the stream has been consumed. At this
* stage we always mark `validated = true`.
*/
private validateAfterConsumed;
/**
* Validate the file size
*/
validate(): void;
}
@@ -0,0 +1,102 @@
"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.SizeValidator = void 0;
/// <reference path="../../../adonis-typings/bodyparser.ts" />
const bytes_1 = __importDefault(require("bytes"));
/**
* Size validator validates the file size
*/
class SizeValidator {
/**
* Defining the maximum bytes the file can have
*/
get maxLimit() {
return this.maximumAllowedLimit;
}
set maxLimit(limit) {
if (this.maximumAllowedLimit !== undefined) {
throw new Error('Cannot reset sizeLimit after file has been validated');
}
this.validated = false;
this.maximumAllowedLimit = limit;
if (this.maximumAllowedLimit) {
this.bytesLimit =
typeof this.maximumAllowedLimit === 'string'
? (0, bytes_1.default)(this.maximumAllowedLimit)
: this.maximumAllowedLimit;
}
}
constructor(file) {
this.file = file;
this.bytesLimit = 0;
this.validated = false;
}
/**
* Reporting error to the file
*/
reportError() {
this.file.errors.push({
fieldName: this.file.fieldName,
clientName: this.file.clientName,
message: `File size should be less than ${(0, bytes_1.default)(this.bytesLimit)}`,
type: 'size',
});
}
/**
* Validating file size while it is getting streamed. We only mark
* the file as `validated` when it's validation fails. Otherwise
* we keep re-validating the file as we receive more data.
*/
validateWhenGettingStreamed() {
if (this.file.size > this.bytesLimit) {
this.validated = true;
this.reportError();
}
}
/**
* We have the final file size after the stream has been consumed. At this
* stage we always mark `validated = true`.
*/
validateAfterConsumed() {
this.validated = true;
if (this.file.size > this.bytesLimit) {
this.reportError();
}
}
/**
* Validate the file size
*/
validate() {
if (this.validated) {
return;
}
/**
* Do not attempt to validate when `maximumAllowedLimit` is not
* defined.
*/
if (this.maximumAllowedLimit === undefined) {
this.validated = true;
return;
}
if (this.file.state === 'streaming') {
this.validateWhenGettingStreamed();
return;
}
if (this.file.state === 'consumed') {
this.validateAfterConsumed();
return;
}
}
}
exports.SizeValidator = SizeValidator;
+117
View File
@@ -0,0 +1,117 @@
/// <reference path="../../adonis-typings/bodyparser.d.ts" />
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import { MultipartContract, PartHandler as PartHandlerType } from '@ioc:Adonis/Core/BodyParser';
import { DriveManagerContract } from '@ioc:Adonis/Core/Drive';
/**
* 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.
*/
export declare class Multipart implements MultipartContract {
private ctx;
private config;
private drive;
/**
* The registered handlers to handle the file uploads
*/
private handlers;
/**
* Collected fields from the multipart stream
*/
private fields;
/**
* Collected files from the multipart stream. Files are only collected
* when there is an attached listener for a given file.
*/
private files;
/**
* We track the finishing of `this.onFile` async handlers
* to make sure that `process` promise resolves for all
* handlers to finish.
*/
private pendingHandlers;
/**
* The reference to underlying multiparty form
*/
private form;
/**
* Total size limit of the multipart stream. If it goes beyond
* the limit, then an exception will be raised.
*/
private upperLimit?;
/**
* Total size in bytes for all the fields (not the files)
*/
private maxFieldsSize?;
/**
* A track of total number of file bytes processed so far
*/
private processedBytes;
/**
* The current state of the multipart form handler
*/
state: 'idle' | 'processing' | 'error' | 'success';
constructor(ctx: HttpContextContract, config: Partial<{
limit: string | number;
fieldsLimit: string | number;
maxFields: number;
convertEmptyStringsToNull: boolean;
}>, drive: DriveManagerContract);
/**
* Returns a boolean telling whether all streams have been
* consumed along with all handlers execution
*/
private isClosed;
/**
* Removes array like expression from the part name to
* find the handler
*/
private getHandlerName;
/**
* Validates and returns an error when upper limit is defined and
* processed bytes is over the upper limit
*/
private validateProcessedBytes;
/**
* Handles a given part by invoking it's handler or
* by resuming the part, if there is no defined
* handler
*/
private handlePart;
/**
* Record the fields inside multipart contract
*/
private handleField;
/**
* Processes the user config and computes the `upperLimit` value from
* it.
*/
private processConfig;
/**
* Mark the process as finished
*/
private finish;
/**
* 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: string, options: Parameters<MultipartContract['onFile']>[1], handler: PartHandlerType): this;
/**
* Abort request by emitting error
*/
abort(error: any): void;
/**
* Process the request by going all the file and field
* streams.
*/
process(config?: Parameters<MultipartContract['process']>[0]): Promise<void>;
}
+307
View File
@@ -0,0 +1,307 @@
"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(/stream ended unexpectedly/)) {
reject(new utils_1.Exception('Invalid multipart request', 400, 'E_INVALID_MULTIPART_REQUEST'));
}
else 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;
@@ -0,0 +1,9 @@
/// <reference types="node" />
/// <reference types="node" />
import { Readable } from 'stream';
/**
* Writes readable stream to the given location by properly cleaning up readable
* and writable streams in case of any errors. Also an optional data listener
* can listen for the `data` event.
*/
export declare function streamFile(readStream: Readable, location: string, dataListener?: (line: Buffer) => void): Promise<void>;
@@ -0,0 +1,36 @@
"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.streamFile = void 0;
const util_1 = require("util");
const fs_extra_1 = require("fs-extra");
const fs_1 = require("fs");
const stream_1 = require("stream");
const pump = (0, util_1.promisify)(stream_1.pipeline);
/**
* Writes readable stream to the given location by properly cleaning up readable
* and writable streams in case of any errors. Also an optional data listener
* can listen for the `data` event.
*/
async function streamFile(readStream, location, dataListener) {
if (typeof dataListener === 'function') {
readStream.pause();
readStream.on('data', dataListener);
}
const writeStream = (0, fs_1.createWriteStream)(location);
try {
await pump(readStream, writeStream);
}
catch (error) {
(0, fs_extra_1.unlink)(writeStream.path).catch(() => { });
throw error;
}
}
exports.streamFile = streamFile;
+25
View File
@@ -0,0 +1,25 @@
/// <reference path="../adonis-typings/bodyparser.d.ts" />
/// <reference types="node" />
/**
* We can detect file types for these files using the magic
* number
*/
export declare const supportMagicFileTypes: Set<import("file-type/core").FileExtension>;
/**
* Returns the file `type`, `subtype` and `extension`.
*/
export declare function getFileType(fileContents: Buffer): Promise<null | {
ext: string;
type?: string;
subtype?: string;
}>;
/**
* Computes file name from the file type
*/
export declare function computeFileTypeFromName(clientName: string, headers: {
[key: string]: string;
}): {
ext: string;
type?: string;
subtype?: string;
};
+60
View File
@@ -0,0 +1,60 @@
"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.computeFileTypeFromName = exports.getFileType = exports.supportMagicFileTypes = void 0;
/// <reference path="../adonis-typings/bodyparser.ts" />
const path_1 = require("path");
const file_type_1 = require("file-type");
const media_typer_1 = __importDefault(require("media-typer"));
/**
* We can detect file types for these files using the magic
* number
*/
exports.supportMagicFileTypes = file_type_1.extensions;
/**
* Attempts to parse the file mime type using the file magic number
*/
function parseMimeType(mime) {
try {
const { type, subtype } = media_typer_1.default.parse(mime);
return { type, subtype };
}
catch (error) {
return null;
}
}
/**
* Returns the file `type`, `subtype` and `extension`.
*/
async function getFileType(fileContents) {
/**
* Attempt to detect file type from it's content
*/
const magicType = await (0, file_type_1.fromBuffer)(fileContents);
if (magicType) {
return Object.assign({ ext: magicType.ext }, parseMimeType(magicType.mime));
}
return null;
}
exports.getFileType = getFileType;
/**
* Computes file name from the file type
*/
function computeFileTypeFromName(clientName, headers) {
/**
* Otherwise fallback to file extension from it's client name
* and pull type/subtype from the headers content type.
*/
return Object.assign({ ext: (0, path_1.extname)(clientName).replace(/^\./, '') }, parseMimeType(headers['content-type']));
}
exports.computeFileTypeFromName = computeFileTypeFromName;
+163
View File
@@ -0,0 +1,163 @@
{
"name": "@adonisjs/bodyparser",
"version": "8.1.9",
"description": "AdonisJs body parser to read and parse HTTP request bodies",
"main": "build/providers/BodyParserProvider.js",
"files": [
"build/src",
"build/adonis-typings",
"build/providers"
],
"scripts": {
"mrm": "mrm --preset=@adonisjs/mrm-preset",
"pretest": "npm run lint",
"test": "node -r @adonisjs/require-ts/build/register bin/test.ts",
"clean": "del build",
"compile": "npm run lint && npm run clean && tsc",
"build": "npm run compile",
"commit": "git-cz",
"release": "np --message=\"chore(release): %s\"",
"version": "npm run build",
"prepublishOnly": "npm run build",
"lint": "eslint . --ext=.ts",
"format": "prettier --write .",
"sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json adonisjs/bodyparser"
},
"devDependencies": {
"@adonisjs/application": "^5.2.5",
"@adonisjs/drive": "^2.3.0",
"@adonisjs/encryption": "^4.0.8",
"@adonisjs/http-server": "^5.11.0",
"@adonisjs/mrm-preset": "^5.0.3",
"@adonisjs/require-ts": "^2.0.13",
"@japa/assert": "^1.3.6",
"@japa/run-failed-tests": "^1.1.0",
"@japa/runner": "^2.2.1",
"@japa/spec-reporter": "^1.3.1",
"@poppinss/dev-utils": "^2.0.3",
"@types/bytes": "^3.1.1",
"@types/fs-extra": "^9.0.13",
"@types/media-typer": "^1.1.1",
"@types/node": "^18.8.3",
"@types/supertest": "^2.0.12",
"@types/uuid": "^8.3.4",
"commitizen": "^4.2.5",
"cz-conventional-changelog": "^3.3.0",
"del-cli": "^5.0.0",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-adonis": "^2.1.1",
"eslint-plugin-prettier": "^4.2.1",
"github-label-sync": "^2.2.0",
"husky": "^8.0.1",
"mrm": "^4.1.6",
"node-fetch": "^2.6.11",
"np": "^7.6.2",
"prettier": "^2.7.1",
"reflect-metadata": "^0.1.13",
"supertest": "^6.3.0",
"typescript": "^4.8.4"
},
"peerDependencies": {
"@adonisjs/application": "^5.0.0",
"@adonisjs/drive": "^2.0.0",
"@adonisjs/http-server": "^5.0.0"
},
"nyc": {
"exclude": [
"test"
],
"extension": [
".ts"
]
},
"license": "MIT",
"husky": {
"hooks": {
"commit-msg": "node ./node_modules/@adonisjs/mrm-preset/validateCommit/conventional/validate.js"
}
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"np": {
"contents": ".",
"anyBranch": false
},
"dependencies": {
"@poppinss/co-body": "^1.1.3",
"@poppinss/multiparty": "^2.0.1",
"@poppinss/utils": "^5.0.0",
"bytes": "^3.1.2",
"file-type": "^16.5.4",
"fs-extra": "^10.1.0",
"media-typer": "^1.1.0",
"slash": "^3.0.0"
},
"publishConfig": {
"access": "public",
"tag": "latest"
},
"directories": {
"test": "test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/adonisjs/adonis-bodyparser.git"
},
"keywords": [
"adonisjs",
"bodyparser",
"multipart"
],
"author": "virk,adonisjs",
"bugs": {
"url": "https://github.com/adonisjs/adonis-bodyparser/issues"
},
"homepage": "https://github.com/adonisjs/adonis-bodyparser#readme",
"mrmConfig": {
"core": true,
"license": "MIT",
"services": [
"github-actions"
],
"minNodeVersion": "14.15.4",
"probotApps": [
"stale",
"lock"
],
"runGhActionsOnWindows": true
},
"eslintConfig": {
"extends": [
"plugin:adonis/typescriptPackage",
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
},
"eslintIgnore": [
"build"
],
"prettier": {
"trailingComma": "es5",
"semi": false,
"singleQuote": true,
"useTabs": false,
"quoteProps": "consistent",
"bracketSpacing": true,
"arrowParens": "always",
"printWidth": 100
}
}