fix: complete first version issue remediation
This commit is contained in:
@@ -49,6 +49,38 @@ describe('FilesService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('restores UTF-8 filenames that multipart parsing exposed as Latin-1', async () => {
|
||||
const prisma = {
|
||||
fileObject: {
|
||||
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'file-1', ...data })),
|
||||
},
|
||||
};
|
||||
const objectStorage = {
|
||||
getBucket: jest.fn().mockReturnValue('cmpp-platform'),
|
||||
putObject: jest.fn().mockResolvedValue({ etag: 'etag-1' }),
|
||||
presignedPutObject: jest.fn(),
|
||||
};
|
||||
const service = new FilesService(prisma as never, objectStorage as never);
|
||||
const originalname = Buffer.from('营业执照.png', 'utf8').toString('latin1');
|
||||
|
||||
await service.upload({ purpose: 'enterprise_photo' }, {
|
||||
originalname,
|
||||
mimetype: 'image/png',
|
||||
size: 12,
|
||||
buffer: Buffer.from('file-content'),
|
||||
});
|
||||
|
||||
expect(prisma.fileObject.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ fileName: '营业执照.png' }),
|
||||
});
|
||||
expect(objectStorage.putObject).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/营业执照\.png$/),
|
||||
expect.any(Buffer),
|
||||
12,
|
||||
'image/png',
|
||||
);
|
||||
});
|
||||
|
||||
it('downloads file content from object storage by FileObject id', async () => {
|
||||
const fileObject = {
|
||||
id: 'file-1',
|
||||
|
||||
@@ -66,14 +66,15 @@ export class FilesService {
|
||||
}
|
||||
|
||||
async upload(data: UploadFileDto, file: { originalname: string; mimetype: string; size: number; buffer: Buffer }) {
|
||||
const safeName = file.originalname.replace(/[^\w.\-\u4e00-\u9fa5]/g, '_');
|
||||
const fileName = normalizeMultipartFileName(file.originalname);
|
||||
const safeName = fileName.replace(/[^\w.\-\u4e00-\u9fa5]/g, '_');
|
||||
const objectKey = `${data.prefix ?? data.purpose}/${Date.now()}-${randomUUID()}-${safeName}`;
|
||||
await this.objectStorage.putObject(objectKey, file.buffer, file.size, file.mimetype || 'application/octet-stream');
|
||||
return this.create({
|
||||
tenantId: data.tenantId,
|
||||
bucket: this.objectStorage.getBucket(),
|
||||
objectKey,
|
||||
fileName: file.originalname,
|
||||
fileName,
|
||||
contentType: file.mimetype || 'application/octet-stream',
|
||||
sizeBytes: file.size,
|
||||
purpose: data.purpose,
|
||||
@@ -93,6 +94,15 @@ export class FilesService {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMultipartFileName(value: string) {
|
||||
if (![...value].some((character) => character.charCodeAt(0) > 0x7f) || [...value].some((character) => character.charCodeAt(0) > 0xff)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const decoded = Buffer.from(value, 'latin1').toString('utf8');
|
||||
return decoded.includes('\uFFFD') ? value : decoded;
|
||||
}
|
||||
|
||||
function serializeFileObject<T extends { sizeBytes: bigint | number | string }>(fileObject: T) {
|
||||
return {
|
||||
...fileObject,
|
||||
|
||||
Reference in New Issue
Block a user