import path from 'node:path' import { BadRequestException, Controller, Delete, Get, Param, Post, Query, Req, Res } from '@nestjs/common' import { ApiTags } from '@nestjs/swagger' import { Throttle } from '@nestjs/throttler' import { Bypass } from '@server/common/decorators/bypass.decorator' import { PagerDto } from '@server/common/dto/pager.dto' import { BizException } from '@server/common/exceptions/biz.exception' import { ErrorCodeEnum } from '@server/constants/error-code.constant' import { alphabet } from '@server/constants/other.constant' import { FastifyReply, FastifyRequest } from 'fastify' import { lookup } from 'mime-types' import { customAlphabet } from 'nanoid' import { Public } from '../auth/decorators/public.decorator' import { FileQueryDto, FileUploadDto } from './file.dto' import { FileService } from './file.service' @ApiTags('System - 文件模块') @Controller(['objects', 'files']) export class FileController { constructor( private readonly fileService: FileService, ) { } @Get('/:type') async getTypes(@Query() query: PagerDto, @Param() params: FileUploadDto) { const { type = 'file' } = params // const { page, size } = query const dir = await this.fileService.getDir(type) return Promise.all( dir.map(async (name) => { return { name, url: await this.fileService.resolveFileUrl(type, name) } }), ) } @Get('/:type/:name') @Throttle({ default: { limit: 60, ttl: 60_000 } }) @Bypass() @Public() async get(@Param() params: FileQueryDto, @Res() res: FastifyReply) { const { type, name } = params const ext = path.extname(name) const mimetype = lookup(ext) try { const stream = await this.fileService.getFileStream(type, name) if (mimetype) { res.type(mimetype) res.header('cache-control', 'public, max-age=31536000') res.header( 'expires', new Date(Date.now() + 31536000 * 1000).toUTCString(), ) } return res.send(stream) } catch { throw new BizException(ErrorCodeEnum.ResourceNotFound) } } @Post('/upload') async upload(@Query() query: FileUploadDto, @Req() req: FastifyRequest) { const file = await req.file() if (!file) throw new BadRequestException('仅供上传文件!') if (file.fieldname !== 'file') throw new BadRequestException('字段必须为 file') const { type = 'file' } = query const ext = path.extname(file.filename) const filename = customAlphabet(alphabet)(18) + ext.toLowerCase() if (!(await this.fileService.exists(type, filename))) await this.fileService.writeFile(type, filename, file.file) // TODO: save record in dabase return { url: await this.fileService.resolveFileUrl(type, filename), name: filename, } } @Post('/upload/multiple') async uploadMultiple(@Query() query: FileUploadDto, @Req() req: FastifyRequest) { const { type = 'file' } = query const uploadedFiles: any[] = [] const files = req.files() for await (const file of files) { const ext = path.extname(file.filename) const filename = customAlphabet(alphabet)(18) + ext.toLowerCase() if (!(await this.fileService.exists(type, filename))) await this.fileService.writeFile(type, filename, file.file) uploadedFiles.push({ url: await this.fileService.resolveFileUrl(type, filename), name: filename, }) } return uploadedFiles } @Delete('/:type/:name') async delete(@Param() params: FileQueryDto) { const { type, name } = params await this.fileService.deleteFile(type, name) } }