feat: add file_hash column and related index to files table; refactor Telegram API utilities for improved file handling
This commit is contained in:
@@ -19,5 +19,6 @@ ALTER TABLE files ADD COLUMN IF NOT EXISTS file_hash VARCHAR;
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_files_public_id ON files(public_id);
|
CREATE INDEX IF NOT EXISTS idx_files_public_id ON files(public_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_files_telegram_file_id ON files(telegram_file_id);
|
CREATE INDEX IF NOT EXISTS idx_files_telegram_file_id ON files(telegram_file_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_files_file_hash ON files(file_hash);
|
||||||
CREATE INDEX IF NOT EXISTS idx_files_uploader_id ON files(uploader_id);
|
CREATE INDEX IF NOT EXISTS idx_files_uploader_id ON files(uploader_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_files_created_at ON files(created_at DESC);
|
CREATE INDEX IF NOT EXISTS idx_files_created_at ON files(created_at DESC);
|
||||||
+1
-1
@@ -2,6 +2,7 @@ import { findFileByPublicId } from '../db/files';
|
|||||||
import { formatCreatedAt, getErrorMessage } from '../utils/file';
|
import { formatCreatedAt, getErrorMessage } from '../utils/file';
|
||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
import { checkRateLimit } from '../utils/rateLimit';
|
import { checkRateLimit } from '../utils/rateLimit';
|
||||||
|
import { getBot } from '../utils/telegram';
|
||||||
|
|
||||||
type RequestWithParams = Request & {
|
type RequestWithParams = Request & {
|
||||||
params?: {
|
params?: {
|
||||||
@@ -25,7 +26,6 @@ export const handleFileRedirect = async (req: RequestWithParams): Promise<Respon
|
|||||||
return Response.json({ error: 'File not found' }, { status: 404 });
|
return Response.json({ error: 'File not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getBot } = await import('../utils/telegram');
|
|
||||||
const bot = getBot();
|
const bot = getBot();
|
||||||
const fileInfo = await bot.telegram.getFile(file.telegramFileId);
|
const fileInfo = await bot.telegram.getFile(file.telegramFileId);
|
||||||
|
|
||||||
|
|||||||
+3
-10
@@ -14,18 +14,13 @@ import {
|
|||||||
getFileType,
|
getFileType,
|
||||||
} from '../utils/file';
|
} from '../utils/file';
|
||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
import { forwardToStorage, getBot } from '../utils/telegram';
|
import { forwardToStorage } from '../utils/telegram';
|
||||||
|
|
||||||
type UploadedFile = NewFile & {
|
type UploadedFile = NewFile & {
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TelegramFileLookup = {
|
|
||||||
mime_type?: string;
|
|
||||||
file_size?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface JsonUploadPayload {
|
interface JsonUploadPayload {
|
||||||
file?: unknown;
|
file?: unknown;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
@@ -57,8 +52,6 @@ const performUpload = async (
|
|||||||
await Bun.write(tempPath, fileBuffer);
|
await Bun.write(tempPath, fileBuffer);
|
||||||
const fileStream = createReadStream(tempPath);
|
const fileStream = createReadStream(tempPath);
|
||||||
const result = await forwardToStorage(fileStream, fileName, getFileType(mimeType, fileName));
|
const result = await forwardToStorage(fileStream, fileName, getFileType(mimeType, fileName));
|
||||||
const bot = getBot();
|
|
||||||
const fileInfo = (await bot.telegram.getFile(result.telegramFileId)) as TelegramFileLookup;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
publicId: nanoid(),
|
publicId: nanoid(),
|
||||||
@@ -67,8 +60,8 @@ const performUpload = async (
|
|||||||
storageChatId: config.storageChatId,
|
storageChatId: config.storageChatId,
|
||||||
storageMessageId: result.storageMessageId,
|
storageMessageId: result.storageMessageId,
|
||||||
fileName,
|
fileName,
|
||||||
mimeType: fileInfo.mime_type || mimeType || 'application/octet-stream',
|
mimeType: mimeType || 'application/octet-stream',
|
||||||
sizeBytes: fileInfo.file_size || fileBuffer.byteLength,
|
sizeBytes: fileBuffer.byteLength,
|
||||||
fileType: getFileType(mimeType, fileName),
|
fileType: getFileType(mimeType, fileName),
|
||||||
uploaderId: 0,
|
uploaderId: 0,
|
||||||
fileHash: computeHash(fileBuffer),
|
fileHash: computeHash(fileBuffer),
|
||||||
|
|||||||
+10
-38
@@ -6,7 +6,6 @@ import { enqueueUpload } from './telegramQueue';
|
|||||||
const botTokens = Array.from(new Set([config.botToken, ...config.additionalBotTokens]));
|
const botTokens = Array.from(new Set([config.botToken, ...config.additionalBotTokens]));
|
||||||
|
|
||||||
const bots = botTokens.map((token) => new Telegraf(token));
|
const bots = botTokens.map((token) => new Telegraf(token));
|
||||||
const TELEGRAM_API_URL = `https://api.telegram.org/bot${config.botToken}/`;
|
|
||||||
|
|
||||||
let currentBotIndex = 0;
|
let currentBotIndex = 0;
|
||||||
|
|
||||||
@@ -69,20 +68,6 @@ interface TelegramFileInfo {
|
|||||||
file_path: string;
|
file_path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TelegramGetFileResponse {
|
|
||||||
ok: boolean;
|
|
||||||
description?: string;
|
|
||||||
result: {
|
|
||||||
file_id: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TelegramGetInfoResponse {
|
|
||||||
ok: boolean;
|
|
||||||
description?: string;
|
|
||||||
result: TelegramFileInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UploadedTelegramFile {
|
interface UploadedTelegramFile {
|
||||||
file_id?: string;
|
file_id?: string;
|
||||||
file_unique_id?: string;
|
file_unique_id?: string;
|
||||||
@@ -256,32 +241,17 @@ export const forwardMediaGroupToStorage = async (
|
|||||||
|
|
||||||
export const getFileInfo = async (
|
export const getFileInfo = async (
|
||||||
telegramFileId: string,
|
telegramFileId: string,
|
||||||
telegramFileUniqueId: string,
|
|
||||||
): Promise<TelegramFileInfo> => {
|
): Promise<TelegramFileInfo> => {
|
||||||
try {
|
try {
|
||||||
const result = await fetch(`${TELEGRAM_API_URL}getFile`);
|
const result = await executeWithBotRetry((activeBot) =>
|
||||||
const data = (await result.json()) as TelegramGetFileResponse;
|
activeBot.telegram.getFile(telegramFileId),
|
||||||
|
);
|
||||||
if (!data.ok) {
|
|
||||||
throw new Error(data.description || 'Telegram API error');
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileId = data.result.file_id === telegramFileId ? telegramFileId : telegramFileUniqueId;
|
|
||||||
const fileResult = await fetch(`${TELEGRAM_API_URL}getInfo`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ file_id: fileId }),
|
|
||||||
});
|
|
||||||
const fileInfo = (await fileResult.json()) as TelegramGetInfoResponse;
|
|
||||||
|
|
||||||
if (!fileInfo.ok) {
|
|
||||||
throw new Error(fileInfo.description || 'Telegram info error');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const fileData = result as unknown as TelegramFileInfo;
|
||||||
return {
|
return {
|
||||||
file_size: fileInfo.result.file_size,
|
file_size: fileData.file_size || 0,
|
||||||
mime_type: fileInfo.result.mime_type,
|
mime_type: fileData.mime_type || 'application/octet-stream',
|
||||||
file_path: fileInfo.result.file_path,
|
file_path: fileData.file_path || '',
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Failed to get file info', {
|
logger.error('Failed to get file info', {
|
||||||
@@ -291,4 +261,6 @@ export const getFileInfo = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBot = (): Telegraf => bots[0];
|
export const getBot = (): Telegraf => bots[currentBotIndex];
|
||||||
|
|
||||||
|
export const getCurrentBotIndex = (): number => currentBotIndex;
|
||||||
|
|||||||
+9
-46
@@ -46,6 +46,14 @@ mock.module('telegraf', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
getFile: mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
file_id: 'some_file_id',
|
||||||
|
file_size: 98765,
|
||||||
|
mime_type: 'image/jpeg',
|
||||||
|
file_path: 'photos/file_0.jpg',
|
||||||
|
}),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -169,34 +177,7 @@ describe('Telegram API Utilities', () => {
|
|||||||
|
|
||||||
describe('getFileInfo', () => {
|
describe('getFileInfo', () => {
|
||||||
it('should fetch file details successfully', async () => {
|
it('should fetch file details successfully', async () => {
|
||||||
global.fetch = mock((url, _init) => {
|
const result = await getFileInfo('some_file_id');
|
||||||
if (url.endsWith('getFile')) {
|
|
||||||
return Promise.resolve(
|
|
||||||
new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
ok: true,
|
|
||||||
result: { file_id: 'some_file_id' },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (url.endsWith('getInfo')) {
|
|
||||||
return Promise.resolve(
|
|
||||||
new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
ok: true,
|
|
||||||
result: {
|
|
||||||
file_size: 98765,
|
|
||||||
mime_type: 'image/jpeg',
|
|
||||||
file_path: 'photos/file_0.jpg',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Promise.reject(new Error('Unknown URL'));
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await getFileInfo('some_file_id', 'some_unique_id');
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
file_size: 98765,
|
file_size: 98765,
|
||||||
@@ -204,23 +185,5 @@ describe('Telegram API Utilities', () => {
|
|||||||
file_path: 'photos/file_0.jpg',
|
file_path: 'photos/file_0.jpg',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle error when getFile fails', async () => {
|
|
||||||
global.fetch = mock(() =>
|
|
||||||
Promise.resolve(
|
|
||||||
new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
ok: false,
|
|
||||||
description: 'Bad Request: file_id invalid',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(getFileInfo('invalid_file_id', 'invalid_unique_id')).rejects.toThrow(
|
|
||||||
'Bad Request: file_id invalid',
|
|
||||||
);
|
|
||||||
expect(errorSpy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user