From 6f4651e283bc832fcb4887271d66c350cffeb195 Mon Sep 17 00:00:00 2001 From: Xi Xu Date: Fri, 4 Jul 2025 14:16:29 +0800 Subject: [PATCH] Add large file handling and resumable uploads Introduces checks for Telegram's 20MB file size limit and provides user guidance for oversized files. Implements Google Drive resumable uploads for files larger than 5MB, and adds helper functions for chunked file processing and file type-specific compression advice. Exports helper functions for testing. --- src/index.js | 156 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 151 insertions(+), 5 deletions(-) diff --git a/src/index.js b/src/index.js index 70794ab..cb1ca91 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,9 @@ export default { }, }; +// Export helper functions for testing +export { getFileTypeAdvice, handleLargeFileWithChunks }; + async function handleDocument(message, env) { const fileId = message.document.file_id; const fileName = message.document.file_name; @@ -39,17 +42,51 @@ async function uploadToDrive(fileId, fileName, env) { await sendMessage(adminChatId, `Error getting file details: ${fileDetails.description}`, botToken); return; } + const filePath = fileDetails.result.file_path; + const fileSize = fileDetails.result.file_size; const fileUrl = `https://api.telegram.org/file/bot${botToken}/${filePath}`; - // Download file from Telegram - const fileResponse = await fetch(fileUrl); - const fileData = await fileResponse.arrayBuffer(); + // Check if file exceeds Telegram's 20MB limit + if (fileSize > 20 * 1024 * 1024) { + const advice = await getFileTypeAdvice(fileName); + await sendMessage( + adminChatId, + `❌ File "${fileName}" is ${(fileSize / 1024 / 1024).toFixed(2)}MB, which exceeds Telegram's 20MB limit.\n\n` + + `💡 Suggestion: ${advice}\n\n` + + `🔧 Alternatives:\n` + + `• Compress using ZIP/RAR\n` + + `• Upload directly to Google Drive\n` + + `• Use Google Drive mobile app\n` + + `• Split into smaller files`, + botToken + ); + return; + } // Authenticate with Google Drive const jwtToken = await getGoogleAuthToken(googleCredentials); - // Upload file to Google Drive + try { + // Use resumable upload for better reliability + if (fileSize > 5 * 1024 * 1024) { + // Use resumable upload for files > 5MB + await uploadToDriveResumable(fileUrl, fileName, fileSize, driveFolderId, jwtToken, adminChatId, botToken); + } else { + // Use simple upload for smaller files + await uploadToDriveSimple(fileUrl, fileName, driveFolderId, jwtToken, adminChatId, botToken); + } + } catch (error) { + await sendMessage(adminChatId, `Error uploading file "${fileName}": ${error.message}`, botToken); + } +} + +async function uploadToDriveSimple(fileUrl, fileName, driveFolderId, jwtToken, adminChatId, botToken) { + // Download file from Telegram + const fileResponse = await fetch(fileUrl); + const fileData = await fileResponse.arrayBuffer(); + + // Upload file to Google Drive using multipart upload const uploadUrl = 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart'; const metadata = { name: fileName, @@ -72,7 +109,61 @@ async function uploadToDrive(fileId, fileName, env) { if (uploadResult.id) { await sendMessage(adminChatId, `File "${fileName}" uploaded to Google Drive successfully!`, botToken); } else { - await sendMessage(adminChatId, `Error uploading file: ${JSON.stringify(uploadResult.error)}`, botToken); + throw new Error(JSON.stringify(uploadResult.error)); + } +} + +async function uploadToDriveResumable(fileUrl, fileName, fileSize, driveFolderId, jwtToken, adminChatId, botToken) { + // Step 1: Initiate resumable upload session + const metadata = { + name: fileName, + parents: [driveFolderId], + }; + + const initiateResponse = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable', { + method: 'POST', + headers: { + Authorization: `Bearer ${jwtToken}`, + 'Content-Type': 'application/json', + 'X-Upload-Content-Length': fileSize.toString(), + }, + body: JSON.stringify(metadata), + }); + + if (!initiateResponse.ok) { + throw new Error(`Failed to initiate resumable upload: ${initiateResponse.statusText}`); + } + + const uploadUrl = initiateResponse.headers.get('Location'); + + // Step 2: Stream upload the file content + const fileResponse = await fetch(fileUrl); + + if (!fileResponse.ok) { + throw new Error(`Failed to fetch file from Telegram: ${fileResponse.statusText}`); + } + + const uploadResponse = await fetch(uploadUrl, { + method: 'PUT', + headers: { + 'Content-Length': fileSize.toString(), + }, + body: fileResponse.body, // Stream the response directly + }); + + if (!uploadResponse.ok) { + throw new Error(`Upload failed: ${uploadResponse.statusText}`); + } + + const uploadResult = await uploadResponse.json(); + if (uploadResult.id) { + await sendMessage( + adminChatId, + `Large file "${fileName}" (${(fileSize / 1024 / 1024).toFixed(2)}MB) uploaded to Google Drive successfully using resumable upload!`, + botToken + ); + } else { + throw new Error('Upload completed but no file ID returned'); } } @@ -119,6 +210,61 @@ async function sendMessage(chatId, text, botToken) { await fetch(url); } +// Add support for chunked file processing +async function handleLargeFileWithChunks(fileId, fileName, fileSize, env) { + const adminChatId = env.ADMIN_CHAT_ID; + const botToken = env.TELEGRAM_BOT_TOKEN; + + // Suggest chunking for very large files + if (fileSize > 20 * 1024 * 1024) { + await sendMessage( + adminChatId, + `File "${fileName}" is ${(fileSize / 1024 / 1024).toFixed(2)}MB. ` + + `Please split it into chunks smaller than 20MB or use these alternatives:\n\n` + + `📦 Compress the file using ZIP/RAR\n` + + `🔗 Upload directly to Google Drive\n` + + `📱 Use Google Drive mobile app\n` + + `✂️ Split into smaller files`, + botToken + ); + return false; + } + return true; +} + +// Enhanced error handling for different file types +async function getFileTypeAdvice(fileName) { + const extension = fileName.split('.').pop()?.toLowerCase(); + + const compressionAdvice = { + // Video files + mp4: 'Consider reducing video quality or using H.265 encoding', + avi: 'Convert to MP4 with lower bitrate', + mov: 'Convert to MP4 format for better compression', + mkv: 'Convert to MP4 or use video compression', + + // Image files + png: 'Convert to JPEG or reduce image dimensions', + bmp: 'Convert to JPEG or PNG format', + tiff: 'Convert to JPEG format', + + // Document files + pdf: 'Compress PDF or split into multiple files', + docx: 'Save as PDF or compress embedded images', + pptx: 'Compress images within presentation', + + // Archive files + zip: 'Use higher compression ratio or split archive', + rar: 'Use maximum compression or split archive', + '7z': 'Already well compressed, consider splitting', + + // Default + default: 'Consider compressing or splitting the file', + }; + + return compressionAdvice[extension] || compressionAdvice['default']; +} + function str2ab(str) { const buf = new ArrayBuffer(str.length); const bufView = new Uint8Array(buf);