@nestjs/platform-fastify 12.1.2 → 12.2.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 +58 -11
- package/adapters/fastify-adapter.js +430 -53
- package/adapters/utils/get-media-type-version.util.d.ts +4 -0
- package/adapters/utils/get-media-type-version.util.js +37 -3
- package/multipart/interfaces/multipart-options.interface.d.ts +19 -5
- package/multipart/multipart/append-field.util.d.ts +3 -2
- package/multipart/multipart/append-field.util.js +18 -7
- package/multipart/multipart/multipart-interceptor.factory.js +20 -4
- package/multipart/multipart/multipart.constants.d.ts +14 -0
- package/multipart/multipart/multipart.constants.js +14 -0
- package/multipart/multipart/multipart.parser.d.ts +6 -4
- package/multipart/multipart/multipart.parser.js +57 -11
- package/multipart/multipart/multipart.utils.js +5 -0
- package/multipart/storage/disk.storage.js +27 -2
- package/multipart/storage/memory.storage.js +12 -4
- package/package.json +9 -8
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* media range of a list (`text/html, application/json;v=2`); the first match
|
|
7
7
|
* wins. Returns `undefined` when no parameter starts with the key.
|
|
8
8
|
*
|
|
9
|
+
* Quoted parameter values are unescaped, and separators inside them are ignored.
|
|
10
|
+
* An unterminated quoted string runs to the end of the header value, so a
|
|
11
|
+
* version that follows its opening quote is not read.
|
|
12
|
+
*
|
|
9
13
|
* Duplicated in each platform package so that a platform package never
|
|
10
14
|
* depends on a newer `@nestjs/core` than the one installed.
|
|
11
15
|
*/
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splits a header value at separators outside quoted strings in linear time.
|
|
3
|
+
* Backslash-escaped characters inside quoted strings are treated as literals.
|
|
4
|
+
*/
|
|
5
|
+
const splitOutsideQuotes = (value, separator) => {
|
|
6
|
+
const parts = [];
|
|
7
|
+
let start = 0;
|
|
8
|
+
let isQuoted = false;
|
|
9
|
+
const len = value.length;
|
|
10
|
+
for (let index = 0; index < len; index++) {
|
|
11
|
+
const character = value[index];
|
|
12
|
+
if (isQuoted && character === '\\') {
|
|
13
|
+
// A quoted-pair consumes the next character, including an escaped quote.
|
|
14
|
+
index++;
|
|
15
|
+
}
|
|
16
|
+
else if (character === '"') {
|
|
17
|
+
isQuoted = !isQuoted;
|
|
18
|
+
}
|
|
19
|
+
else if (character === separator && !isQuoted) {
|
|
20
|
+
parts.push(value.slice(start, index));
|
|
21
|
+
start = index + 1;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
parts.push(value.slice(start));
|
|
25
|
+
return parts;
|
|
26
|
+
};
|
|
1
27
|
/**
|
|
2
28
|
* Returns the version carried by an `Accept` header for media type versioning,
|
|
3
29
|
* e.g. `2` for `application/json;v=2` with the `v=` key.
|
|
@@ -6,19 +32,27 @@
|
|
|
6
32
|
* media range of a list (`text/html, application/json;v=2`); the first match
|
|
7
33
|
* wins. Returns `undefined` when no parameter starts with the key.
|
|
8
34
|
*
|
|
35
|
+
* Quoted parameter values are unescaped, and separators inside them are ignored.
|
|
36
|
+
* An unterminated quoted string runs to the end of the header value, so a
|
|
37
|
+
* version that follows its opening quote is not read.
|
|
38
|
+
*
|
|
9
39
|
* Duplicated in each platform package so that a platform package never
|
|
10
40
|
* depends on a newer `@nestjs/core` than the one installed.
|
|
11
41
|
*/
|
|
12
42
|
export function getMediaTypeVersion(acceptHeader, key) {
|
|
13
43
|
const mediaRanges = []
|
|
14
44
|
.concat(acceptHeader ?? [])
|
|
15
|
-
.flatMap(value => value
|
|
45
|
+
.flatMap(value => splitOutsideQuotes(value, ','));
|
|
16
46
|
for (const mediaRange of mediaRanges) {
|
|
17
|
-
const [, ...parameters] = mediaRange
|
|
47
|
+
const [, ...parameters] = splitOutsideQuotes(mediaRange, ';');
|
|
18
48
|
for (const parameter of parameters) {
|
|
19
49
|
const trimmedParameter = parameter.trim();
|
|
20
50
|
if (trimmedParameter.startsWith(key)) {
|
|
21
|
-
|
|
51
|
+
const version = trimmedParameter.slice(key.length);
|
|
52
|
+
if (version.startsWith('"') && version.endsWith('"')) {
|
|
53
|
+
return version.slice(1, -1).replace(/\\([\s\S])/g, '$1');
|
|
54
|
+
}
|
|
55
|
+
return version;
|
|
22
56
|
}
|
|
23
57
|
}
|
|
24
58
|
}
|
|
@@ -12,11 +12,11 @@ export interface MultipartStorageEngine {
|
|
|
12
12
|
/**
|
|
13
13
|
* Same keys as platform-express's `MulterLimits`.
|
|
14
14
|
*
|
|
15
|
-
* Limits that are not set
|
|
16
|
-
* registered with, and then to its own
|
|
17
|
-
* defaults `fileSize` and `parts` to
|
|
18
|
-
* `
|
|
19
|
-
* and `parts` at 1000.
|
|
15
|
+
* Limits that are not set, or set to `null` or `undefined`, fall back to the
|
|
16
|
+
* options `@fastify/multipart` was registered with, and then to its own
|
|
17
|
+
* defaults. Unlike multer (which defaults `fileSize` and `parts` to
|
|
18
|
+
* `Infinity`), `@fastify/multipart` caps `fileSize` at the Fastify
|
|
19
|
+
* instance's `bodyLimit` (1 MiB unless configured) and `parts` at 1000.
|
|
20
20
|
*
|
|
21
21
|
* @publicApi
|
|
22
22
|
*/
|
|
@@ -39,6 +39,20 @@ export interface MultipartLimits {
|
|
|
39
39
|
parts?: number;
|
|
40
40
|
/** For multipart forms, the max number of header key => value pairs to parse (Default: 2000) */
|
|
41
41
|
headerPairs?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Max nesting depth of field names; `a[b][c]` has 2 levels (Default: 32,
|
|
44
|
+
* where multer's own default is Infinity). Set it to `Infinity` to lift
|
|
45
|
+
* the limit. Like `fieldNameSize`, it does not fall back to the options
|
|
46
|
+
* `@fastify/multipart` was registered with.
|
|
47
|
+
*/
|
|
48
|
+
fieldNestingDepth?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Max numeric array index accepted in field names; `a[3]` uses index 3
|
|
51
|
+
* (Default: 1000, where multer's own default is Infinity). Set it to
|
|
52
|
+
* `Infinity` to lift the limit. Like `fieldNameSize`, it does not fall back
|
|
53
|
+
* to the options `@fastify/multipart` was registered with.
|
|
54
|
+
*/
|
|
55
|
+
fieldArrayIndexLimit?: number;
|
|
42
56
|
}
|
|
43
57
|
/**
|
|
44
58
|
* Same keys and semantics as platform-express's `MulterOptions`, so option
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Appends a text field to `store` using multer's bracket notation.
|
|
3
|
-
* Returns `false`
|
|
4
|
-
* prototype
|
|
3
|
+
* Returns `false` for names it cannot apply: names that would pollute a
|
|
4
|
+
* prototype (leaving `store` untouched) and appends to an array that already
|
|
5
|
+
* has the maximum length.
|
|
5
6
|
*/
|
|
6
7
|
export declare function appendField(store: object, key: string, value: unknown): boolean;
|
|
@@ -84,11 +84,13 @@ function setValue(ctx, step, current, value) {
|
|
|
84
84
|
}
|
|
85
85
|
obj = Object.create(null);
|
|
86
86
|
ctx[step.key] = obj;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
// Only the indices that hold a value, so that the work depends on the
|
|
88
|
+
// number of items rather than on the length of a sparse array.
|
|
89
|
+
for (const index of Object.keys(current)) {
|
|
90
|
+
if (current[index] !== undefined) {
|
|
91
|
+
obj[index] = current[index];
|
|
90
92
|
}
|
|
91
|
-
}
|
|
93
|
+
}
|
|
92
94
|
return obj;
|
|
93
95
|
case 'scalar':
|
|
94
96
|
obj = Object.create(null);
|
|
@@ -99,14 +101,23 @@ function setValue(ctx, step, current, value) {
|
|
|
99
101
|
}
|
|
100
102
|
/**
|
|
101
103
|
* Appends a text field to `store` using multer's bracket notation.
|
|
102
|
-
* Returns `false`
|
|
103
|
-
* prototype
|
|
104
|
+
* Returns `false` for names it cannot apply: names that would pollute a
|
|
105
|
+
* prototype (leaving `store` untouched) and appends to an array that already
|
|
106
|
+
* has the maximum length.
|
|
104
107
|
*/
|
|
105
108
|
export function appendField(store, key, value) {
|
|
106
109
|
const steps = parsePath(key);
|
|
107
110
|
if (steps.some(step => FORBIDDEN_KEYS.has(String(step.key)))) {
|
|
108
111
|
return false;
|
|
109
112
|
}
|
|
110
|
-
|
|
113
|
+
try {
|
|
114
|
+
steps.reduce((ctx, step) => setValue(ctx, step, ctx[step.key], value), store);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (err instanceof RangeError) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
111
122
|
return true;
|
|
112
123
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { __decorate, __metadata, __param } from "tslib";
|
|
2
2
|
import { Inject, mixin, Optional, StreamableFile, } from '@nestjs/common';
|
|
3
3
|
import { HttpAdapterHost } from '@nestjs/core';
|
|
4
|
+
import { isRejectedByPipes } from '@nestjs/core/internal';
|
|
4
5
|
import { catchError, concatMap, from, throwError } from 'rxjs';
|
|
6
|
+
import { finished } from 'stream';
|
|
5
7
|
import { MULTIPART_MODULE_OPTIONS } from '../files.constants.js';
|
|
6
8
|
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';
|
|
9
|
+
import { drainRequest, processMultipart, processStream, removeFiles, resolveStorage, validateLimits, } from './multipart.parser.js';
|
|
8
10
|
import { mergeMultipartOptions } from './multipart.utils.js';
|
|
9
11
|
const verifiedInstances = new WeakSet();
|
|
10
12
|
/**
|
|
@@ -78,9 +80,23 @@ export function createMultipartInterceptor(spec, localOptions) {
|
|
|
78
80
|
this.storage = resolveStorage(this.options);
|
|
79
81
|
}
|
|
80
82
|
async intercept(context, next) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
const ctx = context.switchToHttp();
|
|
84
|
+
const req = ctx.getRequest();
|
|
85
|
+
const stored = await processMultipart(req, spec, this.options, this.storage);
|
|
86
|
+
return next.handle().pipe(
|
|
87
|
+
// When the route's pipes reject the request (e.g. `ParseFilePipe`),
|
|
88
|
+
// the route handler never sees the files, and nothing else would
|
|
89
|
+
// remove them. They are removed once the response has been sent, as
|
|
90
|
+
// an exception filter may still use (or remove) them.
|
|
91
|
+
catchError(err => {
|
|
92
|
+
if (isRejectedByPipes(req)) {
|
|
93
|
+
const reply = ctx.getResponse();
|
|
94
|
+
finished(reply.raw, () => {
|
|
95
|
+
removeFiles(this.storage, req, stored).catch(() => undefined);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return throwError(() => err);
|
|
99
|
+
}));
|
|
84
100
|
}
|
|
85
101
|
};
|
|
86
102
|
MixinInterceptor = __decorate([
|
|
@@ -14,9 +14,23 @@ export declare const multerExceptions: {
|
|
|
14
14
|
readonly LIMIT_UNEXPECTED_FILE: "Unexpected file field";
|
|
15
15
|
readonly MISSING_FIELD_NAME: "Field name missing";
|
|
16
16
|
readonly LIMIT_FIELD_NESTING: "Field name nesting too deep";
|
|
17
|
+
readonly LIMIT_FIELD_ARRAY_INDEX: "Field name array index too large";
|
|
17
18
|
readonly INVALID_FIELD_NAME: "Invalid field name";
|
|
18
19
|
};
|
|
19
20
|
export type MultipartErrorCode = keyof typeof multerExceptions;
|
|
21
|
+
/**
|
|
22
|
+
* Defaults of the field name limits that multer leaves at `Infinity`, which
|
|
23
|
+
* let a single small request build an arbitrarily deep `req.body`
|
|
24
|
+
* (`fieldNestingDepth`) or arrays of arbitrary length (`fieldArrayIndexLimit`).
|
|
25
|
+
* `32` is the nesting depth `body-parser` allows in the urlencoded bodies the
|
|
26
|
+
* ExpressAdapter parses; `1000` is the number of parameters it parses, and
|
|
27
|
+
* the number of parts `@fastify/multipart` accepts. Kept identical to
|
|
28
|
+
* platform-express's `DEFAULT_FIELD_NAME_LIMITS`.
|
|
29
|
+
*/
|
|
30
|
+
export declare const DEFAULT_FIELD_NAME_LIMITS: {
|
|
31
|
+
readonly fieldNestingDepth: 32;
|
|
32
|
+
readonly fieldArrayIndexLimit: 1000;
|
|
33
|
+
};
|
|
20
34
|
/**
|
|
21
35
|
* The messages platform-express reports for busboy 1.x parser errors
|
|
22
36
|
* (prefixed with "Multipart: "), which is what multer bundles.
|
|
@@ -15,10 +15,24 @@ export const multerExceptions = {
|
|
|
15
15
|
LIMIT_UNEXPECTED_FILE: 'Unexpected file field',
|
|
16
16
|
MISSING_FIELD_NAME: 'Field name missing',
|
|
17
17
|
LIMIT_FIELD_NESTING: 'Field name nesting too deep',
|
|
18
|
+
LIMIT_FIELD_ARRAY_INDEX: 'Field name array index too large',
|
|
18
19
|
// A multer 2.4 code that platform-express's table lacks. Raised here for
|
|
19
20
|
// field names that would pollute a prototype, which multer accepts.
|
|
20
21
|
INVALID_FIELD_NAME: 'Invalid field name',
|
|
21
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Defaults of the field name limits that multer leaves at `Infinity`, which
|
|
25
|
+
* let a single small request build an arbitrarily deep `req.body`
|
|
26
|
+
* (`fieldNestingDepth`) or arrays of arbitrary length (`fieldArrayIndexLimit`).
|
|
27
|
+
* `32` is the nesting depth `body-parser` allows in the urlencoded bodies the
|
|
28
|
+
* ExpressAdapter parses; `1000` is the number of parameters it parses, and
|
|
29
|
+
* the number of parts `@fastify/multipart` accepts. Kept identical to
|
|
30
|
+
* platform-express's `DEFAULT_FIELD_NAME_LIMITS`.
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_FIELD_NAME_LIMITS = {
|
|
33
|
+
fieldNestingDepth: 32,
|
|
34
|
+
fieldArrayIndexLimit: 1000,
|
|
35
|
+
};
|
|
22
36
|
/**
|
|
23
37
|
* The messages platform-express reports for busboy 1.x parser errors
|
|
24
38
|
* (prefixed with "Multipart: "), which is what multer bundles.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FastifyRequest } from 'fastify';
|
|
2
2
|
import type { IncomingMessage } from 'http';
|
|
3
|
-
import type { MultipartField, MultipartLimits, MultipartOptions, MultipartStorageEngine } from '../interfaces/index.js';
|
|
3
|
+
import type { MultipartField, MultipartLimits, MultipartOptions, MultipartStorageEngine, UploadedMultipartFile } from '../interfaces/index.js';
|
|
4
4
|
/**
|
|
5
5
|
* Where accepted files end up, named after multer's `FileAppender` strategies.
|
|
6
6
|
*/
|
|
@@ -12,6 +12,7 @@ export interface UploadSpec {
|
|
|
12
12
|
}
|
|
13
13
|
/** Same resolution order as multer: `storage`, then `dest`, then memory. */
|
|
14
14
|
export declare function resolveStorage(options: MultipartOptions): MultipartStorageEngine;
|
|
15
|
+
export declare function removeFiles(storage: MultipartStorageEngine, req: FastifyRequest, files: UploadedMultipartFile[]): Promise<void[]>;
|
|
15
16
|
/**
|
|
16
17
|
* Stops parsing and discards the rest of the body, so that an error response
|
|
17
18
|
* does not race a client that is still uploading (multer does the same
|
|
@@ -26,10 +27,11 @@ export declare function drainRequest(raw: IncomingMessage): Promise<void>;
|
|
|
26
27
|
export declare function validateLimits(limits: MultipartLimits | undefined): void;
|
|
27
28
|
/**
|
|
28
29
|
* Parses the multipart body with multer's semantics and populates
|
|
29
|
-
* `req.body` and `req.file` / `req.files
|
|
30
|
-
* are passed through untouched, as multer
|
|
30
|
+
* `req.body` and `req.file` / `req.files`, and returns the stored files.
|
|
31
|
+
* Requests that are not multipart are passed through untouched, as multer
|
|
32
|
+
* does.
|
|
31
33
|
*/
|
|
32
|
-
export declare function processMultipart(request: FastifyRequest, spec: UploadSpec, options: MultipartOptions, storage: MultipartStorageEngine): Promise<
|
|
34
|
+
export declare function processMultipart(request: FastifyRequest, spec: UploadSpec, options: MultipartOptions, storage: MultipartStorageEngine): Promise<UploadedMultipartFile[]>;
|
|
33
35
|
/**
|
|
34
36
|
* Parses up to and including the first file in `fieldName`, then stops and
|
|
35
37
|
* hands that file's stream to the route handler (`req.file.stream`). Text
|
|
@@ -2,7 +2,7 @@ import { BadRequestException, PayloadTooLargeException } from '@nestjs/common';
|
|
|
2
2
|
import { diskStorage } from '../storage/disk.storage.js';
|
|
3
3
|
import { memoryStorage } from '../storage/memory.storage.js';
|
|
4
4
|
import { appendField } from './append-field.util.js';
|
|
5
|
-
import { busboyExceptions, MISSING_PLUGIN_MESSAGE, multerExceptions, } from './multipart.constants.js';
|
|
5
|
+
import { busboyExceptions, DEFAULT_FIELD_NAME_LIMITS, MISSING_PLUGIN_MESSAGE, multerExceptions, } from './multipart.constants.js';
|
|
6
6
|
import { MultipartError } from './multipart.error.js';
|
|
7
7
|
import { transformException } from './multipart.utils.js';
|
|
8
8
|
/** Same resolution order as multer: `storage`, then `dest`, then memory. */
|
|
@@ -22,15 +22,18 @@ function assertPluginRegistered(req) {
|
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Per-route options handed to `req.parts()`; `@fastify/multipart` deep-merges
|
|
25
|
-
* them over the options the plugin was registered with.
|
|
26
|
-
*
|
|
25
|
+
* them over the options the plugin was registered with. Limits set to `null`
|
|
26
|
+
* or `undefined` are left out, as the merge would copy them over the
|
|
27
|
+
* plugin's limits (and its `fileSize` and `parts` defaults), which
|
|
28
|
+
* `@fastify/busboy` then reads as no limit. `@fastify/busboy` treats
|
|
29
|
+
* `fileSize` and `parts` as "max allowed" (it fires once a limit is
|
|
27
30
|
* exceeded), so unlike multer there is no `+1` adjustment. Truncation is
|
|
28
31
|
* detected here (`file.truncated`), so the plugin's own throw is disabled.
|
|
29
32
|
*/
|
|
30
33
|
function toBusboyConfig(limits, options) {
|
|
31
34
|
const config = { throwFileSizeLimit: false };
|
|
32
35
|
if (limits) {
|
|
33
|
-
config.limits =
|
|
36
|
+
config.limits = Object.fromEntries(Object.entries(limits).filter(([, value]) => value != null));
|
|
34
37
|
}
|
|
35
38
|
if (options.preservePath !== undefined) {
|
|
36
39
|
config.preservePath = options.preservePath;
|
|
@@ -86,9 +89,12 @@ const ABORTED = Symbol('aborted');
|
|
|
86
89
|
* the engine (e.g. by a `pipeline()` to a file that cannot be written), which
|
|
87
90
|
* reports that error itself.
|
|
88
91
|
*
|
|
89
|
-
* Whatever the engine stores after an abort is removed
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
+
* Whatever the engine stores after an abort is removed, as multer does. The
|
|
93
|
+
* file at `path` (which engines set before writing, by multer's convention)
|
|
94
|
+
* is removed right away, as an engine waiting on `pipeline()` never calls
|
|
95
|
+
* back for a stream destroyed after its last chunk; removing it clears
|
|
96
|
+
* `path`. What the engine stored is removed once it calls back: what it
|
|
97
|
+
* returned when it succeeds, and the file at `path` when it fails.
|
|
92
98
|
*/
|
|
93
99
|
function storeFile(storage, req, file) {
|
|
94
100
|
return new Promise((resolve, reject) => {
|
|
@@ -101,6 +107,10 @@ function storeFile(storage, req, file) {
|
|
|
101
107
|
if (!stream.readableEnded && !stream.errored) {
|
|
102
108
|
aborted = true;
|
|
103
109
|
resolve(ABORTED);
|
|
110
|
+
const partial = file;
|
|
111
|
+
if (partial.path) {
|
|
112
|
+
removeFiles(storage, req, [partial]).catch(() => undefined);
|
|
113
|
+
}
|
|
104
114
|
}
|
|
105
115
|
};
|
|
106
116
|
stream.once('close', onClose);
|
|
@@ -116,7 +126,7 @@ function storeFile(storage, req, file) {
|
|
|
116
126
|
});
|
|
117
127
|
});
|
|
118
128
|
}
|
|
119
|
-
function removeFiles(storage, req, files) {
|
|
129
|
+
export function removeFiles(storage, req, files) {
|
|
120
130
|
return Promise.all(files.map(file => new Promise(resolve => storage._removeFile(req, file, () => resolve()))));
|
|
121
131
|
}
|
|
122
132
|
/**
|
|
@@ -197,6 +207,39 @@ function checkFieldNameSize(rawName, limits) {
|
|
|
197
207
|
throw new MultipartError('LIMIT_FIELD_KEY');
|
|
198
208
|
}
|
|
199
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Whether the field name holds a numeric index above `limit`, decided as
|
|
212
|
+
* multer does. Only a name `appendField` parses as a bracket path builds an
|
|
213
|
+
* array; one it keeps as a literal key (`a[6]suffix`, `[6]`) never does.
|
|
214
|
+
*/
|
|
215
|
+
function exceedsArrayIndexLimit(name, limit) {
|
|
216
|
+
if (!/^[^[]+(?:\[[^\]]+\])*(?:\[\])?$/.test(name)) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
for (const [, index] of name.matchAll(/\[(\d+)\]/g)) {
|
|
220
|
+
if (Number(index) > limit) {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* `fieldNestingDepth` and `fieldArrayIndexLimit` are multer's own limits
|
|
228
|
+
* (busboy knows neither), so they are checked here, against the decoded name.
|
|
229
|
+
* Unlike multer, they default to `DEFAULT_FIELD_NAME_LIMITS`.
|
|
230
|
+
*/
|
|
231
|
+
function checkFieldNameShape(name, limits) {
|
|
232
|
+
const nestingDepth = limits?.fieldNestingDepth ?? DEFAULT_FIELD_NAME_LIMITS.fieldNestingDepth;
|
|
233
|
+
if (name.split('[').length - 1 > nestingDepth) {
|
|
234
|
+
throw new MultipartError('LIMIT_FIELD_NESTING', name);
|
|
235
|
+
}
|
|
236
|
+
const arrayIndexLimit = limits?.fieldArrayIndexLimit ??
|
|
237
|
+
DEFAULT_FIELD_NAME_LIMITS.fieldArrayIndexLimit;
|
|
238
|
+
if (arrayIndexLimit !== Infinity &&
|
|
239
|
+
exceedsArrayIndexLimit(name, arrayIndexLimit)) {
|
|
240
|
+
throw new MultipartError('LIMIT_FIELD_ARRAY_INDEX', name);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
200
243
|
/**
|
|
201
244
|
* Adds a text field to `body`, with multer's checks, in multer's order.
|
|
202
245
|
*/
|
|
@@ -209,6 +252,7 @@ function appendTextField(body, part, limits) {
|
|
|
209
252
|
throw new MultipartError('LIMIT_FIELD_VALUE', part.fieldname);
|
|
210
253
|
}
|
|
211
254
|
checkFieldNameSize(rawName, limits);
|
|
255
|
+
checkFieldNameShape(part.fieldname, limits);
|
|
212
256
|
if (!appendField(body, part.fieldname, part.value)) {
|
|
213
257
|
throw new MultipartError('INVALID_FIELD_NAME', part.fieldname);
|
|
214
258
|
}
|
|
@@ -229,15 +273,16 @@ function readFilePart(part, limits) {
|
|
|
229
273
|
}
|
|
230
274
|
/**
|
|
231
275
|
* Parses the multipart body with multer's semantics and populates
|
|
232
|
-
* `req.body` and `req.file` / `req.files
|
|
233
|
-
* are passed through untouched, as multer
|
|
276
|
+
* `req.body` and `req.file` / `req.files`, and returns the stored files.
|
|
277
|
+
* Requests that are not multipart are passed through untouched, as multer
|
|
278
|
+
* does.
|
|
234
279
|
*/
|
|
235
280
|
export async function processMultipart(request, spec, options, storage) {
|
|
236
281
|
const req = request;
|
|
237
282
|
assertPluginRegistered(req);
|
|
238
283
|
resetFiles(req);
|
|
239
284
|
if (!req.isMultipart()) {
|
|
240
|
-
return;
|
|
285
|
+
return [];
|
|
241
286
|
}
|
|
242
287
|
const filesLeft = new Map();
|
|
243
288
|
if (spec.fields !== 'ANY') {
|
|
@@ -308,6 +353,7 @@ export async function processMultipart(request, spec, options, storage) {
|
|
|
308
353
|
break;
|
|
309
354
|
}
|
|
310
355
|
}
|
|
356
|
+
return stored;
|
|
311
357
|
}
|
|
312
358
|
/**
|
|
313
359
|
* Parses up to and including the first file in `fieldName`, then stops and
|
|
@@ -13,6 +13,11 @@ export function mergeMultipartOptions(options = {}, localOptions = {}) {
|
|
|
13
13
|
if (isPlainObject(options.limits) && isPlainObject(localOptions.limits)) {
|
|
14
14
|
merged.limits = { ...options.limits, ...localOptions.limits };
|
|
15
15
|
}
|
|
16
|
+
// `dest` and `storage` both say where files go, and `resolveStorage()`
|
|
17
|
+
// prefers `storage`, so a global storage engine would shadow a local `dest`.
|
|
18
|
+
if (localOptions.dest && !localOptions.storage) {
|
|
19
|
+
delete merged.storage;
|
|
20
|
+
}
|
|
16
21
|
return merged;
|
|
17
22
|
}
|
|
18
23
|
/**
|
|
@@ -3,10 +3,24 @@ import { createWriteStream, mkdirSync } from 'fs';
|
|
|
3
3
|
import { unlink } from 'fs/promises';
|
|
4
4
|
import { tmpdir } from 'os';
|
|
5
5
|
import { join } from 'path';
|
|
6
|
-
import { pipeline } from 'stream/promises';
|
|
6
|
+
import { finished, pipeline } from 'stream/promises';
|
|
7
7
|
function resolveName(resolver, req, file) {
|
|
8
8
|
return new Promise((resolve, reject) => resolver(req, file, (err, value) => (err ? reject(err) : resolve(value))));
|
|
9
9
|
}
|
|
10
|
+
const STREAM_DESTROYED_MESSAGE = 'File stream was destroyed';
|
|
11
|
+
/**
|
|
12
|
+
* Rejects once `stream` is destroyed before its end without an error, as
|
|
13
|
+
* `@fastify/multipart` does when the form is cut off. `pipeline()` never
|
|
14
|
+
* settles then if the last chunk had already been pushed: it waits for an
|
|
15
|
+
* 'end' that is never emitted, with the file still open.
|
|
16
|
+
*/
|
|
17
|
+
function rejectWhenCutOff(stream) {
|
|
18
|
+
return new Promise((_resolve, reject) => stream.once('close', () => {
|
|
19
|
+
if (!stream.readableEnded && !stream.errored) {
|
|
20
|
+
reject(new Error(STREAM_DESTROYED_MESSAGE));
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
10
24
|
/**
|
|
11
25
|
* Writes each file to disk, with multer's `diskStorage` defaults and file
|
|
12
26
|
* shape (`destination`, `filename`, `path`, `size`).
|
|
@@ -30,14 +44,25 @@ export function diskStorage(options = {}) {
|
|
|
30
44
|
const store = async (req, file) => {
|
|
31
45
|
const destination = await resolveName(resolveDestination, req, file);
|
|
32
46
|
const filename = await resolveName(resolveFilename, req, file);
|
|
47
|
+
// As in multer, no file is created for a stream destroyed in the meantime.
|
|
48
|
+
if (file.stream.destroyed) {
|
|
49
|
+
throw new Error(STREAM_DESTROYED_MESSAGE);
|
|
50
|
+
}
|
|
33
51
|
const path = join(destination, filename);
|
|
34
52
|
// As in multer, `path` is known to the file before it is written.
|
|
35
53
|
file.path = path;
|
|
36
54
|
const out = createWriteStream(path);
|
|
37
55
|
try {
|
|
38
|
-
await
|
|
56
|
+
await Promise.race([
|
|
57
|
+
pipeline(file.stream, out),
|
|
58
|
+
rejectWhenCutOff(file.stream),
|
|
59
|
+
]);
|
|
39
60
|
}
|
|
40
61
|
catch (err) {
|
|
62
|
+
// Removed once closed: a file still being opened would be created
|
|
63
|
+
// after the unlink.
|
|
64
|
+
out.destroy();
|
|
65
|
+
await finished(out).catch(() => undefined);
|
|
41
66
|
await unlink(path).catch(() => undefined);
|
|
42
67
|
throw err;
|
|
43
68
|
}
|
|
@@ -8,12 +8,20 @@ export function memoryStorage() {
|
|
|
8
8
|
return {
|
|
9
9
|
_handleFile(_req, file, callback) {
|
|
10
10
|
const chunks = [];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const onData = (chunk) => chunks.push(chunk);
|
|
12
|
+
const onError = (err) => {
|
|
13
|
+
stopListening();
|
|
14
|
+
callback(err);
|
|
15
|
+
};
|
|
16
|
+
const onEnd = () => {
|
|
17
|
+
stopListening();
|
|
14
18
|
const buffer = Buffer.concat(chunks);
|
|
15
19
|
callback(null, { buffer, size: buffer.length });
|
|
16
|
-
}
|
|
20
|
+
};
|
|
21
|
+
// `@fastify/multipart` keeps every part's stream until the request
|
|
22
|
+
// ends, and with it these listeners and the chunks they hold.
|
|
23
|
+
const stopListening = () => file.stream.off('data', onData).off('error', onError).off('end', onEnd);
|
|
24
|
+
file.stream.on('data', onData).once('error', onError).once('end', onEnd);
|
|
17
25
|
},
|
|
18
26
|
_removeFile(_req, file, callback) {
|
|
19
27
|
delete file.buffer;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestjs/platform-fastify",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.2.0",
|
|
4
4
|
"description": "Nest - modern, fast, powerful node.js web framework (@platform-fastify)",
|
|
5
5
|
"author": "Kamil Mysliwiec",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"main": "./index.js",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./index.js",
|
|
11
|
+
"./package.json": "./package.json",
|
|
11
12
|
"./multipart": "./multipart/index.js",
|
|
12
13
|
"./*.js": "./*.js",
|
|
13
14
|
"./*": "./*.js"
|
|
@@ -26,13 +27,13 @@
|
|
|
26
27
|
"access": "public"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"@fastify/cors": "11.3.
|
|
30
|
-
"@fastify/formbody": "9.0.
|
|
31
|
-
"@fastify/middie": "9.3.
|
|
30
|
+
"@fastify/cors": "11.3.1",
|
|
31
|
+
"@fastify/formbody": "9.0.1",
|
|
32
|
+
"@fastify/middie": "9.3.5",
|
|
32
33
|
"fast-querystring": "1.1.2",
|
|
33
|
-
"fastify": "5.12.
|
|
34
|
-
"find-my-way": "9.
|
|
35
|
-
"light-my-request": "6.
|
|
34
|
+
"fastify": "5.12.7",
|
|
35
|
+
"find-my-way": "9.10.0",
|
|
36
|
+
"light-my-request": "6.8.0",
|
|
36
37
|
"path-to-regexp": "8.4.2",
|
|
37
38
|
"tslib": "2.8.1"
|
|
38
39
|
},
|
|
@@ -54,5 +55,5 @@
|
|
|
54
55
|
"optional": true
|
|
55
56
|
}
|
|
56
57
|
},
|
|
57
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "af37111368de7fd2dfab0bdec2f530c7845510c2"
|
|
58
59
|
}
|