@nestjs/platform-fastify 12.0.4 → 12.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters/fastify-adapter.d.ts +48 -5
- package/adapters/fastify-adapter.js +137 -9
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/interfaces/external/fastify-multipart-options.interface.d.ts +53 -0
- package/interfaces/external/fastify-multipart-options.interface.js +1 -0
- package/interfaces/external/index.d.ts +1 -0
- package/interfaces/external/index.js +1 -0
- package/multipart/files.constants.d.ts +1 -0
- package/multipart/files.constants.js +1 -0
- package/multipart/index.d.ts +4 -0
- package/multipart/index.js +4 -0
- package/multipart/interceptors/any-files.interceptor.d.ts +12 -0
- package/multipart/interceptors/any-files.interceptor.js +13 -0
- package/multipart/interceptors/file-fields.interceptor.d.ts +13 -0
- package/multipart/interceptors/file-fields.interceptor.js +14 -0
- package/multipart/interceptors/file-stream.interceptor.d.ts +31 -0
- package/multipart/interceptors/file-stream.interceptor.js +32 -0
- package/multipart/interceptors/file.interceptor.d.ts +17 -0
- package/multipart/interceptors/file.interceptor.js +18 -0
- package/multipart/interceptors/files.interceptor.d.ts +14 -0
- package/multipart/interceptors/files.interceptor.js +15 -0
- package/multipart/interceptors/index.d.ts +6 -0
- package/multipart/interceptors/index.js +6 -0
- package/multipart/interceptors/no-files.interceptor.d.ts +12 -0
- package/multipart/interceptors/no-files.interceptor.js +13 -0
- package/multipart/interfaces/files-upload-module.interface.d.ts +18 -0
- package/multipart/interfaces/files-upload-module.interface.js +1 -0
- package/multipart/interfaces/index.d.ts +3 -0
- package/multipart/interfaces/index.js +3 -0
- package/multipart/interfaces/multipart-file.interface.d.ts +70 -0
- package/multipart/interfaces/multipart-file.interface.js +1 -0
- package/multipart/interfaces/multipart-options.interface.d.ts +77 -0
- package/multipart/interfaces/multipart-options.interface.js +1 -0
- package/multipart/multipart/append-field.util.d.ts +6 -0
- package/multipart/multipart/append-field.util.js +112 -0
- package/multipart/multipart/multipart-interceptor.factory.d.ts +13 -0
- package/multipart/multipart/multipart-interceptor.factory.js +130 -0
- package/multipart/multipart/multipart.constants.d.ts +55 -0
- package/multipart/multipart/multipart.constants.js +74 -0
- package/multipart/multipart/multipart.error.d.ts +10 -0
- package/multipart/multipart/multipart.error.js +15 -0
- package/multipart/multipart/multipart.parser.d.ts +39 -0
- package/multipart/multipart/multipart.parser.js +369 -0
- package/multipart/multipart/multipart.utils.d.ts +28 -0
- package/multipart/multipart/multipart.utils.js +54 -0
- package/multipart/multipart.constants.d.ts +1 -0
- package/multipart/multipart.constants.js +1 -0
- package/multipart/multipart.module.d.ts +18 -0
- package/multipart/multipart.module.js +75 -0
- package/multipart/storage/disk.storage.d.ts +24 -0
- package/multipart/storage/disk.storage.js +58 -0
- package/multipart/storage/index.d.ts +2 -0
- package/multipart/storage/index.js +2 -0
- package/multipart/storage/memory.storage.d.ts +8 -0
- package/multipart/storage/memory.storage.js +23 -0
- package/package.json +7 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { InjectionToken, ModuleMetadata, OptionalFactoryDependency, Type } from '@nestjs/common';
|
|
2
|
+
import type { MultipartOptions } from './multipart-options.interface.js';
|
|
3
|
+
export type MultipartModuleOptions = MultipartOptions;
|
|
4
|
+
/**
|
|
5
|
+
* @publicApi
|
|
6
|
+
*/
|
|
7
|
+
export interface MultipartOptionsFactory {
|
|
8
|
+
createMultipartOptions(): Promise<MultipartModuleOptions> | MultipartModuleOptions;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* @publicApi
|
|
12
|
+
*/
|
|
13
|
+
export interface MultipartModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
|
14
|
+
useExisting?: Type<MultipartOptionsFactory>;
|
|
15
|
+
useClass?: Type<MultipartOptionsFactory>;
|
|
16
|
+
useFactory?: (...args: any[]) => Promise<MultipartModuleOptions> | MultipartModuleOptions;
|
|
17
|
+
inject?: Array<InjectionToken | OptionalFactoryDependency>;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Readable } from 'stream';
|
|
2
|
+
/**
|
|
3
|
+
* A stored upload, in the shape multer produces (`Express.Multer.File`), so
|
|
4
|
+
* `@UploadedFile()`, `ParseFilePipe` and the built-in file validators work
|
|
5
|
+
* unchanged. Memory storage sets `buffer`; disk storage sets `destination`,
|
|
6
|
+
* `filename` and `path`.
|
|
7
|
+
*
|
|
8
|
+
* @publicApi
|
|
9
|
+
*/
|
|
10
|
+
export interface UploadedMultipartFile {
|
|
11
|
+
/** Field name specified in the form */
|
|
12
|
+
fieldname: string;
|
|
13
|
+
/** Name of the file on the user's computer */
|
|
14
|
+
originalname: string;
|
|
15
|
+
/** Encoding type of the file */
|
|
16
|
+
encoding: string;
|
|
17
|
+
/** Mime type of the file */
|
|
18
|
+
mimetype: string;
|
|
19
|
+
/** Size of the file in bytes */
|
|
20
|
+
size: number;
|
|
21
|
+
/** A Buffer of the entire file (memory storage) */
|
|
22
|
+
buffer?: Buffer;
|
|
23
|
+
/** The folder to which the file has been saved (disk storage) */
|
|
24
|
+
destination?: string;
|
|
25
|
+
/** The name of the file within the destination (disk storage) */
|
|
26
|
+
filename?: string;
|
|
27
|
+
/** Location of the uploaded file (disk storage) */
|
|
28
|
+
path?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* What a storage engine and `fileFilter` see before the file is stored.
|
|
32
|
+
*
|
|
33
|
+
* @publicApi
|
|
34
|
+
*/
|
|
35
|
+
export interface IncomingMultipartFile {
|
|
36
|
+
/** Field name specified in the form */
|
|
37
|
+
fieldname: string;
|
|
38
|
+
/** Name of the file on the user's computer */
|
|
39
|
+
originalname: string;
|
|
40
|
+
/** Encoding type of the file */
|
|
41
|
+
encoding: string;
|
|
42
|
+
/** Mime type of the file */
|
|
43
|
+
mimetype: string;
|
|
44
|
+
/**
|
|
45
|
+
* The file contents. Non-enumerable; meant to be consumed by
|
|
46
|
+
* `MultipartStorageEngine._handleFile` only.
|
|
47
|
+
*/
|
|
48
|
+
readonly stream: Readable;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The file `FileStreamInterceptor` hands to the route handler. Its size is
|
|
52
|
+
* not known up front.
|
|
53
|
+
*
|
|
54
|
+
* @publicApi
|
|
55
|
+
*/
|
|
56
|
+
export interface MultipartFileStream {
|
|
57
|
+
/** Field name specified in the form */
|
|
58
|
+
fieldname: string;
|
|
59
|
+
/** Name of the file on the user's computer */
|
|
60
|
+
originalname: string;
|
|
61
|
+
/** Encoding type of the file */
|
|
62
|
+
encoding: string;
|
|
63
|
+
/** Mime type of the file */
|
|
64
|
+
mimetype: string;
|
|
65
|
+
/**
|
|
66
|
+
* The file contents. Errors with a `PayloadTooLargeException` once
|
|
67
|
+
* `limits.fileSize` is exceeded.
|
|
68
|
+
*/
|
|
69
|
+
stream: Readable;
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { IncomingMultipartFile, UploadedMultipartFile } from './multipart-file.interface.js';
|
|
2
|
+
/**
|
|
3
|
+
* multer's storage engine contract (`_handleFile` / `_removeFile`), kept
|
|
4
|
+
* as-is so existing multer storage engines can be passed as `storage`.
|
|
5
|
+
*
|
|
6
|
+
* @publicApi
|
|
7
|
+
*/
|
|
8
|
+
export interface MultipartStorageEngine {
|
|
9
|
+
_handleFile(req: any, file: IncomingMultipartFile, callback: (error?: any, info?: Partial<UploadedMultipartFile>) => void): void;
|
|
10
|
+
_removeFile(req: any, file: UploadedMultipartFile, callback: (error: Error | null) => void): void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Same keys as platform-express's `MulterLimits`.
|
|
14
|
+
*
|
|
15
|
+
* Limits that are not set fall back to the options `@fastify/multipart` was
|
|
16
|
+
* registered with, and then to its own defaults. Unlike multer (which
|
|
17
|
+
* defaults `fileSize` and `parts` to `Infinity`), `@fastify/multipart` caps
|
|
18
|
+
* `fileSize` at the Fastify instance's `bodyLimit` (1 MiB unless configured)
|
|
19
|
+
* and `parts` at 1000.
|
|
20
|
+
*
|
|
21
|
+
* @publicApi
|
|
22
|
+
*/
|
|
23
|
+
export interface MultipartLimits {
|
|
24
|
+
/**
|
|
25
|
+
* Max field name size in bytes (Default: unlimited). Unlike the other
|
|
26
|
+
* limits, it does not fall back to the options `@fastify/multipart` was
|
|
27
|
+
* registered with.
|
|
28
|
+
*/
|
|
29
|
+
fieldNameSize?: number;
|
|
30
|
+
/** Max field value size (Default: 1MB) */
|
|
31
|
+
fieldSize?: number;
|
|
32
|
+
/** Max number of non-file fields (Default: Infinity) */
|
|
33
|
+
fields?: number;
|
|
34
|
+
/** For multipart forms, the max file size in bytes (Default: the Fastify `bodyLimit`, 1 MiB) */
|
|
35
|
+
fileSize?: number;
|
|
36
|
+
/** For multipart forms, the max number of file fields (Default: Infinity) */
|
|
37
|
+
files?: number;
|
|
38
|
+
/** For multipart forms, the max number of parts (fields + files) (Default: 1000) */
|
|
39
|
+
parts?: number;
|
|
40
|
+
/** For multipart forms, the max number of header key => value pairs to parse (Default: 2000) */
|
|
41
|
+
headerPairs?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Same keys and semantics as platform-express's `MulterOptions`, so option
|
|
45
|
+
* objects can be moved between adapters as-is.
|
|
46
|
+
*
|
|
47
|
+
* @publicApi
|
|
48
|
+
*/
|
|
49
|
+
export interface MultipartOptions {
|
|
50
|
+
/** Where to store the files (disk storage). Ignored when `storage` is set. */
|
|
51
|
+
dest?: string;
|
|
52
|
+
/** The storage engine to use for uploaded files (Default: memory storage). */
|
|
53
|
+
storage?: MultipartStorageEngine;
|
|
54
|
+
/**
|
|
55
|
+
* An object specifying the size limits, or a function of the request that
|
|
56
|
+
* returns one. Merged over the limits `@fastify/multipart` was registered with.
|
|
57
|
+
*/
|
|
58
|
+
limits?: MultipartLimits | ((req: any) => MultipartLimits);
|
|
59
|
+
/** Keep the full path of files instead of just the base name (Default: false) */
|
|
60
|
+
preservePath?: boolean;
|
|
61
|
+
/** Default character set for part header values (e.g. filename) (Default: 'latin1') */
|
|
62
|
+
defParamCharset?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Decides which files are stored. Call `callback(null, false)` to skip a
|
|
65
|
+
* file, or `callback(error, false)` to fail the request with `error`.
|
|
66
|
+
*/
|
|
67
|
+
fileFilter?(req: any, file: IncomingMultipartFile, callback: (error: Error | null, acceptFile: boolean) => void): void;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* @publicApi
|
|
71
|
+
*/
|
|
72
|
+
export interface MultipartField {
|
|
73
|
+
/** The field name. */
|
|
74
|
+
name: string;
|
|
75
|
+
/** Optional maximum number of files per field to accept. */
|
|
76
|
+
maxCount?: number;
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
2
|
+
function parsePath(key) {
|
|
3
|
+
const failure = () => [{ type: 'object', key, last: true }];
|
|
4
|
+
const firstKey = /^[^[]*/.exec(key)[0];
|
|
5
|
+
if (!firstKey) {
|
|
6
|
+
return failure();
|
|
7
|
+
}
|
|
8
|
+
let pos = firstKey.length;
|
|
9
|
+
let tail = { type: 'object', key: firstKey };
|
|
10
|
+
const steps = [tail];
|
|
11
|
+
while (pos < key.length) {
|
|
12
|
+
if (key[pos] === '[' && key[pos + 1] === ']') {
|
|
13
|
+
pos += 2;
|
|
14
|
+
tail.append = true;
|
|
15
|
+
if (pos !== key.length) {
|
|
16
|
+
return failure();
|
|
17
|
+
}
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const rest = key.substring(pos);
|
|
21
|
+
let match = /^\[(\d+)\]/.exec(rest);
|
|
22
|
+
if (match) {
|
|
23
|
+
pos += match[0].length;
|
|
24
|
+
tail.nextType = 'array';
|
|
25
|
+
tail = { type: 'array', key: parseInt(match[1], 10) };
|
|
26
|
+
steps.push(tail);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
match = /^\[([^\]]+)\]/.exec(rest);
|
|
30
|
+
if (match) {
|
|
31
|
+
pos += match[0].length;
|
|
32
|
+
tail.nextType = 'object';
|
|
33
|
+
tail = { type: 'object', key: match[1] };
|
|
34
|
+
steps.push(tail);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
return failure();
|
|
38
|
+
}
|
|
39
|
+
tail.last = true;
|
|
40
|
+
return steps;
|
|
41
|
+
}
|
|
42
|
+
function valueType(value) {
|
|
43
|
+
if (value === undefined) {
|
|
44
|
+
return 'undefined';
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
return 'array';
|
|
48
|
+
}
|
|
49
|
+
if (typeof value === 'object') {
|
|
50
|
+
return 'object';
|
|
51
|
+
}
|
|
52
|
+
return 'scalar';
|
|
53
|
+
}
|
|
54
|
+
function setLastValue(ctx, step, current, value) {
|
|
55
|
+
switch (valueType(current)) {
|
|
56
|
+
case 'undefined':
|
|
57
|
+
ctx[step.key] = step.append ? [value] : value;
|
|
58
|
+
break;
|
|
59
|
+
case 'array':
|
|
60
|
+
ctx[step.key].push(value);
|
|
61
|
+
break;
|
|
62
|
+
case 'object':
|
|
63
|
+
return setLastValue(current, { type: 'object', key: '', last: true }, current[''], value);
|
|
64
|
+
case 'scalar':
|
|
65
|
+
ctx[step.key] = [ctx[step.key], value];
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
return ctx;
|
|
69
|
+
}
|
|
70
|
+
function setValue(ctx, step, current, value) {
|
|
71
|
+
if (step.last) {
|
|
72
|
+
return setLastValue(ctx, step, current, value);
|
|
73
|
+
}
|
|
74
|
+
let obj;
|
|
75
|
+
switch (valueType(current)) {
|
|
76
|
+
case 'undefined':
|
|
77
|
+
ctx[step.key] = step.nextType === 'array' ? [] : Object.create(null);
|
|
78
|
+
return ctx[step.key];
|
|
79
|
+
case 'object':
|
|
80
|
+
return current;
|
|
81
|
+
case 'array':
|
|
82
|
+
if (step.nextType === 'array') {
|
|
83
|
+
return current;
|
|
84
|
+
}
|
|
85
|
+
obj = Object.create(null);
|
|
86
|
+
ctx[step.key] = obj;
|
|
87
|
+
current.forEach((item, i) => {
|
|
88
|
+
if (item !== undefined) {
|
|
89
|
+
obj['' + i] = item;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return obj;
|
|
93
|
+
case 'scalar':
|
|
94
|
+
obj = Object.create(null);
|
|
95
|
+
obj[''] = current;
|
|
96
|
+
ctx[step.key] = obj;
|
|
97
|
+
return obj;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Appends a text field to `store` using multer's bracket notation.
|
|
102
|
+
* Returns `false` (leaving `store` untouched) for names that would pollute a
|
|
103
|
+
* prototype.
|
|
104
|
+
*/
|
|
105
|
+
export function appendField(store, key, value) {
|
|
106
|
+
const steps = parsePath(key);
|
|
107
|
+
if (steps.some(step => FORBIDDEN_KEYS.has(String(step.key)))) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
steps.reduce((ctx, step) => setValue(ctx, step, ctx[step.key], value), store);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type NestInterceptor, type Type } from '@nestjs/common';
|
|
2
|
+
import type { MultipartOptions } from '../interfaces/index.js';
|
|
3
|
+
import { type UploadSpec } from './multipart.parser.js';
|
|
4
|
+
/**
|
|
5
|
+
* Creates an interceptor that parses the multipart body with multer's
|
|
6
|
+
* semantics, as described by `spec`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createMultipartInterceptor(spec: UploadSpec, localOptions?: MultipartOptions): Type<NestInterceptor>;
|
|
9
|
+
/**
|
|
10
|
+
* Creates an interceptor that hands the route handler the stream of a single
|
|
11
|
+
* file in `fieldName`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createMultipartStreamInterceptor(fieldName: string, localOptions?: MultipartOptions): Type<NestInterceptor>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { __decorate, __metadata, __param } from "tslib";
|
|
2
|
+
import { Inject, mixin, Optional, StreamableFile, } from '@nestjs/common';
|
|
3
|
+
import { HttpAdapterHost } from '@nestjs/core';
|
|
4
|
+
import { catchError, concatMap, from, throwError } from 'rxjs';
|
|
5
|
+
import { MULTIPART_MODULE_OPTIONS } from '../files.constants.js';
|
|
6
|
+
import { MISSING_PLUGIN_MESSAGE, PLUGIN_NOT_REGISTERED_AT_STARTUP_MESSAGE, WRONG_ADAPTER_MESSAGE, } from './multipart.constants.js';
|
|
7
|
+
import { drainRequest, processMultipart, processStream, resolveStorage, validateLimits, } from './multipart.parser.js';
|
|
8
|
+
import { mergeMultipartOptions } from './multipart.utils.js';
|
|
9
|
+
const verifiedInstances = new WeakSet();
|
|
10
|
+
/**
|
|
11
|
+
* Asks the FastifyAdapter to register the `@fastify/multipart` plugin (it
|
|
12
|
+
* skips that when the plugin is registered already, or when its
|
|
13
|
+
* `multipart` option is `false`), and fails the application's startup with
|
|
14
|
+
* an actionable message if the plugin still is not registered once every
|
|
15
|
+
* plugin has loaded. Without the plugin, Fastify answers every multipart
|
|
16
|
+
* request with a bare 415 before any interceptor runs.
|
|
17
|
+
*
|
|
18
|
+
* This runs from `onModuleInit`, before Fastify's `ready()`: the plugin
|
|
19
|
+
* registered here loads with the others, before the server starts, and
|
|
20
|
+
* the check is deferred to an `onReady` hook.
|
|
21
|
+
*/
|
|
22
|
+
function setUpHttpAdapter(adapterHost) {
|
|
23
|
+
const httpAdapter = adapterHost?.httpAdapter;
|
|
24
|
+
if (!httpAdapter) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (httpAdapter.getType() !== 'fastify') {
|
|
28
|
+
throw new Error(WRONG_ADAPTER_MESSAGE);
|
|
29
|
+
}
|
|
30
|
+
const instance = httpAdapter.getInstance();
|
|
31
|
+
if (verifiedInstances.has(instance)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const isRegistered = () => instance.hasRequestDecorator('isMultipart');
|
|
35
|
+
try {
|
|
36
|
+
httpAdapter.useMultipart?.();
|
|
37
|
+
instance.addHook('onReady', async () => {
|
|
38
|
+
if (!isRegistered()) {
|
|
39
|
+
throw new Error(MISSING_PLUGIN_MESSAGE);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// The instance has already started (e.g. the interceptor was created
|
|
45
|
+
// by a lazy-loaded module): no plugin can be registered any more, and
|
|
46
|
+
// every plugin is loaded.
|
|
47
|
+
if (!isRegistered()) {
|
|
48
|
+
throw new Error(PLUGIN_NOT_REGISTERED_AT_STARTUP_MESSAGE);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
verifiedInstances.add(instance);
|
|
52
|
+
}
|
|
53
|
+
class MultipartInterceptorHost {
|
|
54
|
+
adapterHost;
|
|
55
|
+
options;
|
|
56
|
+
constructor(moduleOptions = {}, adapterHost, localOptions) {
|
|
57
|
+
this.adapterHost = adapterHost;
|
|
58
|
+
this.options = mergeMultipartOptions(moduleOptions, localOptions);
|
|
59
|
+
if (typeof this.options.limits !== 'function') {
|
|
60
|
+
validateLimits(this.options.limits);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
onModuleInit() {
|
|
64
|
+
setUpHttpAdapter(this.adapterHost);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Creates an interceptor that parses the multipart body with multer's
|
|
69
|
+
* semantics, as described by `spec`.
|
|
70
|
+
*/
|
|
71
|
+
export function createMultipartInterceptor(spec, localOptions) {
|
|
72
|
+
let MixinInterceptor = class MixinInterceptor extends MultipartInterceptorHost {
|
|
73
|
+
// Resolved once per interceptor, as multer does, so that a `dest`
|
|
74
|
+
// folder is created at bootstrap.
|
|
75
|
+
storage;
|
|
76
|
+
constructor(options = {}, adapterHost) {
|
|
77
|
+
super(options, adapterHost, localOptions);
|
|
78
|
+
this.storage = resolveStorage(this.options);
|
|
79
|
+
}
|
|
80
|
+
async intercept(context, next) {
|
|
81
|
+
const req = context.switchToHttp().getRequest();
|
|
82
|
+
await processMultipart(req, spec, this.options, this.storage);
|
|
83
|
+
return next.handle();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
MixinInterceptor = __decorate([
|
|
87
|
+
__param(0, Optional()),
|
|
88
|
+
__param(0, Inject(MULTIPART_MODULE_OPTIONS)),
|
|
89
|
+
__param(1, Optional()),
|
|
90
|
+
__param(1, Inject(HttpAdapterHost)),
|
|
91
|
+
__metadata("design:paramtypes", [Object, HttpAdapterHost])
|
|
92
|
+
], MixinInterceptor);
|
|
93
|
+
return mixin(MixinInterceptor);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Creates an interceptor that hands the route handler the stream of a single
|
|
97
|
+
* file in `fieldName`.
|
|
98
|
+
*/
|
|
99
|
+
export function createMultipartStreamInterceptor(fieldName, localOptions) {
|
|
100
|
+
let MixinInterceptor = class MixinInterceptor extends MultipartInterceptorHost {
|
|
101
|
+
constructor(options = {}, adapterHost) {
|
|
102
|
+
super(options, adapterHost, localOptions);
|
|
103
|
+
}
|
|
104
|
+
async intercept(context, next) {
|
|
105
|
+
const ctx = context.switchToHttp();
|
|
106
|
+
const req = ctx.getRequest();
|
|
107
|
+
const reply = ctx.getResponse();
|
|
108
|
+
await processStream(req, fieldName, this.options);
|
|
109
|
+
// Whatever the handler leaves unread is discarded before the response
|
|
110
|
+
// is sent, as multer does: responding while the client is still
|
|
111
|
+
// uploading makes Node.js close the connection under it (EPIPE /
|
|
112
|
+
// ECONNRESET on the client) when it is not kept alive, and leaves a
|
|
113
|
+
// kept-alive connection with an unread body. A StreamableFile
|
|
114
|
+
// response is the exception, as it may be the upload itself, which
|
|
115
|
+
// only drains while the response is sent; what it leaves unread is
|
|
116
|
+
// discarded once the response is out.
|
|
117
|
+
reply.raw.once('close', () => void drainRequest(req.raw));
|
|
118
|
+
const drainBefore = (result) => from(drainRequest(req.raw).then(() => result));
|
|
119
|
+
return next.handle().pipe(concatMap(result => result instanceof StreamableFile ? [result] : drainBefore(result)), catchError(err => drainBefore(undefined).pipe(concatMap(() => throwError(() => err)))));
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
MixinInterceptor = __decorate([
|
|
123
|
+
__param(0, Optional()),
|
|
124
|
+
__param(0, Inject(MULTIPART_MODULE_OPTIONS)),
|
|
125
|
+
__param(1, Optional()),
|
|
126
|
+
__param(1, Inject(HttpAdapterHost)),
|
|
127
|
+
__metadata("design:paramtypes", [Object, HttpAdapterHost])
|
|
128
|
+
], MixinInterceptor);
|
|
129
|
+
return mixin(MixinInterceptor);
|
|
130
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same codes and messages as multer's `MulterError`, copied from
|
|
3
|
+
* `@nestjs/platform-express` (`multer/multer/multer.constants.ts`) so that
|
|
4
|
+
* clients see identical error responses on both adapters without this
|
|
5
|
+
* package depending on platform-express.
|
|
6
|
+
*/
|
|
7
|
+
export declare const multerExceptions: {
|
|
8
|
+
readonly LIMIT_PART_COUNT: "Too many parts";
|
|
9
|
+
readonly LIMIT_FILE_SIZE: "File too large";
|
|
10
|
+
readonly LIMIT_FILE_COUNT: "Too many files";
|
|
11
|
+
readonly LIMIT_FIELD_KEY: "Field name too long";
|
|
12
|
+
readonly LIMIT_FIELD_VALUE: "Field value too long";
|
|
13
|
+
readonly LIMIT_FIELD_COUNT: "Too many fields";
|
|
14
|
+
readonly LIMIT_UNEXPECTED_FILE: "Unexpected file field";
|
|
15
|
+
readonly MISSING_FIELD_NAME: "Field name missing";
|
|
16
|
+
readonly LIMIT_FIELD_NESTING: "Field name nesting too deep";
|
|
17
|
+
readonly INVALID_FIELD_NAME: "Invalid field name";
|
|
18
|
+
};
|
|
19
|
+
export type MultipartErrorCode = keyof typeof multerExceptions;
|
|
20
|
+
/**
|
|
21
|
+
* The messages platform-express reports for busboy 1.x parser errors
|
|
22
|
+
* (prefixed with "Multipart: "), which is what multer bundles.
|
|
23
|
+
*/
|
|
24
|
+
export declare const busboyExceptions: {
|
|
25
|
+
readonly MULTIPART_BOUNDARY_NOT_FOUND: "Multipart: Boundary not found";
|
|
26
|
+
readonly MULTIPART_UNEXPECTED_END_OF_FORM: "Multipart: Unexpected end of form";
|
|
27
|
+
readonly MULTIPART_UNEXPECTED_END_OF_FILE: "Multipart: Unexpected end of file";
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* `@fastify/busboy` raises plain `Error`s without a `code`, so these are the
|
|
31
|
+
* only messages matched verbatim. Everything `@fastify/multipart` raises is
|
|
32
|
+
* matched by its `code` instead (see `fastifyMultipartErrorCodes`).
|
|
33
|
+
*
|
|
34
|
+
* @see https://github.com/fastify/busboy/blob/main/deps/dicer/lib/Dicer.js
|
|
35
|
+
*/
|
|
36
|
+
export declare const fastifyBusboyMessages: {
|
|
37
|
+
readonly BOUNDARY_NOT_FOUND: "Multipart: Boundary not found";
|
|
38
|
+
readonly UNEXPECTED_END_OF_FORM: "Unexpected end of multipart data";
|
|
39
|
+
readonly PART_TERMINATED_EARLY: "terminated early due to unexpected end";
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* `@fastify/multipart` error codes (created with `@fastify/error`) mapped to
|
|
43
|
+
* the multer code that describes the same condition.
|
|
44
|
+
*
|
|
45
|
+
* @see https://github.com/fastify/fastify-multipart/blob/main/index.js
|
|
46
|
+
*/
|
|
47
|
+
export declare const fastifyMultipartErrorCodes: Record<string, MultipartErrorCode>;
|
|
48
|
+
/**
|
|
49
|
+
* `@fastify/multipart` error codes that already describe a client error and
|
|
50
|
+
* are reported as a 400 with their own message.
|
|
51
|
+
*/
|
|
52
|
+
export declare const fastifyMultipartBadRequestCodes: string[];
|
|
53
|
+
export declare const MISSING_PLUGIN_MESSAGE: string;
|
|
54
|
+
export declare const PLUGIN_NOT_REGISTERED_AT_STARTUP_MESSAGE: string;
|
|
55
|
+
export declare const WRONG_ADAPTER_MESSAGE: string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same codes and messages as multer's `MulterError`, copied from
|
|
3
|
+
* `@nestjs/platform-express` (`multer/multer/multer.constants.ts`) so that
|
|
4
|
+
* clients see identical error responses on both adapters without this
|
|
5
|
+
* package depending on platform-express.
|
|
6
|
+
*/
|
|
7
|
+
export const multerExceptions = {
|
|
8
|
+
// from https://github.com/expressjs/multer/blob/master/lib/multer-error.js
|
|
9
|
+
LIMIT_PART_COUNT: 'Too many parts',
|
|
10
|
+
LIMIT_FILE_SIZE: 'File too large',
|
|
11
|
+
LIMIT_FILE_COUNT: 'Too many files',
|
|
12
|
+
LIMIT_FIELD_KEY: 'Field name too long',
|
|
13
|
+
LIMIT_FIELD_VALUE: 'Field value too long',
|
|
14
|
+
LIMIT_FIELD_COUNT: 'Too many fields',
|
|
15
|
+
LIMIT_UNEXPECTED_FILE: 'Unexpected file field',
|
|
16
|
+
MISSING_FIELD_NAME: 'Field name missing',
|
|
17
|
+
LIMIT_FIELD_NESTING: 'Field name nesting too deep',
|
|
18
|
+
// A multer 2.4 code that platform-express's table lacks. Raised here for
|
|
19
|
+
// field names that would pollute a prototype, which multer accepts.
|
|
20
|
+
INVALID_FIELD_NAME: 'Invalid field name',
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The messages platform-express reports for busboy 1.x parser errors
|
|
24
|
+
* (prefixed with "Multipart: "), which is what multer bundles.
|
|
25
|
+
*/
|
|
26
|
+
export const busboyExceptions = {
|
|
27
|
+
MULTIPART_BOUNDARY_NOT_FOUND: 'Multipart: Boundary not found',
|
|
28
|
+
MULTIPART_UNEXPECTED_END_OF_FORM: 'Multipart: Unexpected end of form',
|
|
29
|
+
MULTIPART_UNEXPECTED_END_OF_FILE: 'Multipart: Unexpected end of file',
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* `@fastify/busboy` raises plain `Error`s without a `code`, so these are the
|
|
33
|
+
* only messages matched verbatim. Everything `@fastify/multipart` raises is
|
|
34
|
+
* matched by its `code` instead (see `fastifyMultipartErrorCodes`).
|
|
35
|
+
*
|
|
36
|
+
* @see https://github.com/fastify/busboy/blob/main/deps/dicer/lib/Dicer.js
|
|
37
|
+
*/
|
|
38
|
+
export const fastifyBusboyMessages = {
|
|
39
|
+
BOUNDARY_NOT_FOUND: 'Multipart: Boundary not found',
|
|
40
|
+
// Emitted on the parser when the body ends early...
|
|
41
|
+
UNEXPECTED_END_OF_FORM: 'Unexpected end of multipart data',
|
|
42
|
+
// ...and on the file stream that was being read at that moment. Both are
|
|
43
|
+
// reported as "Unexpected end of form", as multer does for a truncated body.
|
|
44
|
+
PART_TERMINATED_EARLY: 'terminated early due to unexpected end',
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* `@fastify/multipart` error codes (created with `@fastify/error`) mapped to
|
|
48
|
+
* the multer code that describes the same condition.
|
|
49
|
+
*
|
|
50
|
+
* @see https://github.com/fastify/fastify-multipart/blob/main/index.js
|
|
51
|
+
*/
|
|
52
|
+
export const fastifyMultipartErrorCodes = {
|
|
53
|
+
FST_PARTS_LIMIT: 'LIMIT_PART_COUNT',
|
|
54
|
+
FST_FILES_LIMIT: 'LIMIT_FILE_COUNT',
|
|
55
|
+
FST_FIELDS_LIMIT: 'LIMIT_FIELD_COUNT',
|
|
56
|
+
FST_REQ_FILE_TOO_LARGE: 'LIMIT_FILE_SIZE',
|
|
57
|
+
FST_PROTO_VIOLATION: 'INVALID_FIELD_NAME',
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* `@fastify/multipart` error codes that already describe a client error and
|
|
61
|
+
* are reported as a 400 with their own message.
|
|
62
|
+
*/
|
|
63
|
+
export const fastifyMultipartBadRequestCodes = [
|
|
64
|
+
'FST_MP_PREMATURE_CLOSE',
|
|
65
|
+
'FST_INVALID_JSON_FIELD_ERROR',
|
|
66
|
+
];
|
|
67
|
+
export const MISSING_PLUGIN_MESSAGE = 'File upload interceptors from "@nestjs/platform-fastify/multipart" require the "@fastify/multipart" plugin, ' +
|
|
68
|
+
'which the FastifyAdapter registers unless its "multipart" option is false. ' +
|
|
69
|
+
'Either remove that option, or register the plugin yourself (`await app.register(multipart)`).';
|
|
70
|
+
export const PLUGIN_NOT_REGISTERED_AT_STARTUP_MESSAGE = 'File upload interceptors from "@nestjs/platform-fastify/multipart" require the "@fastify/multipart" plugin, ' +
|
|
71
|
+
'which was not registered before the Fastify instance started. ' +
|
|
72
|
+
'Register it at startup: `new FastifyAdapter({ multipart: true })`, or `await app.register(multipart)`.';
|
|
73
|
+
export const WRONG_ADAPTER_MESSAGE = 'File upload interceptors from "@nestjs/platform-fastify/multipart" only work with the FastifyAdapter. ' +
|
|
74
|
+
'With the ExpressAdapter, import them from "@nestjs/platform-express" instead.';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type MultipartErrorCode } from './multipart.constants.js';
|
|
2
|
+
/**
|
|
3
|
+
* Mirrors multer's `MulterError`: identified by `code`, and may carry the
|
|
4
|
+
* name of the offending `field`.
|
|
5
|
+
*/
|
|
6
|
+
export declare class MultipartError extends Error {
|
|
7
|
+
readonly code: MultipartErrorCode;
|
|
8
|
+
readonly field?: string | undefined;
|
|
9
|
+
constructor(code: MultipartErrorCode, field?: string | undefined);
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { multerExceptions, } from './multipart.constants.js';
|
|
2
|
+
/**
|
|
3
|
+
* Mirrors multer's `MulterError`: identified by `code`, and may carry the
|
|
4
|
+
* name of the offending `field`.
|
|
5
|
+
*/
|
|
6
|
+
export class MultipartError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
field;
|
|
9
|
+
constructor(code, field) {
|
|
10
|
+
super(multerExceptions[code]);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.field = field;
|
|
13
|
+
this.name = 'MultipartError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { FastifyRequest } from 'fastify';
|
|
2
|
+
import type { IncomingMessage } from 'http';
|
|
3
|
+
import type { MultipartField, MultipartLimits, MultipartOptions, MultipartStorageEngine } from '../interfaces/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Where accepted files end up, named after multer's `FileAppender` strategies.
|
|
6
|
+
*/
|
|
7
|
+
export type FileStrategy = 'VALUE' | 'ARRAY' | 'OBJECT' | 'NONE';
|
|
8
|
+
export interface UploadSpec {
|
|
9
|
+
/** Accepted file fields; `'ANY'` accepts every field without a count limit. */
|
|
10
|
+
fields: MultipartField[] | 'ANY';
|
|
11
|
+
strategy: FileStrategy;
|
|
12
|
+
}
|
|
13
|
+
/** Same resolution order as multer: `storage`, then `dest`, then memory. */
|
|
14
|
+
export declare function resolveStorage(options: MultipartOptions): MultipartStorageEngine;
|
|
15
|
+
/**
|
|
16
|
+
* Stops parsing and discards the rest of the body, so that an error response
|
|
17
|
+
* does not race a client that is still uploading (multer does the same
|
|
18
|
+
* before it reports an error).
|
|
19
|
+
*/
|
|
20
|
+
export declare function drainRequest(raw: IncomingMessage): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Rejects limits that are not a non-negative integer or `Infinity`, as
|
|
23
|
+
* multer does: busboy compares most limits with strict equality, so such a
|
|
24
|
+
* value would silently disable the limit.
|
|
25
|
+
*/
|
|
26
|
+
export declare function validateLimits(limits: MultipartLimits | undefined): void;
|
|
27
|
+
/**
|
|
28
|
+
* Parses the multipart body with multer's semantics and populates
|
|
29
|
+
* `req.body` and `req.file` / `req.files`. Requests that are not multipart
|
|
30
|
+
* are passed through untouched, as multer does.
|
|
31
|
+
*/
|
|
32
|
+
export declare function processMultipart(request: FastifyRequest, spec: UploadSpec, options: MultipartOptions, storage: MultipartStorageEngine): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Parses up to and including the first file in `fieldName`, then stops and
|
|
35
|
+
* hands that file's stream to the route handler (`req.file.stream`). Text
|
|
36
|
+
* fields sent before the file are in `req.body`; anything after it is not
|
|
37
|
+
* parsed.
|
|
38
|
+
*/
|
|
39
|
+
export declare function processStream(request: FastifyRequest, fieldName: string, options: MultipartOptions): Promise<void>;
|