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:
1 parent
3bc9d13615
commit
6f4651e283
1 file changed
+151
-5
+151
-5
@@ -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);
|
||||||
|
|||||||
Reference in new issue
Block a user