Skip to content

Commit 94b6bab

Browse files
committed
- autoDeleteFile config field separated to two fields: cleanupAfterSuccessHandle and cleanupAfterFailedHandle
- Extended `HasMimeType` validator, added regex support and asterisk match - Extended tests for cover a new functionality - Resolved [Issue 56](#56) - Resolved [Issue 57](#57) - Updated README.md - described additional information about the usage of new config fields and validations
1 parent d46a886 commit 94b6bab

File tree

12 files changed

+280
-35
lines changed

12 files changed

+280
-35
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
### v1.9.7
2+
- `autoDeleteFile` config field separated to two fields: `cleanupAfterSuccessHandle` and `cleanupAfterFailedHandle`
3+
- Extended `HasMimeType` validator, added regex support and asterisk match
4+
- Extended tests for cover a new functionality
5+
- Resolved [Issue 56](https://github.com/dmitriy-nz/nestjs-form-data/issues/56)
6+
- Resolved [Issue 57](https://github.com/dmitriy-nz/nestjs-form-data/issues/57)
7+
- Updated README.md - described additional information about the usage of new config fields and validations
8+
19
### v1.9.6
210
- Updated peer deps: `reflect-metadata^0.2.0`
311
- Resolved [Issue 58](https://github.com/dmitriy-nz/nestjs-form-data/issues/58)

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ export class NestjsFormDataController {
167167
- `isGlobal` - If you want the module to be available globally. Once you import the module and configure it, it will be available globally
168168
- `storage` - The type of storage logic for the uploaded file (Default MemoryStoredFile)
169169
- `fileSystemStoragePath` - The path to the directory for storing temporary files, used only for `storage: FileSystemStoredFile` (Default: /tmp/nestjs-tmp-storage)
170-
- `autoDeleteFile` - Automatically delete files after the request ends (Default true)
170+
- `cleanupAfterSuccessHandle` - If set to true, all processed and uploaded files will be deleted after successful processing by the final method. This means that the `delete` method will be called on all files (StoredFile)
171+
- `cleanupAfterFailedHandle` - If set to true, all processed and uploaded files will be deleted after unsuccessful processing by the final method. This means that the `delete` method will be called on all files (StoredFile)
171172
- `limits` - [busboy](https://www.npmjs.com/package/busboy#busboy-methods) limits configuration. Constraints in this declaration are handled at the serialization stage, so using these parameters is preferable for performance.
172173
## File storage types
173174
### Memory storage
@@ -216,8 +217,21 @@ The library uses two sources to get the mime type for the file:
216217
The default is simple mode, which does not check the data source, but you can pass a second argument to strictly check the mime-type and data source.
217218
You can also get the mime type and data source via the `get mimeTypeWithSource():MetaFieldSource` getter on the `StoredFile`
218219

220+
221+
```ts
222+
type AllowedMimeTypes = Array<AllowedMimeType>
223+
type AllowedMimeType = string | RegExp;
224+
225+
@HasMimeType(allowedMimeTypes: AllowedMimeTypes | AllowedMimeType, strictSource?: MetaSource | ValidationOptions, validationOptions?: ValidationOptions)
226+
```
227+
228+
You can also use partial matching, just pass the unimportant parameter as `*`, for example:
229+
```ts
230+
@HasMimeType('image/*')
231+
```
232+
also as array:
219233
```ts
220-
@HasMimeType(allowedMimeTypes: string[] | string, strictSource?: MetaSource | ValidationOptions, validationOptions?: ValidationOptions)
234+
@HasMimeType(['image/*', 'text/*'])
221235
```
222236

223237
### HasExtension

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nestjs-form-data",
3-
"version": "1.9.6",
3+
"version": "1.9.7",
44
"description": "NestJS middleware for handling multipart/form-data, which is primarily used for uploading files",
55
"main": "dist/index",
66
"types": "dist/index",

src/config/default.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { MemoryStoredFile } from '../classes/storage/MemoryStoredFile';
33

44
export const DEFAULT_CONFIG: FormDataInterceptorConfig = {
55
storage: MemoryStoredFile,
6-
autoDeleteFile: true,
6+
cleanupAfterSuccessHandle: true,
7+
cleanupAfterFailedHandle: true,
78
fileSystemStoragePath: '/tmp/nestjs-tmp-storage',
8-
};
9+
};

src/decorators/validation/has-mime-type.validator.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,53 @@ import { isFile } from './is-file.validator';
44
import { toArray } from '../../helpers/toArray';
55
import { MetaFieldSource, MetaSource } from '../../interfaces/MetaFieldSource';
66

7-
export function HasMimeType(allowedMimeTypes: string[] | string, strictSource?: MetaSource | ValidationOptions, validationOptions?: ValidationOptions): PropertyDecorator {
7+
8+
export type AllowedMimeTypes = Array<AllowedMimeType>
9+
export type AllowedMimeType = string | RegExp;
10+
11+
export function HasMimeType(allowedMimeTypes: AllowedMimeTypes | AllowedMimeType, strictSource?: MetaSource | ValidationOptions, validationOptions?: ValidationOptions): PropertyDecorator {
812

913
return ValidateBy({
1014
name: 'HasMimeType',
1115
constraints: [allowedMimeTypes],
1216
validator: {
1317

1418
validate(value: StoredFile, args: ValidationArguments) {
15-
const allowedMimeTypes: string[] = toArray(args.constraints[0]) || [];
19+
const allowedMimeTypes: AllowedMimeTypes = toArray(args.constraints[0]) || [];
1620
const strictSource: MetaSource = (typeof args.constraints[1] === 'string')
1721
? args.constraints[1] as MetaSource
1822
: undefined;
1923

2024
if (isFile(value)) {
2125
const mimeWithSource: MetaFieldSource = value.mimeTypeWithSource;
22-
return allowedMimeTypes.includes(mimeWithSource.value) && (!strictSource || strictSource === mimeWithSource.source);
26+
const hasSourceMatch = !strictSource || strictSource === mimeWithSource.source;
27+
28+
if (!hasSourceMatch) {
29+
return false;
30+
}
31+
32+
for (let mimeType of allowedMimeTypes) {
33+
switch (true) {
34+
case typeof mimeType === 'string' && !mimeType.includes('*'):
35+
if (mimeType === mimeWithSource.value) {
36+
return true;
37+
}
38+
break;
39+
case typeof mimeType === 'string' && mimeType.includes('*'):
40+
const regex = new RegExp(`^${mimeType as string}$`.replace('*', '.+'));
41+
if (regex.test(mimeWithSource.value)) {
42+
return true;
43+
}
44+
break;
45+
case mimeType instanceof RegExp:
46+
if ((mimeType as RegExp).test(mimeWithSource.value)) {
47+
return true;
48+
}
49+
break;
50+
default:
51+
throw new Error(`Unknown mime type for validate`);
52+
}
53+
}
2354
}
2455

2556
return false;

src/helpers/check-config.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@ import { FormDataInterceptorConfig } from '../interfaces/FormDataInterceptorConf
22
import { DEFAULT_CONFIG } from '../config/default.config';
33

44
export function checkConfig(config: FormDataInterceptorConfig, defaults: FormDataInterceptorConfig = DEFAULT_CONFIG): FormDataInterceptorConfig {
5+
config = Object.assign({}, config)
56

67
if (!config.storage)
78
config.storage = defaults.storage;
89

9-
if (config.autoDeleteFile === undefined)
10-
config.autoDeleteFile = defaults.autoDeleteFile;
10+
11+
if(config.cleanupAfterSuccessHandle === undefined){
12+
config.cleanupAfterSuccessHandle = defaults.cleanupAfterSuccessHandle;
13+
}
14+
15+
if(config.cleanupAfterFailedHandle === undefined){
16+
config.cleanupAfterFailedHandle = defaults.cleanupAfterFailedHandle;
17+
}
1118

1219
if (!config.fileSystemStoragePath)
1320
config.fileSystemStoragePath = defaults.fileSystemStoragePath;
1421

1522
return Object.assign({}, defaults, config);
16-
}
23+
}

src/interceptors/FormData.interceptor.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,12 @@ export class FormDataInterceptor implements NestInterceptor {
5858
}),
5959

6060
catchError((err) => {
61-
if (config.autoDeleteFile) formReader.deleteFiles();
61+
if (config.cleanupAfterFailedHandle || config.autoDeleteFile) formReader.deleteFiles();
6262
return throwError(err);
6363
}),
6464

65-
tap((res) => {
66-
if (config.autoDeleteFile) formReader.deleteFiles();
65+
tap(() => {
66+
if (config.cleanupAfterSuccessHandle || config.autoDeleteFile) formReader.deleteFiles();
6767
}),
6868
);
6969
}

src/interfaces/FormDataInterceptorConfig.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,32 @@ import { Type } from '@nestjs/common';
44
export interface FormDataInterceptorConfig {
55
storage?: Type<StoredFile>,
66
fileSystemStoragePath?: string;
7+
8+
/**
9+
* @deprecated
10+
* Use `cleanupAfterSuccessHandle` and `cleanupAfterFailedHandle` instead;
11+
*/
712
autoDeleteFile?: boolean;
13+
14+
/**
15+
* Indicates whether cleanup should be performed after successful handling.
16+
* If set to true, all processed and uploaded files will be deleted after successful processing by the final method.
17+
* This means that the `delete` method will be called on all files (StoredFile)
18+
* @type {boolean}
19+
* @default true
20+
*/
21+
cleanupAfterSuccessHandle?: boolean;
22+
23+
/**
24+
* Indicates whether cleanup should be performed after error handling.
25+
* If set to true, all processed and uploaded files will be deleted after unsuccessful processing by the final method.
26+
* This means that the `delete` method will be called on all files (StoredFile)
27+
* @type {boolean}
28+
* @default true
29+
*/
30+
cleanupAfterFailedHandle?: boolean;
31+
32+
833
limits?: FormDataInterceptorLimitsConfig;
934
/**
1035
* If you want the module to be available globally

test/auto-delete.e2e-spec.ts

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
import { INestApplication } from '@nestjs/common';
22
import { NestFastifyApplication } from '@nestjs/platform-fastify';
3-
import * as request from 'supertest';
3+
import request from 'supertest';
44
import path from 'path';
55
import { createTestModule } from './helpers/create-test-module';
66
import * as fs from 'fs';
77

88
describe('Express - Auto delete', () => {
99
let app: INestApplication;
10+
let appCleanupAfterSuccessHandleDisabled: INestApplication;
11+
let appCleanupAfterFailedHandle: INestApplication;
1012

1113
beforeEach(async () => {
1214
app = await createTestModule();
15+
appCleanupAfterSuccessHandleDisabled = await createTestModule({
16+
cleanupAfterSuccessHandle: false
17+
});
18+
appCleanupAfterFailedHandle = await createTestModule({
19+
cleanupAfterFailedHandle: false
20+
});
1321
});
1422

15-
it('Delete after success upload', () => {
16-
return request
17-
.default(app.getHttpServer())
23+
it('Delete after success upload (cleanupAfterSuccessHandle: true)', () => {
24+
return request(app.getHttpServer())
1825
.post('/auto-delete-single-file')
1926
.attach('file', path.resolve(__dirname, 'test-files', 'file.txt'))
2027
.expect(200)
@@ -24,9 +31,19 @@ describe('Express - Auto delete', () => {
2431
});
2532
});
2633

27-
it('Delete after failed upload (class validation)', () => {
28-
return request
29-
.default(app.getHttpServer())
34+
it('Delete after success upload (cleanupAfterSuccessHandle: false)', () => {
35+
return request(appCleanupAfterSuccessHandleDisabled.getHttpServer())
36+
.post('/auto-delete-single-file')
37+
.attach('file', path.resolve(__dirname, 'test-files', 'file.txt'))
38+
.expect(200)
39+
.expect((res: any) => {
40+
expect(typeof res.body.path).toBe('string');
41+
expect(fs.existsSync(res.body.path)).toBe(true);
42+
});
43+
});
44+
45+
it('Delete after failed upload (cleanupAfterFailedHandle:true, class validation)', () => {
46+
return request(app.getHttpServer())
3047
.post('/auto-delete-single-file')
3148
.attach('file', path.resolve(__dirname, 'test-files', 'file-large.txt'))
3249
.expect(400)
@@ -35,20 +52,42 @@ describe('Express - Auto delete', () => {
3552
expect(fs.existsSync(res.body.message[0])).toBe(false);
3653
});
3754
});
55+
56+
it('Delete after failed upload (cleanupAfterFailedHandle:true, class validation)', () => {
57+
return request(appCleanupAfterFailedHandle.getHttpServer())
58+
.post('/auto-delete-single-file')
59+
.attach('file', path.resolve(__dirname, 'test-files', 'file-large.txt'))
60+
.expect(400)
61+
.expect((res: any) => {
62+
expect(typeof res.body.message[0]).toBe('string');
63+
expect(fs.existsSync(res.body.message[0])).toBe(true);
64+
});
65+
});
3866
});
3967

4068
describe('Fastify - Auto delete', () => {
4169
let app: NestFastifyApplication;
70+
let appCleanupAfterSuccessHandleDisabled: NestFastifyApplication;
71+
let appCleanupAfterFailedHandle: NestFastifyApplication;
4272

4373
beforeEach(async () => {
44-
app = (await createTestModule({
74+
app = await createTestModule({
75+
fastify: true,
76+
}) as NestFastifyApplication;
77+
78+
appCleanupAfterSuccessHandleDisabled = await createTestModule({
4579
fastify: true,
46-
})) as NestFastifyApplication;
80+
cleanupAfterSuccessHandle: false
81+
}) as NestFastifyApplication;
82+
83+
appCleanupAfterFailedHandle = await createTestModule({
84+
fastify: true,
85+
cleanupAfterFailedHandle: false
86+
}) as NestFastifyApplication;
4787
});
4888

49-
it('Delete after success upload', () => {
50-
return request
51-
.default(app.getHttpServer())
89+
it('Delete after success upload (cleanupAfterSuccessHandle: true)', () => {
90+
return request(app.getHttpServer())
5291
.post('/auto-delete-single-file')
5392
.attach('file', path.resolve(__dirname, 'test-files', 'file.txt'))
5493
.expect(200)
@@ -58,9 +97,19 @@ describe('Fastify - Auto delete', () => {
5897
});
5998
});
6099

61-
it('Delete after failed upload (class validation)', () => {
62-
return request
63-
.default(app.getHttpServer())
100+
it('Delete after success upload (cleanupAfterSuccessHandle: false)', () => {
101+
return request(appCleanupAfterSuccessHandleDisabled.getHttpServer())
102+
.post('/auto-delete-single-file')
103+
.attach('file', path.resolve(__dirname, 'test-files', 'file.txt'))
104+
.expect(200)
105+
.expect((res: any) => {
106+
expect(typeof res.body.path).toBe('string');
107+
expect(fs.existsSync(res.body.path)).toBe(true);
108+
});
109+
});
110+
111+
it('Delete after failed upload (cleanupAfterFailedHandle:true, class validation)', () => {
112+
return request(app.getHttpServer())
64113
.post('/auto-delete-single-file')
65114
.attach('file', path.resolve(__dirname, 'test-files', 'file-large.txt'))
66115
.expect(400)
@@ -69,4 +118,15 @@ describe('Fastify - Auto delete', () => {
69118
expect(fs.existsSync(res.body.message[0])).toBe(false);
70119
});
71120
});
121+
122+
it('Delete after failed upload (cleanupAfterFailedHandle:true, class validation)', () => {
123+
return request(appCleanupAfterFailedHandle.getHttpServer())
124+
.post('/auto-delete-single-file')
125+
.attach('file', path.resolve(__dirname, 'test-files', 'file-large.txt'))
126+
.expect(400)
127+
.expect((res: any) => {
128+
expect(typeof res.body.message[0]).toBe('string');
129+
expect(fs.existsSync(res.body.message[0])).toBe(true);
130+
});
131+
});
72132
});

test/test-module/controllers/test.controller.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class TestController {
4040

4141
@Post('auto-delete-single-file')
4242
@UsePipes(new ValidationPipe({transform: true}))
43-
@FormDataRequest({ autoDeleteFile: true, storage: FileSystemStoredFile })
43+
@FormDataRequest({ storage: FileSystemStoredFile })
4444
@HttpCode(HttpStatus.OK)
4545
uploadSingleWithAutoDeleteFile(@Body() singleFileDto: UploadSingleFileFSStorageDto) {
4646
return {
@@ -52,7 +52,7 @@ export class TestController {
5252

5353
@Post('auto-delete-single-file-busboy')
5454
@UsePipes(new ValidationPipe({transform: true}))
55-
@FormDataRequest({ autoDeleteFile: true, storage: FileSystemStoredFile, limits: { fileSize: 5 } })
55+
@FormDataRequest({ storage: FileSystemStoredFile, limits: { fileSize: 5 } })
5656
@HttpCode(HttpStatus.OK)
5757
uploadSingleWithAutoDeleteFileBusboySizeLimit(@Body() singleFileDto: UploadSingleFileFSStorageDto) {
5858
return {
@@ -82,7 +82,7 @@ export class TestController {
8282
@FormDataRequest()
8383
@HttpCode(HttpStatus.OK)
8484
mimeTypeValidator(@Body() dto: MimeTypeValidatorDto) {
85-
const file: MemoryStoredFile = dto.file || dto.strictMagicNumber || dto.strictContentType || dto.any;
85+
const file: MemoryStoredFile = dto.file || dto.strictMagicNumber || dto.strictContentType || dto.any || dto.filePartial || dto.filePartialArray?.[0] || dto.fileRegex;
8686

8787
return {
8888
filename: file.originalName,

test/test-module/dto/MimeTypeValidator.dto.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { MemoryStoredFile } from '../../../src/classes/storage';
2-
import { IsFile, HasMimeType } from '../../../src/decorators';
2+
import { IsFile, HasMimeType, IsFiles } from '../../../src/decorators';
33
import { MetaSource } from '../../../src/interfaces/MetaFieldSource';
44
import { IsOptional } from 'class-validator';
55

@@ -24,4 +24,19 @@ export class MimeTypeValidatorDto {
2424
@IsFile()
2525
any: MemoryStoredFile;
2626

27+
@IsOptional()
28+
@IsFile()
29+
@HasMimeType('image/*')
30+
filePartial?: MemoryStoredFile;
31+
32+
@IsOptional()
33+
@IsFiles()
34+
@HasMimeType(['image/*'])
35+
filePartialArray?: MemoryStoredFile[];
36+
37+
@IsOptional()
38+
@IsFile()
39+
@HasMimeType(/^image\/webp$/)
40+
fileRegex?: MemoryStoredFile;
41+
2742
}

0 commit comments

Comments
 (0)