@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.
Files changed (57) hide show
  1. package/adapters/fastify-adapter.d.ts +48 -5
  2. package/adapters/fastify-adapter.js +137 -9
  3. package/index.d.ts +1 -0
  4. package/index.js +1 -0
  5. package/interfaces/external/fastify-multipart-options.interface.d.ts +53 -0
  6. package/interfaces/external/fastify-multipart-options.interface.js +1 -0
  7. package/interfaces/external/index.d.ts +1 -0
  8. package/interfaces/external/index.js +1 -0
  9. package/multipart/files.constants.d.ts +1 -0
  10. package/multipart/files.constants.js +1 -0
  11. package/multipart/index.d.ts +4 -0
  12. package/multipart/index.js +4 -0
  13. package/multipart/interceptors/any-files.interceptor.d.ts +12 -0
  14. package/multipart/interceptors/any-files.interceptor.js +13 -0
  15. package/multipart/interceptors/file-fields.interceptor.d.ts +13 -0
  16. package/multipart/interceptors/file-fields.interceptor.js +14 -0
  17. package/multipart/interceptors/file-stream.interceptor.d.ts +31 -0
  18. package/multipart/interceptors/file-stream.interceptor.js +32 -0
  19. package/multipart/interceptors/file.interceptor.d.ts +17 -0
  20. package/multipart/interceptors/file.interceptor.js +18 -0
  21. package/multipart/interceptors/files.interceptor.d.ts +14 -0
  22. package/multipart/interceptors/files.interceptor.js +15 -0
  23. package/multipart/interceptors/index.d.ts +6 -0
  24. package/multipart/interceptors/index.js +6 -0
  25. package/multipart/interceptors/no-files.interceptor.d.ts +12 -0
  26. package/multipart/interceptors/no-files.interceptor.js +13 -0
  27. package/multipart/interfaces/files-upload-module.interface.d.ts +18 -0
  28. package/multipart/interfaces/files-upload-module.interface.js +1 -0
  29. package/multipart/interfaces/index.d.ts +3 -0
  30. package/multipart/interfaces/index.js +3 -0
  31. package/multipart/interfaces/multipart-file.interface.d.ts +70 -0
  32. package/multipart/interfaces/multipart-file.interface.js +1 -0
  33. package/multipart/interfaces/multipart-options.interface.d.ts +77 -0
  34. package/multipart/interfaces/multipart-options.interface.js +1 -0
  35. package/multipart/multipart/append-field.util.d.ts +6 -0
  36. package/multipart/multipart/append-field.util.js +112 -0
  37. package/multipart/multipart/multipart-interceptor.factory.d.ts +13 -0
  38. package/multipart/multipart/multipart-interceptor.factory.js +130 -0
  39. package/multipart/multipart/multipart.constants.d.ts +55 -0
  40. package/multipart/multipart/multipart.constants.js +74 -0
  41. package/multipart/multipart/multipart.error.d.ts +10 -0
  42. package/multipart/multipart/multipart.error.js +15 -0
  43. package/multipart/multipart/multipart.parser.d.ts +39 -0
  44. package/multipart/multipart/multipart.parser.js +369 -0
  45. package/multipart/multipart/multipart.utils.d.ts +28 -0
  46. package/multipart/multipart/multipart.utils.js +54 -0
  47. package/multipart/multipart.constants.d.ts +1 -0
  48. package/multipart/multipart.constants.js +1 -0
  49. package/multipart/multipart.module.d.ts +18 -0
  50. package/multipart/multipart.module.js +75 -0
  51. package/multipart/storage/disk.storage.d.ts +24 -0
  52. package/multipart/storage/disk.storage.js +58 -0
  53. package/multipart/storage/index.d.ts +2 -0
  54. package/multipart/storage/index.js +2 -0
  55. package/multipart/storage/memory.storage.d.ts +8 -0
  56. package/multipart/storage/memory.storage.js +23 -0
  57. package/package.json +7 -2
