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.
This commit is contained in:
xixu-me committed 2025-07-04 14:16:29 +08:00
1 parent 3bc9d13615
commit 6f4651e283
1 file changed
+151 -5
+151 -5
View File
@@ -12,6 +12,9 @@ export default {
}, },
}; };
// Export helper functions for testing
export { getFileTypeAdvice, handleLargeFileWithChunks };
async function handleDocument(message, env) { async function handleDocument(message, env) {
const fileId = message.document.file_id; const fileId = message.document.file_id;
const fileName = message.document.file_name; 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); await sendMessage(adminChatId, `Error getting file details: ${fileDetails.description}`, botToken);
return; return;
} }
const filePath = fileDetails.result.file_path; const filePath = fileDetails.result.file_path;
const fileSize = fileDetails.result.file_size;
const fileUrl = `https://api.telegram.org/file/bot${botToken}/${filePath}`; const fileUrl = `https://api.telegram.org/file/bot${botToken}/${filePath}`;
// Download file from Telegram // Check if file exceeds Telegram's 20MB limit
const fileResponse = await fetch(fileUrl); if (fileSize > 20 * 1024 * 1024) {
const fileData = await fileResponse.arrayBuffer(); 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 // Authenticate with Google Drive
const jwtToken = await getGoogleAuthToken(googleCredentials); 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 uploadUrl = 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart';
const metadata = { const metadata = {
name: fileName, name: fileName,
@@ -72,7 +109,61 @@ async function uploadToDrive(fileId, fileName, env) {
if (uploadResult.id) { if (uploadResult.id) {
await sendMessage(adminChatId, `File "${fileName}" uploaded to Google Drive successfully!`, botToken); await sendMessage(adminChatId, `File "${fileName}" uploaded to Google Drive successfully!`, botToken);
} else { } 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); 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) { function str2ab(str) {
const buf = new ArrayBuffer(str.length); const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf); const bufView = new Uint8Array(buf);