feat: harden CMPP delivery and platform workflows
This commit is contained in:
@@ -50,7 +50,7 @@ export class FilesController {
|
||||
@Post('upload')
|
||||
@UseInterceptors(FileInterceptor('file', {
|
||||
limits: {
|
||||
fileSize: 20 * 1024 * 1024,
|
||||
fileSize: 10 * 1024 * 1024,
|
||||
files: 1,
|
||||
fields: 4,
|
||||
parts: 5,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { FilesService } from './files.service';
|
||||
|
||||
describe('FilesService', () => {
|
||||
@@ -81,6 +82,26 @@ describe('FilesService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects images over 2MB and other files over 10MB before object storage writes', async () => {
|
||||
const prisma = { fileObject: { create: jest.fn() } };
|
||||
const objectStorage = { putObject: jest.fn(), getBucket: jest.fn().mockReturnValue('bucket') };
|
||||
const service = new FilesService(prisma as never, objectStorage as never);
|
||||
|
||||
await expect(service.upload({ purpose: 'test' }, {
|
||||
originalname: 'large.png',
|
||||
mimetype: 'image/png',
|
||||
size: 2 * 1024 * 1024 + 1,
|
||||
buffer: Buffer.alloc(0),
|
||||
})).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(service.upload({ purpose: 'test' }, {
|
||||
originalname: 'large.pdf',
|
||||
mimetype: 'application/pdf',
|
||||
size: 10 * 1024 * 1024 + 1,
|
||||
buffer: Buffer.alloc(0),
|
||||
})).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(objectStorage.putObject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('downloads file content from object storage by FileObject id', async () => {
|
||||
const fileObject = {
|
||||
id: 'file-1',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
@@ -65,6 +65,7 @@ export class FilesService {
|
||||
}
|
||||
|
||||
async upload(data: UploadFileDto, file: { originalname: string; mimetype: string; size: number; buffer: Buffer }) {
|
||||
assertUploadSize(file);
|
||||
const fileName = normalizeMultipartFileName(file.originalname);
|
||||
const safeName = fileName.replace(/[^\w.\-\u4e00-\u9fa5]/g, '_');
|
||||
const objectKey = `${data.prefix ?? data.purpose}/${Date.now()}-${randomUUID()}-${safeName}`;
|
||||
@@ -93,6 +94,18 @@ export class FilesService {
|
||||
}
|
||||
}
|
||||
|
||||
const IMAGE_UPLOAD_MAX_BYTES = 2 * 1024 * 1024;
|
||||
const FILE_UPLOAD_MAX_BYTES = 10 * 1024 * 1024;
|
||||
const IMAGE_FILE_EXTENSION = /\.(?:avif|bmp|gif|heic|heif|jpe?g|png|svg|webp)$/i;
|
||||
|
||||
function assertUploadSize(file: { originalname: string; mimetype: string; size: number }) {
|
||||
const image = file.mimetype.toLowerCase().startsWith('image/') || IMAGE_FILE_EXTENSION.test(file.originalname);
|
||||
const limit = image ? IMAGE_UPLOAD_MAX_BYTES : FILE_UPLOAD_MAX_BYTES;
|
||||
if (file.size > limit) {
|
||||
throw new BadRequestException(image ? '图片大小不能超过 2MB' : '文件大小不能超过 10MB');
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMultipartFileName(value: string) {
|
||||
if (![...value].some((character) => character.charCodeAt(0) > 0x7f) || [...value].some((character) => character.charCodeAt(0) > 0xff)) {
|
||||
return value;
|
||||
|
||||
Reference in New Issue
Block a user