@@ -0,0 +1,369 @@
1
+ import { BadRequestException, PayloadTooLargeException } from '@nestjs/common';
2
+ import { diskStorage } from '../storage/disk.storage.js';
3
+ import { memoryStorage } from '../storage/memory.storage.js';
4
+ import { appendField } from './append-field.util.js';
5
+ import { busboyExceptions, MISSING_PLUGIN_MESSAGE, multerExceptions, } from './multipart.constants.js';
6
+ import { MultipartError } from './multipart.error.js';
7
+ import { transformException } from './multipart.utils.js';
8
+ /** Same resolution order as multer: `storage`, then `dest`, then memory. */
9
+ export function resolveStorage(options) {
10
+ if (options.storage) {
11
+ return options.storage;
12
+ }
13
+ if (options.dest) {
14
+ return diskStorage({ destination: options.dest });
15
+ }
16
+ return memoryStorage();
17
+ }
18
+ function assertPluginRegistered(req) {
19
+ if (typeof req.isMultipart !== 'function') {
20
+ throw new Error(MISSING_PLUGIN_MESSAGE);
21
+ }
22
+ }
23
+ /**
24
+ * Per-route options handed to `req.parts()`; `@fastify/multipart` deep-merges
25
+ * them over the options the plugin was registered with. `@fastify/busboy`
26
+ * treats `fileSize` and `parts` as "max allowed" (it fires once a limit is
27
+ * exceeded), so unlike multer there is no `+1` adjustment. Truncation is
28
+ * detected here (`file.truncated`), so the plugin's own throw is disabled.
29
+ */
30
+ function toBusboyConfig(limits, options) {
31
+ const config = { throwFileSizeLimit: false };
32
+ if (limits) {
33
+ config.limits = { ...limits };
34
+ }
35
+ if (options.preservePath !== undefined) {
36
+ config.preservePath = options.preservePath;
37
+ }
38
+ if (options.defParamCharset) {
39
+ config.defParamCharset = options.defParamCharset;
40
+ }
41
+ return config;
42
+ }
43
+ /**
44
+ * `file()` and `files()` are methods `@fastify/multipart` decorates the
45
+ * request prototype with. An own property shadows them for this request only,
46
+ * which is where `@UploadedFile()` / `@UploadedFiles()` read from on Fastify.
47
+ */
48
+ function setOnRequest(req, key, value) {
49
+ req[key] = value;
50
+ }
51
+ /**
52
+ * Shadows both methods up front, so that the one this interceptor does not
53
+ * populate reads `undefined`, as on Express, rather than the plugin's method.
54
+ */
55
+ function resetFiles(req) {
56
+ setOnRequest(req, 'file', undefined);
57
+ setOnRequest(req, 'files', undefined);
58
+ }
59
+ function toIncomingFile(part) {
60
+ const file = {
61
+ fieldname: part.fieldname,
62
+ originalname: decodeFormDataName(part.filename),
63
+ encoding: part.encoding,
64
+ mimetype: part.mimetype,
65
+ };
66
+ Object.defineProperty(file, 'stream', {
67
+ value: part.file,
68
+ enumerable: false,
69
+ configurable: true,
70
+ });
71
+ return file;
72
+ }
73
+ function applyFileFilter(options, req, file) {
74
+ if (!options.fileFilter) {
75
+ return Promise.resolve(true);
76
+ }
77
+ return new Promise((resolve, reject) => options.fileFilter(req, file, (err, accept) => err ? reject(err) : resolve(accept)));
78
+ }
79
+ const ABORTED = Symbol('aborted');
80
+ /**
81
+ * Runs the storage engine. Resolves `ABORTED` when `@fastify/multipart`
82
+ * destroys the file stream mid-way (it does so, without an error, when the
83
+ * client disconnects or a limit is hit), because a storage engine waiting
84
+ * for `end` would otherwise never call back. The actual reason is then read
85
+ * from the parts iterator. A stream destroyed with an error was destroyed by
86
+ * the engine (e.g. by a `pipeline()` to a file that cannot be written), which
87
+ * reports that error itself.
88
+ *
89
+ * Whatever the engine stores after an abort is removed once it calls back,
90
+ * as multer does: what it stored when it succeeds, and the file at `path`
91
+ * (which engines set before writing, by multer's convention) when it fails.
92
+ */
93
+ function storeFile(storage, req, file) {
94
+ return new Promise((resolve, reject) => {
95
+ const stream = file.stream;
96
+ if (stream.destroyed) {
97
+ return resolve(ABORTED);
98
+ }
99
+ let aborted = false;
100
+ const onClose = () => {
101
+ if (!stream.readableEnded && !stream.errored) {
102
+ aborted = true;
103
+ resolve(ABORTED);
104
+ }
105
+ };
106
+ stream.once('close', onClose);
107
+ storage._handleFile(req, file, (err, info) => {
108
+ stream.off('close', onClose);
109
+ if (!aborted) {
110
+ return err ? reject(err) : resolve(info ?? {});
111
+ }
112
+ const stored = Object.assign(file, err ? {} : info);
113
+ if (!err || stored.path) {
114
+ removeFiles(storage, req, [stored]).catch(() => undefined);
115
+ }
116
+ });
117
+ });
118
+ }
119
+ function removeFiles(storage, req, files) {
120
+ return Promise.all(files.map(file => new Promise(resolve => storage._removeFile(req, file, () => resolve()))));
121
+ }
122
+ /**
123
+ * Stops parsing and discards the rest of the body, so that an error response
124
+ * does not race a client that is still uploading (multer does the same
125
+ * before it reports an error).
126
+ */
127
+ export function drainRequest(raw) {
128
+ raw.unpipe();
129
+ if (raw.readableEnded || raw.destroyed) {
130
+ return Promise.resolve();
131
+ }
132
+ return new Promise(resolve => {
133
+ const done = () => resolve();
134
+ raw.once('end', done);
135
+ raw.once('close', done);
136
+ raw.once('error', done);
137
+ raw.resume();
138
+ });
139
+ }
140
+ /**
141
+ * Rejects limits that are not a non-negative integer or `Infinity`, as
142
+ * multer does: busboy compares most limits with strict equality, so such a
143
+ * value would silently disable the limit.
144
+ */
145
+ export function validateLimits(limits) {
146
+ for (const [key, value] of Object.entries(limits ?? {})) {
147
+ if (value == null ||
148
+ (Number.isInteger(value) && value >= 0) ||
149
+ value === Infinity) {
150
+ continue;
151
+ }
152
+ throw new TypeError(`Expected limits.${key} to be a non-negative integer or Infinity`);
153
+ }
154
+ }
155
+ function resolveLimits(req, options) {
156
+ if (typeof options.limits !== 'function') {
157
+ return options.limits;
158
+ }
159
+ const limits = options.limits(req);
160
+ validateLimits(limits);
161
+ return limits;
162
+ }
163
+ /**
164
+ * Reverses the escapes browsers apply to form field names (`%0A`, `%0D`,
165
+ * `%22`), as multer does.
166
+ */
167
+ function decodeFormDataName(name) {
168
+ return name.replace(/%0A|%0D|%22/gi, match => {
169
+ switch (match.toUpperCase()) {
170
+ case '%0A':
171
+ return '\n';
172
+ case '%0D':
173
+ return '\r';
174
+ default:
175
+ return '"';
176
+ }
177
+ });
178
+ }
179
+ /**
180
+ * Decodes the part's field name, and returns the name as it arrived.
181
+ */
182
+ function decodeFieldName(part) {
183
+ if (part.fieldname == null) {
184
+ throw new MultipartError('MISSING_FIELD_NAME');
185
+ }
186
+ const rawName = part.fieldname;
187
+ part.fieldname = decodeFormDataName(rawName);
188
+ return rawName;
189
+ }
190
+ /**
191
+ * busboy does not enforce `fieldNameSize` for multipart bodies, so (like
192
+ * multer) it is checked here, against the name as it arrived.
193
+ */
194
+ function checkFieldNameSize(rawName, limits) {
195
+ if (typeof limits?.fieldNameSize === 'number' &&
196
+ rawName.length > limits.fieldNameSize) {
197
+ throw new MultipartError('LIMIT_FIELD_KEY');
198
+ }
199
+ }
200
+ /**
201
+ * Adds a text field to `body`, with multer's checks, in multer's order.
202
+ */
203
+ function appendTextField(body, part, limits) {
204
+ const rawName = decodeFieldName(part);
205
+ if (part.fieldnameTruncated) {
206
+ throw new MultipartError('LIMIT_FIELD_KEY');
207
+ }
208
+ if (part.valueTruncated) {
209
+ throw new MultipartError('LIMIT_FIELD_VALUE', part.fieldname);
210
+ }
211
+ checkFieldNameSize(rawName, limits);
212
+ if (!appendField(body, part.fieldname, part.value)) {
213
+ throw new MultipartError('INVALID_FIELD_NAME', part.fieldname);
214
+ }
215
+ }
216
+ /**
217
+ * Returns the stream of a file part, with multer's checks, in multer's
218
+ * order, or `undefined` for a part without a file name (an empty file
219
+ * input), which multer ignores.
220
+ */
221
+ function readFilePart(part, limits) {
222
+ const rawName = decodeFieldName(part);
223
+ if (!part.filename) {
224
+ part.file.resume();
225
+ return undefined;
226
+ }
227
+ checkFieldNameSize(rawName, limits);
228
+ return part.file;
229
+ }
230
+ /**
231
+ * Parses the multipart body with multer's semantics and populates
232
+ * `req.body` and `req.file` / `req.files`. Requests that are not multipart
233
+ * are passed through untouched, as multer does.
234
+ */
235
+ export async function processMultipart(request, spec, options, storage) {
236
+ const req = request;
237
+ assertPluginRegistered(req);
238
+ resetFiles(req);
239
+ if (!req.isMultipart()) {
240
+ return;
241
+ }
242
+ const filesLeft = new Map();
243
+ if (spec.fields !== 'ANY') {
244
+ for (const field of spec.fields) {
245
+ filesLeft.set(field.name, field.maxCount ?? Infinity);
246
+ }
247
+ }
248
+ const body = Object.create(null);
249
+ setOnRequest(req, 'body', body);
250
+ const stored = [];
251
+ try {
252
+ const limits = resolveLimits(req, options);
253
+ const iterator = req
254
+ .parts(toBusboyConfig(limits, options))[Symbol.asyncIterator]();
255
+ for (let result = await iterator.next(); !result.done; result = await iterator.next()) {
256
+ const part = result.value;
257
+ if (part.type === 'field') {
258
+ appendTextField(body, part, limits);
259
+ continue;
260
+ }
261
+ const stream = readFilePart(part, limits);
262
+ if (!stream) {
263
+ continue;
264
+ }
265
+ const left = spec.fields === 'ANY' ? Infinity : (filesLeft.get(part.fieldname) ?? 0);
266
+ if (left <= 0) {
267
+ throw new MultipartError('LIMIT_UNEXPECTED_FILE', part.fieldname);
268
+ }
269
+ const incoming = toIncomingFile(part);
270
+ if (!(await applyFileFilter(options, req, incoming))) {
271
+ stream.resume();
272
+ continue;
273
+ }
274
+ filesLeft.set(part.fieldname, left - 1);
275
+ const info = await storeFile(storage, req, incoming);
276
+ if (info === ABORTED) {
277
+ // Rejects with what aborted the stream: a limit, or the client
278
+ // disconnecting.
279
+ await iterator.next();
280
+ throw new BadRequestException(busboyExceptions.MULTIPART_UNEXPECTED_END_OF_FILE);
281
+ }
282
+ const file = { ...incoming, ...info };
283
+ stored.push(file);
284
+ // busboy truncates the file instead of failing; multer rejects it.
285
+ if (stream.truncated) {
286
+ throw new MultipartError('LIMIT_FILE_SIZE', part.fieldname);
287
+ }
288
+ }
289
+ }
290
+ catch (err) {
291
+ await removeFiles(storage, req, stored);
292
+ await drainRequest(req.raw);
293
+ throw transformException(err);
294
+ }
295
+ switch (spec.strategy) {
296
+ case 'VALUE':
297
+ setOnRequest(req, 'file', stored[0]);
298
+ break;
299
+ case 'ARRAY':
300
+ setOnRequest(req, 'files', stored);
301
+ break;
302
+ case 'OBJECT': {
303
+ const grouped = Object.create(null);
304
+ for (const file of stored) {
305
+ (grouped[file.fieldname] ??= []).push(file);
306
+ }
307
+ setOnRequest(req, 'files', grouped);
308
+ break;
309
+ }
310
+ }
311
+ }
312
+ /**
313
+ * Parses up to and including the first file in `fieldName`, then stops and
314
+ * hands that file's stream to the route handler (`req.file.stream`). Text
315
+ * fields sent before the file are in `req.body`; anything after it is not
316
+ * parsed.
317
+ */
318
+ export async function processStream(request, fieldName, options) {
319
+ const req = request;
320
+ assertPluginRegistered(req);
321
+ resetFiles(req);
322
+ if (!req.isMultipart()) {
323
+ return;
324
+ }
325
+ const body = Object.create(null);
326
+ setOnRequest(req, 'body', body);
327
+ try {
328
+ const limits = resolveLimits(req, options);
329
+ const iterator = req
330
+ .parts(toBusboyConfig(limits, options))[Symbol.asyncIterator]();
331
+ for (let result = await iterator.next(); !result.done; result = await iterator.next()) {
332
+ const part = result.value;
333
+ if (part.type === 'field') {
334
+ appendTextField(body, part, limits);
335
+ continue;
336
+ }
337
+ const stream = readFilePart(part, limits);
338
+ if (!stream) {
339
+ continue;
340
+ }
341
+ if (part.fieldname !== fieldName) {
342
+ throw new MultipartError('LIMIT_UNEXPECTED_FILE', part.fieldname);
343
+ }
344
+ const incoming = toIncomingFile(part);
345
+ if (!(await applyFileFilter(options, req, incoming))) {
346
+ stream.resume();
347
+ continue;
348
+ }
349
+ // busboy would silently truncate; fail the consumer's read instead.
350
+ // The limit may have been hit already: busboy parses the part as far
351
+ // as the chunk at hand goes before the part reaches this loop.
352
+ const rejectFile = () => stream.destroy(new PayloadTooLargeException(multerExceptions.LIMIT_FILE_SIZE));
353
+ if (stream.truncated) {
354
+ rejectFile();
355
+ }
356
+ else {
357
+ stream.once('limit', rejectFile);
358
+ }
359
+ const file = { ...incoming, stream };
360
+ setOnRequest(req, 'file', file);
361
+ // The iterator is left suspended: the handler now owns the stream.
362
+ return;
363
+ }
364
+ }
365
+ catch (err) {
366
+ await drainRequest(req.raw);
367
+ throw transformException(err);
368
+ }
369
+ }
@@ -0,0 +1,28 @@
1
+ import { HttpException } from '@nestjs/common';
2
+ import type { MultipartModuleOptions, MultipartOptions } from '../interfaces/index.js';
3
+ /**
4
+ * Merges module-level (global) options with interceptor-level (local)
5
+ * options, with the same rule as platform-express's `mergeMulterOptions`:
6
+ * `limits` is merged key-by-key when both sides define it as an object, and
7
+ * the local value wins outright otherwise.
8
+ */
9
+ export declare function mergeMultipartOptions(options?: MultipartModuleOptions, localOptions?: MultipartOptions): MultipartModuleOptions;
10
+ /**
11
+ * Maps parser errors to the same HTTP exceptions (status and message)
12
+ * platform-express's `transformException` produces for multer, so clients
13
+ * cannot tell the adapters apart.
14
+ *
15
+ * Errors raised by `@fastify/multipart` are identified by their `code`; only
16
+ * `@fastify/busboy`'s errors, which carry no code, are matched by message.
17
+ */
18
+ export declare function transformException(error: (Error & {
19
+ code?: string;
20
+ part?: {
21
+ fieldname?: string;
22
+ };
23
+ }) | undefined): HttpException | (Error & {
24
+ code?: string;
25
+ part?: {
26
+ fieldname?: string;
27
+ };
28
+ }) | undefined;
@@ -0,0 +1,54 @@
1
+ import { BadRequestException, HttpException, PayloadTooLargeException, } from '@nestjs/common';
2
+ import { isPlainObject } from '@nestjs/common/internal';
3
+ import { busboyExceptions, fastifyBusboyMessages, fastifyMultipartBadRequestCodes, fastifyMultipartErrorCodes, } from './multipart.constants.js';
4
+ import { MultipartError } from './multipart.error.js';
5
+ /**
6
+ * Merges module-level (global) options with interceptor-level (local)
7
+ * options, with the same rule as platform-express's `mergeMulterOptions`:
8
+ * `limits` is merged key-by-key when both sides define it as an object, and
9
+ * the local value wins outright otherwise.
10
+ */
11
+ export function mergeMultipartOptions(options = {}, localOptions = {}) {
12
+ const merged = { ...options, ...localOptions };
13
+ if (isPlainObject(options.limits) && isPlainObject(localOptions.limits)) {
14
+ merged.limits = { ...options.limits, ...localOptions.limits };
15
+ }
16
+ return merged;
17
+ }
18
+ /**
19
+ * Maps parser errors to the same HTTP exceptions (status and message)
20
+ * platform-express's `transformException` produces for multer, so clients
21
+ * cannot tell the adapters apart.
22
+ *
23
+ * Errors raised by `@fastify/multipart` are identified by their `code`; only
24
+ * `@fastify/busboy`'s errors, which carry no code, are matched by message.
25
+ */
26
+ export function transformException(error) {
27
+ if (!error || error instanceof HttpException) {
28
+ return error;
29
+ }
30
+ if (isFastifyMultipartLimitCode(error.code)) {
31
+ error = new MultipartError(fastifyMultipartErrorCodes[error.code], error.part?.fieldname);
32
+ }
33
+ if (error instanceof MultipartError) {
34
+ if (error.code === 'LIMIT_FILE_SIZE') {
35
+ return new PayloadTooLargeException(error.message);
36
+ }
37
+ return new BadRequestException(error.field ? `${error.message} - ${error.field}` : error.message);
38
+ }
39
+ if (error.code && fastifyMultipartBadRequestCodes.includes(error.code)) {
40
+ return new BadRequestException(error.message);
41
+ }
42
+ const message = error.message ?? '';
43
+ if (message === fastifyBusboyMessages.BOUNDARY_NOT_FOUND) {
44
+ return new BadRequestException(busboyExceptions.MULTIPART_BOUNDARY_NOT_FOUND);
45
+ }
46
+ if (message === fastifyBusboyMessages.UNEXPECTED_END_OF_FORM ||
47
+ message.includes(fastifyBusboyMessages.PART_TERMINATED_EARLY)) {
48
+ return new BadRequestException(busboyExceptions.MULTIPART_UNEXPECTED_END_OF_FORM);
49
+ }
50
+ return error;
51
+ }
52
+ function isFastifyMultipartLimitCode(code) {
53
+ return (typeof code === 'string' && Object.hasOwn(fastifyMultipartErrorCodes, code));
54
+ }
@@ -0,0 +1 @@
1
+ export declare const MULTIPART_MODULE_ID = "MULTIPART_MODULE_ID";
@@ -0,0 +1 @@
1
+ export const MULTIPART_MODULE_ID = 'MULTIPART_MODULE_ID';
@@ -0,0 +1,18 @@
1
+ import { type DynamicModule } from '@nestjs/common';
2
+ import type { MultipartModuleAsyncOptions, MultipartModuleOptions } from './interfaces/files-upload-module.interface.js';
3
+ /**
4
+ * Provides default options for every upload interceptor in the importing
5
+ * module. The Fastify counterpart of platform-express's `MulterModule`.
6
+ *
7
+ * It does not configure the `@fastify/multipart` plugin itself (which the
8
+ * FastifyAdapter registers); pass plugin-wide options through the
9
+ * adapter's `multipart` option.
10
+ *
11
+ * @publicApi
12
+ */
13
+ export declare class MultipartModule {
14
+ static register(options?: MultipartModuleOptions): DynamicModule;
15
+ static registerAsync(options: MultipartModuleAsyncOptions): DynamicModule;
16
+ private static createAsyncProviders;
17
+ private static createAsyncOptionsProvider;
18
+ }
@@ -0,0 +1,75 @@
1
+ var MultipartModule_1;
2
+ import { __decorate } from "tslib";
3
+ import { Module } from '@nestjs/common';
4
+ import { randomStringGenerator } from '@nestjs/common/internal';
5
+ import { MULTIPART_MODULE_OPTIONS } from './files.constants.js';
6
+ import { MULTIPART_MODULE_ID } from './multipart.constants.js';
7
+ /**
8
+ * Provides default options for every upload interceptor in the importing
9
+ * module. The Fastify counterpart of platform-express's `MulterModule`.
10
+ *
11
+ * It does not configure the `@fastify/multipart` plugin itself (which the
12
+ * FastifyAdapter registers); pass plugin-wide options through the
13
+ * adapter's `multipart` option.
14
+ *
15
+ * @publicApi
16
+ */
17
+ let MultipartModule = MultipartModule_1 = class MultipartModule {
18
+ static register(options = {}) {
19
+ return {
20
+ module: MultipartModule_1,
21
+ providers: [
22
+ { provide: MULTIPART_MODULE_OPTIONS, useFactory: () => options },
23
+ {
24
+ provide: MULTIPART_MODULE_ID,
25
+ useValue: randomStringGenerator(),
26
+ },
27
+ ],
28
+ exports: [MULTIPART_MODULE_OPTIONS],
29
+ };
30
+ }
31
+ static registerAsync(options) {
32
+ return {
33
+ module: MultipartModule_1,
34
+ imports: options.imports,
35
+ providers: [
36
+ ...this.createAsyncProviders(options),
37
+ {
38
+ provide: MULTIPART_MODULE_ID,
39
+ useValue: randomStringGenerator(),
40
+ },
41
+ ],
42
+ exports: [MULTIPART_MODULE_OPTIONS],
43
+ };
44
+ }
45
+ static createAsyncProviders(options) {
46
+ if (options.useExisting || options.useFactory) {
47
+ return [this.createAsyncOptionsProvider(options)];
48
+ }
49
+ return [
50
+ this.createAsyncOptionsProvider(options),
51
+ {
52
+ provide: options.useClass,
53
+ useClass: options.useClass,
54
+ },
55
+ ];
56
+ }
57
+ static createAsyncOptionsProvider(options) {
58
+ if (options.useFactory) {
59
+ return {
60
+ provide: MULTIPART_MODULE_OPTIONS,
61
+ useFactory: options.useFactory,
62
+ inject: options.inject || [],
63
+ };
64
+ }
65
+ return {
66
+ provide: MULTIPART_MODULE_OPTIONS,
67
+ useFactory: async (optionsFactory) => optionsFactory.createMultipartOptions(),
68
+ inject: [options.useExisting || options.useClass],
69
+ };
70
+ }
71
+ };
72
+ MultipartModule = MultipartModule_1 = __decorate([
73
+ Module({})
74
+ ], MultipartModule);
75
+ export { MultipartModule };
@@ -0,0 +1,24 @@
1
+ import type { IncomingMultipartFile, MultipartStorageEngine } from '../interfaces/index.js';
2
+ type NameCallback = (error: Error | null, value: string) => void;
3
+ type NameResolver = (req: any, file: IncomingMultipartFile, callback: NameCallback) => void;
4
+ /**
5
+ * @publicApi
6
+ */
7
+ export interface DiskStorageOptions {
8
+ /**
9
+ * The folder files are written to (Default: `os.tmpdir()`). A string is
10
+ * created when the storage engine is; a function must return a folder that
11
+ * already exists.
12
+ */
13
+ destination?: string | NameResolver;
14
+ /** The file name within `destination` (Default: 32 random hex characters, no extension). */
15
+ filename?: NameResolver;
16
+ }
17
+ /**
18
+ * Writes each file to disk, with multer's `diskStorage` defaults and file
19
+ * shape (`destination`, `filename`, `path`, `size`).
20
+ *
21
+ * @publicApi
22
+ */
23
+ export declare function diskStorage(options?: DiskStorageOptions): MultipartStorageEngine;
24
+ export {};
@@ -0,0 +1,58 @@
1
+ import { randomBytes } from 'crypto';
2
+ import { createWriteStream, mkdirSync } from 'fs';
3
+ import { unlink } from 'fs/promises';
4
+ import { tmpdir } from 'os';
5
+ import { join } from 'path';
6
+ import { pipeline } from 'stream/promises';
7
+ function resolveName(resolver, req, file) {
8
+ return new Promise((resolve, reject) => resolver(req, file, (err, value) => (err ? reject(err) : resolve(value))));
9
+ }
10
+ /**
11
+ * Writes each file to disk, with multer's `diskStorage` defaults and file
12
+ * shape (`destination`, `filename`, `path`, `size`).
13
+ *
14
+ * @publicApi
15
+ */
16
+ export function diskStorage(options = {}) {
17
+ let resolveDestination;
18
+ if (typeof options.destination === 'string') {
19
+ const destination = options.destination;
20
+ mkdirSync(destination, { recursive: true });
21
+ resolveDestination = (_req, _file, callback) => callback(null, destination);
22
+ }
23
+ else {
24
+ resolveDestination =
25
+ options.destination ??
26
+ ((_req, _file, callback) => callback(null, tmpdir()));
27
+ }
28
+ const resolveFilename = options.filename ??
29
+ ((_req, _file, callback) => callback(null, randomBytes(16).toString('hex')));
30
+ const store = async (req, file) => {
31
+ const destination = await resolveName(resolveDestination, req, file);
32
+ const filename = await resolveName(resolveFilename, req, file);
33
+ const path = join(destination, filename);
34
+ // As in multer, `path` is known to the file before it is written.
35
+ file.path = path;
36
+ const out = createWriteStream(path);
37
+ try {
38
+ await pipeline(file.stream, out);
39
+ }
40
+ catch (err) {
41
+ await unlink(path).catch(() => undefined);
42
+ throw err;
43
+ }
44
+ return { destination, filename, path, size: out.bytesWritten };
45
+ };
46
+ return {
47
+ _handleFile(req, file, callback) {
48
+ store(req, file).then(info => callback(null, info), err => callback(err));
49
+ },
50
+ _removeFile(_req, file, callback) {
51
+ const path = file.path;
52
+ delete file.destination;
53
+ delete file.filename;
54
+ delete file.path;
55
+ unlink(path).then(() => callback(null), err => callback(err));
56
+ },
57
+ };
58
+ }
@@ -0,0 +1,2 @@
1
+ export * from './disk.storage.js';
2
+ export * from './memory.storage.js';
@@ -0,0 +1,2 @@
1
+ export * from './disk.storage.js';
2
+ export * from './memory.storage.js';
@@ -0,0 +1,8 @@
1
+ import type { MultipartStorageEngine } from '../interfaces/index.js';
2
+ /**
3
+ * Buffers each file into `file.buffer`. The default storage engine, as in
4
+ * multer.
5
+ *
6
+ * @publicApi
7
+ */
8
+ export declare function memoryStorage(): MultipartStorageEngine;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Buffers each file into `file.buffer`. The default storage engine, as in
3
+ * multer.
4
+ *
5
+ * @publicApi
6
+ */
7
+ export function memoryStorage() {
8
+ return {
9
+ _handleFile(_req, file, callback) {
10
+ const chunks = [];
11
+ file.stream.on('data', (chunk) => chunks.push(chunk));
12
+ file.stream.once('error', callback);
13
+ file.stream.once('end', () => {
14
+ const buffer = Buffer.concat(chunks);
15
+ callback(null, { buffer, size: buffer.length });
16
+ });
17
+ },
18
+ _removeFile(_req, file, callback) {
19
+ delete file.buffer;
20
+ callback(null);
21
+ },
22
+ };
23
+ }