fix: auto-append file extension based on magic bytes or mime type if missing
This commit is contained in:
+30
-15
@@ -1,7 +1,7 @@
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { db, files as fileSchema } from '../db';
|
import { db, files as fileSchema } from '../db';
|
||||||
import { config } from '../env';
|
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 logger from '../utils/logger';
|
||||||
import { forwardToStorage, getBot } from '../utils/telegram';
|
import { forwardToStorage, getBot } from '../utils/telegram';
|
||||||
|
|
||||||
@@ -38,18 +38,23 @@ const handleMultipartUpload = async (req: Request): Promise<Response> => {
|
|||||||
|
|
||||||
const fileBytes = await file.arrayBuffer();
|
const fileBytes = await file.arrayBuffer();
|
||||||
const fileBuffer = Buffer.from(fileBytes);
|
const fileBuffer = Buffer.from(fileBytes);
|
||||||
const mimeType = file.type || extractMimeType({}, req) || 'application/octet-stream';
|
const rawMimeType = file.type || extractMimeType({}, req) || 'application/octet-stream';
|
||||||
const fileType = getFileType(mimeType, fileName);
|
const { fileName: finalFileName, mimeType } = ensureExtension(
|
||||||
|
fileName,
|
||||||
|
fileBuffer,
|
||||||
|
rawMimeType,
|
||||||
|
);
|
||||||
|
const fileType = getFileType(mimeType, finalFileName);
|
||||||
|
|
||||||
if (!checkFileSize(fileBuffer.byteLength, fileType)) {
|
if (!checkFileSize(fileBuffer.byteLength, fileType)) {
|
||||||
return Response.json({ error: `File size exceeds ${fileType} limit` }, { status: 400 });
|
return Response.json({ error: `File size exceeds ${fileType} limit` }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDocument =
|
const isDocument =
|
||||||
fileName.endsWith('.pdf') ||
|
finalFileName.endsWith('.pdf') ||
|
||||||
fileName.endsWith('.txt') ||
|
finalFileName.endsWith('.txt') ||
|
||||||
!['photo', 'video', 'audio', 'voice', 'animation'].includes(fileType);
|
!['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 bot = getBot();
|
||||||
const fileInfo = (await bot.telegram.getFile(result.telegramFileId)) as any;
|
const fileInfo = (await bot.telegram.getFile(result.telegramFileId)) as any;
|
||||||
|
|
||||||
@@ -59,7 +64,7 @@ const handleMultipartUpload = async (req: Request): Promise<Response> => {
|
|||||||
telegramFileUniqueId: result.telegramFileUniqueId,
|
telegramFileUniqueId: result.telegramFileUniqueId,
|
||||||
storageChatId: config.storageChatId,
|
storageChatId: config.storageChatId,
|
||||||
storageMessageId: result.storageMessageId,
|
storageMessageId: result.storageMessageId,
|
||||||
fileName: fileName,
|
fileName: finalFileName,
|
||||||
mimeType: fileInfo.mime_type || mimeType || 'application/octet-stream',
|
mimeType: fileInfo.mime_type || mimeType || 'application/octet-stream',
|
||||||
sizeBytes: fileInfo.file_size || fileBuffer.byteLength,
|
sizeBytes: fileInfo.file_size || fileBuffer.byteLength,
|
||||||
fileType: fileType,
|
fileType: fileType,
|
||||||
@@ -103,22 +108,32 @@ const handleJSONUpload = async (req: Request): Promise<Response> => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileBytes = Buffer.from(file, 'base64');
|
let base64Data = file;
|
||||||
const mimeType = 'application/octet-stream';
|
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 =
|
const fileType =
|
||||||
getFileType(mimeType, fileName) === 'application'
|
getFileType(mimeType, finalFileName) === 'application'
|
||||||
? 'document'
|
? 'document'
|
||||||
: getFileType(mimeType, fileName);
|
: getFileType(mimeType, finalFileName);
|
||||||
|
|
||||||
if (!checkFileSize(fileBytes.byteLength, fileType)) {
|
if (!checkFileSize(fileBytes.byteLength, fileType)) {
|
||||||
return Response.json({ error: `File size exceeds ${fileType} limit` }, { status: 400 });
|
return Response.json({ error: `File size exceeds ${fileType} limit` }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDocument =
|
const isDocument =
|
||||||
fileName.endsWith('.pdf') ||
|
finalFileName.endsWith('.pdf') ||
|
||||||
fileName.endsWith('.txt') ||
|
finalFileName.endsWith('.txt') ||
|
||||||
!['photo', 'video', 'audio', 'voice', 'animation'].includes(fileType);
|
!['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 bot = getBot();
|
||||||
const fileInfo = (await bot.telegram.getFile(result.telegramFileId)) as any;
|
const fileInfo = (await bot.telegram.getFile(result.telegramFileId)) as any;
|
||||||
|
|
||||||
@@ -128,7 +143,7 @@ const handleJSONUpload = async (req: Request): Promise<Response> => {
|
|||||||
telegramFileUniqueId: result.telegramFileUniqueId,
|
telegramFileUniqueId: result.telegramFileUniqueId,
|
||||||
storageChatId: config.storageChatId,
|
storageChatId: config.storageChatId,
|
||||||
storageMessageId: result.storageMessageId,
|
storageMessageId: result.storageMessageId,
|
||||||
fileName: fileName,
|
fileName: finalFileName,
|
||||||
mimeType: fileInfo.mime_type || mimeType || 'application/octet-stream',
|
mimeType: fileInfo.mime_type || mimeType || 'application/octet-stream',
|
||||||
sizeBytes: fileInfo.file_size || fileBytes.byteLength,
|
sizeBytes: fileInfo.file_size || fileBytes.byteLength,
|
||||||
fileType: fileType,
|
fileType: fileType,
|
||||||
|
|||||||
@@ -26,6 +26,48 @@ export const checkFileSize = (sizeBytes: number, fileType: string): boolean => {
|
|||||||
return sizeBytes <= limit;
|
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 => {
|
export const extractFileName = (msg: any, request: any): string => {
|
||||||
if (request?.headers?.['x-file-name']) {
|
if (request?.headers?.['x-file-name']) {
|
||||||
return request.headers['x-file-name'];
|
return request.headers['x-file-name'];
|
||||||
|
|||||||
Reference in New Issue
Block a user