Skip to content

Commit fae3845

Browse files
committed
feat(platform): add fastify-multipart support for fastify platform
add FileInterceptor, FilesInterceptor, AnyFilesInterceptor and FileFieldsInterceptor for platform-fastify closes nestjs#6894
1 parent c076add commit fae3845

27 files changed

+1699
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const MULTIPART_MODULE_OPTIONS = 'MULTIPART_MODULE_OPTIONS';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './interceptors';
2+
export * from './interfaces';
3+
export * from './multipart.module';
4+
export * from './utils';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {
2+
CallHandler,
3+
ExecutionContext,
4+
Inject,
5+
mixin,
6+
NestInterceptor,
7+
Optional,
8+
Type,
9+
} from '@nestjs/common';
10+
import { Observable } from 'rxjs';
11+
import { MULTIPART_MODULE_OPTIONS } from '../files.constants';
12+
import { MultipartOptions } from '../interfaces/multipart-options.interface';
13+
import { MultipartWrapper, transformException } from '../multipart';
14+
15+
export const AnyFilesInterceptor = (
16+
localOptions?: MultipartOptions,
17+
): Type<NestInterceptor> => {
18+
class MixinInterceptor implements NestInterceptor {
19+
protected options: MultipartOptions;
20+
protected multipart: MultipartWrapper;
21+
22+
public constructor(
23+
@Optional()
24+
@Inject(MULTIPART_MODULE_OPTIONS)
25+
options: MultipartOptions = {},
26+
) {
27+
this.multipart = new MultipartWrapper({
28+
...options,
29+
...localOptions,
30+
});
31+
}
32+
33+
public async intercept(
34+
context: ExecutionContext,
35+
next: CallHandler,
36+
): Promise<Observable<any>> {
37+
const req = context.switchToHttp().getRequest();
38+
const fieldname = 'files';
39+
try {
40+
req[fieldname] = await this.multipart.any()(req);
41+
} catch (err) {
42+
throw transformException(err);
43+
}
44+
return next.handle();
45+
}
46+
}
47+
48+
const Interceptor = mixin(MixinInterceptor);
49+
return Interceptor as Type<NestInterceptor>;
50+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {
2+
CallHandler,
3+
ExecutionContext,
4+
Inject,
5+
mixin,
6+
NestInterceptor,
7+
Optional,
8+
Type,
9+
} from '@nestjs/common';
10+
import { Observable } from 'rxjs';
11+
import { MULTIPART_MODULE_OPTIONS } from '../files.constants';
12+
import {
13+
MultipartOptions,
14+
UploadField,
15+
} from '../interfaces/multipart-options.interface';
16+
import { MultipartWrapper, transformException } from '../multipart';
17+
18+
export const FileFieldsInterceptor = (
19+
uploadFields: UploadField[],
20+
localOptions?: MultipartOptions,
21+
): Type<NestInterceptor> => {
22+
class MixinInterceptor implements NestInterceptor {
23+
protected multipart: MultipartWrapper;
24+
25+
public constructor(
26+
@Optional()
27+
@Inject(MULTIPART_MODULE_OPTIONS)
28+
options: MultipartOptions = {},
29+
) {
30+
this.multipart = new MultipartWrapper({
31+
...options,
32+
...localOptions,
33+
});
34+
}
35+
36+
public async intercept(
37+
context: ExecutionContext,
38+
next: CallHandler,
39+
): Promise<Observable<any>> {
40+
const req = context.switchToHttp().getRequest();
41+
const fieldname = 'files';
42+
try {
43+
req[fieldname] = await this.multipart.fileFields(uploadFields)(req);
44+
} catch (err) {
45+
throw transformException(err);
46+
}
47+
return next.handle();
48+
}
49+
}
50+
51+
const Interceptor = mixin(MixinInterceptor);
52+
return Interceptor as Type<NestInterceptor>;
53+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
CallHandler,
3+
ExecutionContext,
4+
Inject,
5+
mixin,
6+
NestInterceptor,
7+
Optional,
8+
Type,
9+
} from '@nestjs/common';
10+
import { Observable } from 'rxjs';
11+
import { MULTIPART_MODULE_OPTIONS } from '../files.constants';
12+
import { MultipartOptions } from '../interfaces/multipart-options.interface';
13+
import { MultipartWrapper, transformException } from '../multipart';
14+
15+
export const FileInterceptor = (
16+
fieldname: string,
17+
localOptions?: MultipartOptions,
18+
): Type<NestInterceptor> => {
19+
class MixinInterceptor implements NestInterceptor {
20+
protected multipart: MultipartWrapper;
21+
22+
public constructor(
23+
@Optional()
24+
@Inject(MULTIPART_MODULE_OPTIONS)
25+
options: MultipartOptions = {},
26+
) {
27+
this.multipart = new MultipartWrapper({
28+
...options,
29+
...localOptions,
30+
});
31+
}
32+
33+
public async intercept(
34+
context: ExecutionContext,
35+
next: CallHandler,
36+
): Promise<Observable<any>> {
37+
const req = context.switchToHttp().getRequest();
38+
try {
39+
req[fieldname] = await this.multipart.file(fieldname)(req);
40+
} catch (err) {
41+
throw transformException(err);
42+
}
43+
return next.handle();
44+
}
45+
}
46+
47+
const Interceptor = mixin(MixinInterceptor);
48+
return Interceptor as Type<NestInterceptor>;
49+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {
2+
CallHandler,
3+
ExecutionContext,
4+
Inject,
5+
mixin,
6+
NestInterceptor,
7+
Optional,
8+
Type,
9+
} from '@nestjs/common';
10+
import { Observable } from 'rxjs';
11+
import { MULTIPART_MODULE_OPTIONS } from '../files.constants';
12+
import { MultipartOptions } from '../interfaces/multipart-options.interface';
13+
import { MultipartWrapper, transformException } from '../multipart';
14+
15+
export const FilesInterceptor = (
16+
fieldname: string,
17+
maxCount?: number,
18+
localOptions?: MultipartOptions,
19+
): Type<NestInterceptor> => {
20+
class MixinInterceptor implements NestInterceptor {
21+
protected multipart: MultipartWrapper;
22+
23+
public constructor(
24+
@Optional()
25+
@Inject(MULTIPART_MODULE_OPTIONS)
26+
options: MultipartOptions = {},
27+
) {
28+
this.multipart = new MultipartWrapper({
29+
...options,
30+
...localOptions,
31+
});
32+
}
33+
34+
public async intercept(
35+
context: ExecutionContext,
36+
next: CallHandler,
37+
): Promise<Observable<any>> {
38+
const req = context.switchToHttp().getRequest();
39+
try {
40+
req[fieldname] = await this.multipart.files(fieldname, maxCount)(req);
41+
} catch (err) {
42+
throw transformException(err);
43+
}
44+
return next.handle();
45+
}
46+
}
47+
48+
const Interceptor = mixin(MixinInterceptor);
49+
return Interceptor as Type<NestInterceptor>;
50+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './any-files.interceptor';
2+
export * from './file-fields.interceptor';
3+
export * from './file.interceptor';
4+
export * from './files.interceptor';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Type } from '@nestjs/common';
2+
import { ModuleMetadata } from '@nestjs/common/interfaces';
3+
import { MultipartOptions } from './multipart-options.interface';
4+
5+
export type MultipartModuleOptions = MultipartOptions;
6+
7+
export interface MultipartOptionsFactory {
8+
createMultipartOptions():
9+
| Promise<MultipartModuleOptions>
10+
| MultipartModuleOptions;
11+
}
12+
13+
export interface MultipartModuleAsyncOptions
14+
extends Pick<ModuleMetadata, 'imports'> {
15+
useExisting?: Type<MultipartOptionsFactory>;
16+
useClass?: Type<MultipartOptionsFactory>;
17+
useFactory?: (
18+
...args: any[]
19+
) => Promise<MultipartModuleOptions> | MultipartModuleOptions;
20+
inject?: any[];
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './files-upload-module.interface';
2+
export * from './multipart-file.interface';
3+
export * from './multipart-options.interface';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export interface MultipartDiskFile extends MultipartFile {
2+
path: string;
3+
destination: string;
4+
}
5+
6+
interface MultipartFields {
7+
[x: string]: FastifyMultipartFile | FastifyMultipartFile[];
8+
}
9+
10+
export interface FastifyMultipartFile {
11+
toBuffer: () => Promise<Buffer>;
12+
file: NodeJS.ReadStream;
13+
filepath: string;
14+
fieldname: string;
15+
filename: string;
16+
encoding: string;
17+
mimetype: string;
18+
fields: MultipartFields;
19+
}
20+
21+
export interface MultipartFile extends FastifyMultipartFile {
22+
originalname: string;
23+
size: number;
24+
}
25+
26+
export type InterceptorFile = MultipartFile | MultipartDiskFile;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { FastifyMultipartFile } from './multipart-file.interface';
2+
3+
export interface MultipartOptions {
4+
/** Destination folder, if not undefined uploaded file will be saved locally in dest path */
5+
dest?: string;
6+
/**
7+
* An object specifying the size limits of the following optional properties. This object is passed to busboy
8+
* directly, and the details of properties can be found on https://github.com/mscdex/busboy#busboy-methods
9+
*/
10+
limits?: {
11+
/** Max field name size (in bytes) (Default: 100 bytes) */
12+
fieldnameSize?: number;
13+
/** Max field value size (in bytes) (Default: 1MB) */
14+
fieldSize?: number;
15+
/** Max number of non-file fields (Default: Infinity) */
16+
fields?: number;
17+
/** For multipart forms, the max file size (in bytes) (Default: Infinity) */
18+
fileSize?: number;
19+
/** For multipart forms, the max number of file fields (Default: Infinity) */
20+
files?: number;
21+
/** For multipart forms, the max number of parts (fields + files) (Default: Infinity) */
22+
parts?: number;
23+
/** For multipart forms, the max number of header key=>value pairs to parse Default: 2000 (same as node's http) */
24+
headerPairs?: number;
25+
};
26+
/** These are the HTTP headers of the incoming request, which are used by individual parsers */
27+
headers?: any;
28+
/** highWaterMark to use for this Busboy instance (Default: WritableStream default). */
29+
highWaterMark?: number;
30+
/** highWaterMark to use for file streams (Default: ReadableStream default) */
31+
fileHwm?: number;
32+
/** Default character set to use when one isn't defined (Default: 'utf8') */
33+
defCharset?: string;
34+
/** If paths in the multipart 'filename' field shall be preserved. (Default: false) */
35+
preservePath?: boolean;
36+
/** Function to control which files are accepted */
37+
fileFilter?(
38+
req: any,
39+
file: FastifyMultipartFile,
40+
callback: (error: Error | null, acceptFile?: boolean) => void,
41+
): void;
42+
}
43+
44+
export interface UploadField {
45+
/** The field name. */
46+
name: string;
47+
/** Optional maximum number of files per field to accept. */
48+
maxCount?: number;
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const MULTIPART_MODULE_ID = 'MULTIPART_MODULE_ID';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { DynamicModule, Module, Provider } from '@nestjs/common';
2+
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
3+
import { MULTIPART_MODULE_OPTIONS } from './files.constants';
4+
import {
5+
MultipartModuleAsyncOptions,
6+
MultipartModuleOptions,
7+
MultipartOptionsFactory,
8+
} from './interfaces/files-upload-module.interface';
9+
import { MULTIPART_MODULE_ID } from './multipart.constants';
10+
11+
@Module({})
12+
export class MultipartModule {
13+
static register(options: MultipartModuleOptions = {}): DynamicModule {
14+
return {
15+
module: MultipartModule,
16+
providers: [
17+
{ provide: MULTIPART_MODULE_OPTIONS, useValue: options },
18+
{
19+
provide: MULTIPART_MODULE_ID,
20+
useValue: randomStringGenerator(),
21+
},
22+
],
23+
exports: [MULTIPART_MODULE_OPTIONS],
24+
};
25+
}
26+
27+
static registerAsync(options: MultipartModuleAsyncOptions): DynamicModule {
28+
return {
29+
module: MultipartModule,
30+
imports: options.imports,
31+
providers: [
32+
...this.createAsyncProviders(options),
33+
{
34+
provide: MULTIPART_MODULE_ID,
35+
useValue: randomStringGenerator(),
36+
},
37+
],
38+
exports: [MULTIPART_MODULE_OPTIONS],
39+
};
40+
}
41+
42+
private static createAsyncProviders(
43+
options: MultipartModuleAsyncOptions,
44+
): Provider[] {
45+
if (options.useExisting || options.useFactory) {
46+
return [this.createAsyncOptionsProvider(options)];
47+
}
48+
return [
49+
this.createAsyncOptionsProvider(options),
50+
{
51+
provide: options.useClass,
52+
useClass: options.useClass,
53+
},
54+
];
55+
}
56+
57+
private static createAsyncOptionsProvider(
58+
options: MultipartModuleAsyncOptions,
59+
): Provider {
60+
if (options.useFactory) {
61+
return {
62+
provide: MULTIPART_MODULE_OPTIONS,
63+
useFactory: options.useFactory,
64+
inject: options.inject || [],
65+
};
66+
}
67+
return {
68+
provide: MULTIPART_MODULE_OPTIONS,
69+
useFactory: async (optionsFactory: MultipartOptionsFactory) =>
70+
optionsFactory.createMultipartOptions(),
71+
inject: [options.useExisting || options.useClass],
72+
};
73+
}
74+
}

0 commit comments

Comments
 (0)