diff --git a/server/src/common/multipart-filename.ts b/server/src/common/multipart-filename.ts new file mode 100644 index 0000000..9a43c85 --- /dev/null +++ b/server/src/common/multipart-filename.ts @@ -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+0000–U+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; + } +} diff --git a/server/src/modules/equipment/equipment.service.ts b/server/src/modules/equipment/equipment.service.ts index e2e2027..0997621 100644 --- a/server/src/modules/equipment/equipment.service.ts +++ b/server/src/modules/equipment/equipment.service.ts @@ -8,6 +8,7 @@ import { EquipmentStatus, Prisma } from '@prisma/client'; import type { Equipment } from '@prisma/client'; import { Response } from 'express'; import type { Express } from 'express'; +import { decodeUtf8FilenameFromMultipart } from '../../common/multipart-filename'; import { setListHeaders } from '../../common/http'; import { PrismaService } from '../../prisma/prisma.service'; import type { StoredAttachmentMeta } from '../../storage/storage.service'; @@ -133,10 +134,11 @@ export class EquipmentService { 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 = { objectKey, - originalFileName: file.originalname, + originalFileName: decodedOriginalName, contentType: file.mimetype || 'application/octet-stream', sizeBytes: file.size, }; @@ -217,7 +219,7 @@ export class EquipmentService { } return { objectKey: raw.objectKey, - originalFileName: raw.originalFileName, + originalFileName: decodeUtf8FilenameFromMultipart(raw.originalFileName), contentType: raw.contentType, sizeBytes: raw.sizeBytes, downloadUrl,