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
+217
View File
@@ -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;
}
+8
View File
@@ -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.
*/
+2
View File
@@ -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" />
+14
View File
@@ -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;
}
}
+8
View File
@@ -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;
+7
View File
@@ -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;
+44
View File
@@ -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;
+33
View File
@@ -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;
+94
View 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>;
}
+207
View File
@@ -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>;
+36
View File
@@ -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;