Add UTF-8 filename decoding for multipart uploads in EquipmentService

- Introduced a new utility function to decode filenames from multipart uploads that may be misencoded as ISO-8859-1.
- Updated EquipmentService to utilize this function when processing file attachments, ensuring correct original file names are stored and returned.
This commit is contained in:
Первов Артем
2026-04-21 01:15:37 +03:00
parent 7735988ed5
commit eb36c04a4b
2 changed files with 37 additions and 3 deletions

View File

@@ -0,0 +1,32 @@
/**
* Busboy/Multer often pass multipart `filename` where UTF-8 bytes were read as ISO-8859-1,
* which looks like mojibake in the UI (e.g. Cyrillic → "Акт...").
* Re-encode the string as Latin-1 bytes and decode as UTF-8.
*
* Only applied when every character is in U+0000U+00FF (typical for this bug).
*/
export function decodeUtf8FilenameFromMultipart(name: string): string {
if (!name || name.length < 2) {
return name;
}
for (let i = 0; i < name.length; i += 1) {
if (name.charCodeAt(i) > 0xff) {
return name;
}
}
if (!/[\u00a1-\u00ff]{2}/.test(name)) {
return name;
}
try {
const decoded = Buffer.from(name, 'latin1').toString('utf8');
if (decoded.includes('\uFFFD')) {
return name;
}
if (decoded === name) {
return name;
}
return decoded;
} catch {
return name;
}
}

View File

@@ -8,6 +8,7 @@ import { EquipmentStatus, Prisma } from '@prisma/client';
import type { Equipment } from '@prisma/client'; import type { Equipment } from '@prisma/client';
import { Response } from 'express'; import { Response } from 'express';
import type { Express } from 'express'; import type { Express } from 'express';
import { decodeUtf8FilenameFromMultipart } from '../../common/multipart-filename';
import { setListHeaders } from '../../common/http'; import { setListHeaders } from '../../common/http';
import { PrismaService } from '../../prisma/prisma.service'; import { PrismaService } from '../../prisma/prisma.service';
import type { StoredAttachmentMeta } from '../../storage/storage.service'; import type { StoredAttachmentMeta } from '../../storage/storage.service';
@@ -133,10 +134,11 @@ export class EquipmentService {
await this.deleteStoredAttachmentIfAny(item); await this.deleteStoredAttachmentIfAny(item);
const objectKey = this.storage.buildEquipmentObjectKey(id, file.originalname); const decodedOriginalName = decodeUtf8FilenameFromMultipart(file.originalname);
const objectKey = this.storage.buildEquipmentObjectKey(id, decodedOriginalName);
const meta: StoredAttachmentMeta = { const meta: StoredAttachmentMeta = {
objectKey, objectKey,
originalFileName: file.originalname, originalFileName: decodedOriginalName,
contentType: file.mimetype || 'application/octet-stream', contentType: file.mimetype || 'application/octet-stream',
sizeBytes: file.size, sizeBytes: file.size,
}; };
@@ -217,7 +219,7 @@ export class EquipmentService {
} }
return { return {
objectKey: raw.objectKey, objectKey: raw.objectKey,
originalFileName: raw.originalFileName, originalFileName: decodeUtf8FilenameFromMultipart(raw.originalFileName),
contentType: raw.contentType, contentType: raw.contentType,
sizeBytes: raw.sizeBytes, sizeBytes: raw.sizeBytes,
downloadUrl, downloadUrl,