fix: auto-append file extension based on magic bytes or mime type if missing

This commit is contained in:
MythEclipse
2026-05-18 09:20:03 +07:00
parent 5de424bf8e
commit 3d2500ed7f
2 changed files with 72 additions and 15 deletions
+30 -15
View File
@@ -1,7 +1,7 @@
import { nanoid } from 'nanoid';
import { db, files as fileSchema } from '../db';
import { config } from '../env';
import { checkFileSize, extractMimeType, getFileType } from '../utils/file';
import { checkFileSize, ensureExtension, extractMimeType, getFileType } from '../utils/file';
import logger from '../utils/logger';
import { forwardToStorage, getBot } from '../utils/telegram';
@@ -38,18 +38,23 @@ const handleMultipartUpload = async (req: Request): Promise<Response> => {
const fileBytes = await file.arrayBuffer();
const fileBuffer = Buffer.from(fileBytes);
const mimeType = file.type || extractMimeType({}, req) || 'application/octet-stream';
const fileType = getFileType(mimeType, fileName);
const rawMimeType = file.type || extractMimeType({}, req) || 'application/octet-stream';
const { fileName: finalFileName, mimeType } = ensureExtension(
fileName,
fileBuffer,
rawMimeType,
);
const fileType = getFileType(mimeType, finalFileName);
if (!checkFileSize(fileBuffer.byteLength, fileType)) {
return Response.json({ error: `File size exceeds ${fileType} limit` }, { status: 400 });
}
const isDocument =
fileName.endsWith('.pdf') ||
fileName.endsWith('.txt') ||
finalFileName.endsWith('.pdf') ||
finalFileName.endsWith('.txt') ||
!['photo', 'video', 'audio', 'voice', 'animation'].includes(fileType);
const result = await forwardToStorage(fileBuffer, fileName, isDocument);
const result = await forwardToStorage(fileBuffer, finalFileName, isDocument);
const bot = getBot();
const fileInfo = (await bot.telegram.getFile(result.telegramFileId)) as any;
@@ -59,7 +64,7 @@ const handleMultipartUpload = async (req: Request): Promise<Response> => {
telegramFileUniqueId: result.telegramFileUniqueId,
storageChatId: config.storageChatId,
storageMessageId: result.storageMessageId,
fileName: fileName,
fileName: finalFileName,
mimeType: fileInfo.mime_type || mimeType || 'application/octet-stream',
sizeBytes: fileInfo.file_size || fileBuffer.byteLength,
fileType: fileType,
@@ -103,22 +108,32 @@ const handleJSONUpload = async (req: Request): Promise<Response> => {
);
}
const fileBytes = Buffer.from(file, 'base64');
const mimeType = 'application/octet-stream';
let base64Data = file;
let rawMimeType = 'application/octet-stream';
if (file.startsWith('data:')) {
const match = file.match(/^data:([^;]+);base64,(.+)$/);
if (match) {
rawMimeType = match[1];
base64Data = match[2];
}
}
const fileBytes = Buffer.from(base64Data, 'base64');
const { fileName: finalFileName, mimeType } = ensureExtension(fileName, fileBytes, rawMimeType);
const fileType =
getFileType(mimeType, fileName) === 'application'
getFileType(mimeType, finalFileName) === 'application'
? 'document'
: getFileType(mimeType, fileName);
: getFileType(mimeType, finalFileName);
if (!checkFileSize(fileBytes.byteLength, fileType)) {
return Response.json({ error: `File size exceeds ${fileType} limit` }, { status: 400 });
}
const isDocument =
fileName.endsWith('.pdf') ||
fileName.endsWith('.txt') ||
finalFileName.endsWith('.pdf') ||
finalFileName.endsWith('.txt') ||
!['photo', 'video', 'audio', 'voice', 'animation'].includes(fileType);
const result = await forwardToStorage(fileBytes, fileName, isDocument);
const result = await forwardToStorage(fileBytes, finalFileName, isDocument);
const bot = getBot();
const fileInfo = (await bot.telegram.getFile(result.telegramFileId)) as any;
@@ -128,7 +143,7 @@ const handleJSONUpload = async (req: Request): Promise<Response> => {
telegramFileUniqueId: result.telegramFileUniqueId,
storageChatId: config.storageChatId,
storageMessageId: result.storageMessageId,
fileName: fileName,
fileName: finalFileName,
mimeType: fileInfo.mime_type || mimeType || 'application/octet-stream',
sizeBytes: fileInfo.file_size || fileBytes.byteLength,
fileType: fileType,
+42
View File
@@ -26,6 +26,48 @@ export const checkFileSize = (sizeBytes: number, fileType: string): boolean => {
return sizeBytes <= limit;
};
export const ensureExtension = (
fileName: string,
buffer: Buffer,
detectedMime?: string,
): { fileName: string; mimeType: string } => {
const mimeMap: Record<string, string> = {
'application/pdf': 'pdf',
'image/png': 'png',
'image/jpeg': 'jpg',
'image/gif': 'gif',
'text/plain': 'txt',
'application/zip': 'zip',
};
let ext: string | null = null;
if (buffer.subarray(0, 4).toString() === '%PDF') {
ext = 'pdf';
} else if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
ext = 'png';
} else if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
ext = 'jpg';
} else if (buffer.subarray(0, 4).toString() === 'GIF8') {
ext = 'gif';
} else if (detectedMime) {
ext = mimeMap[detectedMime.toLowerCase()] || null;
}
let finalFileName = fileName;
const hasExtension = fileName.includes('.') && fileName.split('.').pop()!.length >= 2;
if (!hasExtension && ext) {
finalFileName = `${fileName}.${ext}`;
}
const mimeType = ext
? Object.keys(mimeMap).find((k) => mimeMap[k] === ext) ||
detectedMime ||
'application/octet-stream'
: detectedMime || 'application/octet-stream';
return { fileName: finalFileName, mimeType };
};
export const extractFileName = (msg: any, request: any): string => {
if (request?.headers?.['x-file-name']) {
return request.headers['x-file-name'];