diff --git a/src/routes/upload.ts b/src/routes/upload.ts index 0726dc2..8d03df1 100644 --- a/src/routes/upload.ts +++ b/src/routes/upload.ts @@ -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 => { 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 => { 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 => { ); } - 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 => { 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, diff --git a/src/utils/file.ts b/src/utils/file.ts index 2ecb56f..03b37e4 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -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 = { + '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'];