Skip to content

Commit ded8f05

Browse files
committed
feat: Add conditional check for updating file path in FilestorageService
1 parent 79ade1f commit ded8f05

File tree

4 files changed

+97
-19
lines changed

4 files changed

+97
-19
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { applyDecorators, Type } from '@nestjs/common';
2+
import { ApiBody, ApiBodyOptions, ApiConsumes, ApiExtraModels, ApiOperationOptions, getSchemaPath } from '@nestjs/swagger';
3+
import { ApiResponseOptions } from '@nestjs/swagger/dist/decorators/api-response.decorator';
4+
import { ApiBodyDecorator } from '~/_common/decorators/api-body.decorator';
5+
import { ApiCreatedResponseDecorator } from '~/_common/decorators/api-created-response.decorator';
6+
7+
export const ApiFileUploadDecorator = <TModel extends Type<NonNullable<unknown>>>(
8+
uploadModel: Type<NonNullable<unknown>>,
9+
bodyModel: TModel,
10+
responseModel: TModel,
11+
options?: {
12+
bodyOptions?: ApiBodyOptions | null | undefined,
13+
responseOptions?: ApiResponseOptions | null | undefined,
14+
operationOptions?: ApiOperationOptions | null | undefined,
15+
consumesOptions?: string[],
16+
},
17+
) => {
18+
const consumes = options?.consumesOptions || ['multipart/form-data'];
19+
20+
return applyDecorators(
21+
ApiExtraModels(uploadModel),
22+
ApiExtraModels(bodyModel),
23+
ApiBody({
24+
schema: {
25+
type: 'object',
26+
allOf: [
27+
{ $ref: getSchemaPath(bodyModel) },
28+
{ $ref: getSchemaPath(uploadModel) },
29+
],
30+
// properties: {
31+
// // comment: { type: 'string' },
32+
// // outletId: { type: 'integer' },
33+
34+
// file: {
35+
// type: 'string',
36+
// format: 'binary',
37+
// },
38+
// },
39+
},
40+
description: 'Corps de création de l\'enregistrement',
41+
...options?.bodyOptions,
42+
}),
43+
ApiCreatedResponseDecorator(responseModel, {
44+
responseOptions: options?.responseOptions,
45+
operationOptions: options?.operationOptions,
46+
}),
47+
ApiConsumes(...consumes),
48+
);
49+
};

src/core/filestorage/_dto/filestorage.dto.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,59 @@
11
import { ApiProperty, PartialType } from '@nestjs/swagger'
22
import { CustomFieldsDto } from '~/_common/abstracts/dto/custom-fields.dto'
3-
import { IsMongoId, IsString, IsEnum, IsOptional, IsObject, IsBoolean, IsNotEmpty, IsMimeType } from 'class-validator'
3+
import { IsMongoId, IsString, IsEnum, IsOptional, IsObject, IsBoolean, IsNotEmpty, IsMimeType, Matches } from 'class-validator'
44
import { FsType, FsTypeList } from '~/core/filestorage/_enum/fs-type.enum'
55
import { MixedValue } from '~/_common/types/mixed-value.type'
66

7+
export class FileUploadDto {
8+
@ApiProperty({ type: 'string', format: 'binary' })
9+
public file: string
10+
}
11+
712
export class FilestorageCreateDto extends CustomFieldsDto {
813
@IsEnum(FsTypeList)
914
@IsNotEmpty()
10-
@ApiProperty({ enum: FsTypeList })
15+
@ApiProperty({ enum: FsTypeList, default: FsType.FILE, })
1116
public type: FsType
1217

1318
@IsString()
1419
@IsNotEmpty()
15-
@ApiProperty()
20+
@ApiProperty({ type: String, required: true })
1621
public namespace: string
1722

1823
@IsString()
1924
@IsNotEmpty()
20-
@ApiProperty()
25+
@ApiProperty({ type: String, default: '/' })
26+
@Matches(/^\/(\.?[^\/\0]+\/?)+$/, { message: 'Path must be a valid path' })
2127
public path: string
2228

2329
@IsOptional()
2430
@IsMongoId()
25-
@ApiProperty({ type: String })
31+
@ApiProperty({ type: String, required: false })
2632
public linkedTo?: string
2733

2834
@IsOptional()
2935
@IsString()
30-
@ApiProperty()
36+
@ApiProperty({ type: String, required: false })
3137
public comments?: string
3238

3339
@IsOptional()
3440
@IsMimeType()
35-
@ApiProperty()
41+
@ApiProperty({ type: String, required: false })
3642
public mime?: string
3743

3844
@IsOptional()
3945
@IsBoolean()
40-
@ApiProperty()
46+
@ApiProperty({ type: Boolean, required: false })
4147
public hidden?: boolean
4248

4349
@IsOptional()
4450
@IsObject()
45-
@ApiProperty({ type: Object })
51+
@ApiProperty({ type: String, required: false, nullable: true })
4652
public tags?: { [key: string]: MixedValue }
4753

4854
@IsOptional()
4955
@IsObject()
50-
@ApiProperty({ type: Object })
56+
@ApiProperty({ type: String, required: false, nullable: true })
5157
public acls?: { [key: string]: any } // eslint-disable-line
5258
}
5359

