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:
32
server/src/common/multipart-filename.ts
Normal file
32
server/src/common/multipart-filename.ts
Normal 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+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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user