src/core/filestorage/filestorage.controller.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Body, Controller, Delete, Get, HttpStatus, Param, ParseFilePipe, Patch, Post, Query, Res, UploadedFile, UseInterceptors } from '@nestjs/common'
1+
import { Body, Controller, Delete, Get, HttpStatus, Param, ParseBoolPipe, ParseFilePipe, Patch, Post, Query, Res, UploadedFile, UseInterceptors } from '@nestjs/common'
22
import { FileInterceptor } from '@nestjs/platform-express'
3-
import { ApiParam, ApiTags } from '@nestjs/swagger'
3+
import { ApiBody, ApiConsumes, ApiOperation, ApiParam, ApiProduces, ApiTags, getSchemaPath } from '@nestjs/swagger'
44
import { FilterOptions, FilterSchema, ObjectIdValidationPipe, SearchFilterOptions, SearchFilterSchema } from '@the-software-compagny/nestjs_module_restools'
55
import { Response } from 'express'
66
import { Types } from 'mongoose'
@@ -12,11 +12,12 @@ import { ApiReadResponseDecorator } from '~/_common/decorators/api-read-response
1212
import { ApiUpdateDecorator } from '~/_common/decorators/api-update.decorator'
1313
import { PickProjectionHelper } from '~/_common/helpers/pick-projection.helper'
1414
import { PartialProjectionType } from '~/_common/types/partial-projection.type'
15-
import { FilestorageCreateDto, FilestorageDto, FilestorageUpdateDto } from './_dto/filestorage.dto'
15+
import { FilestorageCreateDto, FilestorageDto, FilestorageUpdateDto, FileUploadDto } from './_dto/filestorage.dto'
1616
import { TransformersFilestorageService } from './_services/transformers-filestorage.service'
1717
import { FilestorageService } from './filestorage.service'
18+
import { ApiFileUploadDecorator } from '~/_common/decorators/api-file-upload.decorator'
1819

19-
@ApiTags('core')
20+
@ApiTags('default')
2021
@Controller('filestorage')
2122
export class FilestorageController extends AbstractController {
2223
protected static readonly projection: PartialProjectionType<FilestorageDto> = {
@@ -32,7 +33,7 @@ export class FilestorageController extends AbstractController {
3233

3334
@Post()
3435
@UseInterceptors(FileInterceptor('file'))
35-
@ApiCreateDecorator(FilestorageCreateDto, FilestorageDto)
36+
@ApiFileUploadDecorator(FileUploadDto, FilestorageCreateDto, FilestorageDto)
3637
public async create(
3738
@Res() res: Response,
3839
@Body() body: FilestorageCreateDto,
@@ -47,8 +48,21 @@ export class FilestorageController extends AbstractController {
4748

4849
@Get()
4950
@ApiPaginatedDecorator(PickProjectionHelper(FilestorageDto, FilestorageController.projection))
50-
public async search(@Res() res: Response, @SearchFilterSchema() searchFilterSchema: FilterSchema, @SearchFilterOptions() searchFilterOptions: FilterOptions): Promise<Response> {
51-
const [data, total] = await this._service.findAndCount(searchFilterSchema, FilestorageController.projection, searchFilterOptions)
51+
public async search(
52+
@Res() res: Response,
53+
@SearchFilterSchema() searchFilterSchema: FilterSchema,
54+
@SearchFilterOptions() searchFilterOptions: FilterOptions,
55+
@Query('hidden') hiddenQuery: string,
56+
): Promise<Response> {
57+
const hidden = /true|on|yes|1/i.test(hiddenQuery);
58+
const extraSearch = { hidden: { $ne: true } }
59+
if (hidden) delete extraSearch['hidden']
60+
61+
const [data, total] = await this._service.findAndCount({
62+
...extraSearch,
63+
...searchFilterSchema,
64+
}, FilestorageController.projection, searchFilterOptions)
65+
5266
return res.status(HttpStatus.OK).json({
5367
statusCode: HttpStatus.OK,
5468
total,

src/core/filestorage/filestorage.service.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,20 @@ export class FilestorageService extends AbstractServiceSchema {
4444
const basePath = this.reservedChars.reduce((acc, char) => {
4545
if (char === EMBED_SEPARATOR) return acc
4646
return acc.replace(char, '_')
47-
}, payload.path.replace(/^\//, '').replace(/\.\.\//g, ''))
48-
const partPath = ['', basePath]
47+
}, payload.path.replace(/(?<!^)\/{2,}|(?<!\/)\/+$/, ''))
48+
const partPath = [basePath]
4949
// noinspection ExceptionCaughtLocallyJS
5050
switch (data.type) {
5151
case FsType.FILE: {
52+
console.log('basePath', basePath)
53+
console.log('hasFileExtension', hasFileExtension(basePath))
5254
if (!basePath || !hasFileExtension(basePath)) {
5355
partPath.push(file.originalname)
5456
}
57+
const exists = await this.storage.getDisk(data.namespace).exists(partPath.join('/'))
58+
if (exists.exists) {
59+
throw new BadRequestException(`File ${partPath.join('/')} already exists`)
60+
}
5561
payload.mime = data.mime || file.mimetype
5662
await this.storage.getDisk(data.namespace).put(partPath.join('/'), file.buffer)
5763
break
@@ -150,7 +156,10 @@ export class FilestorageService extends AbstractServiceSchema {
150156
throw e
151157
}
152158
})
159+
} else {
160+
updated = await super.update(_id, update, options)
153161
}
162+
154163
return updated
155164
}
156165
/* eslint-enable */

0 commit comments

Comments
 (0)