Refactor upload logic and remove upload tests
Refactored file upload logic to pass file size directly and streamline handling of large files with a new resumableUpload function. Removed the upload.test.js file.
This commit is contained in:
1 parent
cb5b329a5b
commit
eac9e037e2
2 files changed
+40
-213
No files matched your search
+40
-141
@@ -15,17 +15,19 @@ export default {
|
|||||||
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;
|
||||||
await uploadToDrive(fileId, fileName, env);
|
const fileSize = message.document.file_size;
|
||||||
|
await uploadToDrive(fileId, fileName, fileSize, env);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePhoto(message, env) {
|
async function handlePhoto(message, env) {
|
||||||
const photo = message.photo[message.photo.length - 1]; // Get the highest resolution photo
|
const photo = message.photo[message.photo.length - 1]; // Get the highest resolution photo
|
||||||
const fileId = photo.file_id;
|
const fileId = photo.file_id;
|
||||||
const fileName = `${fileId}.jpg`;
|
const fileName = `${fileId}.jpg`;
|
||||||
await uploadToDrive(fileId, fileName, env);
|
const fileSize = photo.file_size;
|
||||||
|
await uploadToDrive(fileId, fileName, fileSize, env);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadToDrive(fileId, fileName, env) {
|
async function uploadToDrive(fileId, fileName, fileSize, env) {
|
||||||
const botToken = env.TELEGRAM_BOT_TOKEN;
|
const botToken = env.TELEGRAM_BOT_TOKEN;
|
||||||
const driveFolderId = env.GOOGLE_DRIVE_FOLDER_ID;
|
const driveFolderId = env.GOOGLE_DRIVE_FOLDER_ID;
|
||||||
const adminChatId = env.ADMIN_CHAT_ID;
|
const adminChatId = env.ADMIN_CHAT_ID;
|
||||||
@@ -42,27 +44,20 @@ async function uploadToDrive(fileId, fileName, env) {
|
|||||||
const filePath = fileDetails.result.file_path;
|
const filePath = fileDetails.result.file_path;
|
||||||
const fileUrl = `https://api.telegram.org/file/bot${botToken}/${filePath}`;
|
const fileUrl = `https://api.telegram.org/file/bot${botToken}/${filePath}`;
|
||||||
|
|
||||||
// Download file from Telegram
|
|
||||||
const fileResponse = await fetch(fileUrl);
|
|
||||||
const fileData = await fileResponse.arrayBuffer();
|
|
||||||
const fileSize = fileData.byteLength;
|
|
||||||
|
|
||||||
// Authenticate with Google Drive
|
// Authenticate with Google Drive
|
||||||
const jwtToken = await getGoogleAuthToken(googleCredentials);
|
const jwtToken = await getGoogleAuthToken(googleCredentials);
|
||||||
|
|
||||||
// Check if file is larger than 20MB (20 * 1024 * 1024 = 20971520 bytes)
|
// For files larger than 20MB, use resumable upload
|
||||||
const MAX_SIMPLE_UPLOAD_SIZE = 20971520;
|
if (fileSize > 20 * 1024 * 1024) {
|
||||||
|
await resumableUpload(fileUrl, fileName, driveFolderId, jwtToken, adminChatId, botToken);
|
||||||
if (fileSize > MAX_SIMPLE_UPLOAD_SIZE) {
|
return;
|
||||||
await sendMessage(adminChatId, `File "${fileName}" is ${Math.round(fileSize / 1024 / 1024)}MB, using resumable upload...`, botToken);
|
|
||||||
await uploadLargeFile(fileData, fileName, driveFolderId, jwtToken, adminChatId, botToken);
|
|
||||||
} else {
|
|
||||||
await uploadSmallFile(fileData, fileName, driveFolderId, jwtToken, adminChatId, botToken);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function uploadSmallFile(fileData, fileName, driveFolderId, jwtToken, adminChatId, botToken) {
|
// Download file from Telegram
|
||||||
// Upload file to Google Drive using multipart upload
|
const fileResponse = await fetch(fileUrl);
|
||||||
|
const fileData = await fileResponse.arrayBuffer();
|
||||||
|
|
||||||
|
// Upload file to Google Drive using multipart upload for smaller files
|
||||||
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,
|
||||||
@@ -89,160 +84,64 @@ async function uploadSmallFile(fileData, fileName, driveFolderId, jwtToken, admi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadLargeFile(fileData, fileName, driveFolderId, jwtToken, adminChatId, botToken) {
|
async function resumableUpload(fileUrl, fileName, driveFolderId, jwtToken, adminChatId, botToken) {
|
||||||
const metadata = {
|
const metadata = {
|
||||||
name: fileName,
|
name: fileName,
|
||||||
parents: [driveFolderId],
|
parents: [driveFolderId],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Step 1: Initiate resumable upload
|
const initiationResponse = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable', {
|
||||||
const initiateUrl = 'https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable';
|
|
||||||
const initiateResponse = await fetch(initiateUrl, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${jwtToken}`,
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-Upload-Content-Type': 'application/octet-stream',
|
|
||||||
'X-Upload-Content-Length': fileData.byteLength.toString(),
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify(metadata),
|
body: JSON.stringify(metadata),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!initiateResponse.ok) {
|
if (!initiationResponse.ok) {
|
||||||
const error = await initiateResponse.text();
|
await sendMessage(adminChatId, `Error initiating resumable upload: ${await initiationResponse.text()}`, botToken);
|
||||||
await sendMessage(adminChatId, `Error initiating resumable upload: ${error}`, botToken);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resumableUploadUrl = initiateResponse.headers.get('Location');
|
const location = initiationResponse.headers.get('Location');
|
||||||
if (!resumableUploadUrl) {
|
|
||||||
await sendMessage(adminChatId, `Error: No resumable upload URL received`, botToken);
|
const fileResponse = await fetch(fileUrl);
|
||||||
return;
|
const reader = fileResponse.body.getReader();
|
||||||
|
let uploaded = 0;
|
||||||
|
const chunkSize = 256 * 1024 * 10; // 2.56MB chunks
|
||||||
|
const totalSize = Number(fileResponse.headers.get('Content-Length'));
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Check if any bytes have already been uploaded
|
const chunk = value;
|
||||||
let uploadedBytes = await getUploadStatus(resumableUploadUrl, jwtToken);
|
const uploadResponse = await fetch(location, {
|
||||||
if (uploadedBytes > 0) {
|
|
||||||
await sendMessage(adminChatId, `Resuming upload from ${Math.round(uploadedBytes / 1024 / 1024)}MB`, botToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Upload file in chunks
|
|
||||||
const CHUNK_SIZE = 10 * 1024 * 1024; // 10MB chunks
|
|
||||||
const MAX_RETRIES = 3;
|
|
||||||
const totalSize = fileData.byteLength;
|
|
||||||
|
|
||||||
while (uploadedBytes < totalSize) {
|
|
||||||
const start = uploadedBytes;
|
|
||||||
const end = Math.min(start + CHUNK_SIZE, totalSize);
|
|
||||||
const chunk = fileData.slice(start, end);
|
|
||||||
const chunkSize = end - start;
|
|
||||||
|
|
||||||
let retryCount = 0;
|
|
||||||
let success = false;
|
|
||||||
|
|
||||||
while (retryCount < MAX_RETRIES && !success) {
|
|
||||||
try {
|
|
||||||
const chunkResponse = await fetch(resumableUploadUrl, {
|
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${jwtToken}`,
|
'Content-Range': `bytes ${uploaded}-${uploaded + chunk.length - 1}/${totalSize}`,
|
||||||
'Content-Range': `bytes ${start}-${end - 1}/${totalSize}`,
|
|
||||||
'Content-Length': chunkSize.toString(),
|
|
||||||
},
|
},
|
||||||
body: chunk,
|
body: chunk,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (chunkResponse.status === 308) {
|
if (uploadResponse.status === 308) {
|
||||||
// Chunk uploaded successfully, continue with next chunk
|
// Resume Incomplete
|
||||||
const rangeHeader = chunkResponse.headers.get('Range');
|
uploaded += chunk.length;
|
||||||
if (rangeHeader) {
|
} else if (uploadResponse.ok) {
|
||||||
const rangeMatch = rangeHeader.match(/bytes=0-(\d+)/);
|
const uploadResult = await uploadResponse.json();
|
||||||
if (rangeMatch) {
|
|
||||||
uploadedBytes = parseInt(rangeMatch[1]) + 1;
|
|
||||||
} else {
|
|
||||||
uploadedBytes = end;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
uploadedBytes = end;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send progress update (only every 20% to avoid spam)
|
|
||||||
const progress = Math.round((uploadedBytes / totalSize) * 100);
|
|
||||||
if (progress % 20 === 0 || uploadedBytes === end) {
|
|
||||||
await sendMessage(
|
|
||||||
adminChatId,
|
|
||||||
`Upload progress: ${progress}% (${Math.round(uploadedBytes / 1024 / 1024)}MB / ${Math.round(totalSize / 1024 / 1024)}MB)`,
|
|
||||||
botToken
|
|
||||||
);
|
|
||||||
}
|
|
||||||
success = true;
|
|
||||||
} else if (chunkResponse.status === 200 || chunkResponse.status === 201) {
|
|
||||||
// Upload completed
|
|
||||||
const uploadResult = await chunkResponse.json();
|
|
||||||
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 completing upload: ${JSON.stringify(uploadResult)}`, botToken);
|
await sendMessage(adminChatId, `Error completing resumable upload: ${JSON.stringify(uploadResult.error)}`, botToken);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
} else if (chunkResponse.status === 404) {
|
|
||||||
// Session expired, need to restart
|
|
||||||
await sendMessage(adminChatId, `Upload session expired. Please try uploading the file again.`, botToken);
|
|
||||||
return;
|
|
||||||
} else if (chunkResponse.status >= 500) {
|
|
||||||
// Server error, retry
|
|
||||||
retryCount++;
|
|
||||||
if (retryCount < MAX_RETRIES) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000 * retryCount)); // Exponential backoff
|
|
||||||
await sendMessage(adminChatId, `Server error, retrying chunk upload... (${retryCount}/${MAX_RETRIES})`, botToken);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Other error, don't retry
|
await sendMessage(adminChatId, `Error during resumable upload chunk: ${await uploadResponse.text()}`, botToken);
|
||||||
const error = await chunkResponse.text();
|
|
||||||
await sendMessage(adminChatId, `Error uploading chunk: ${error}`, botToken);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
retryCount++;
|
|
||||||
if (retryCount < MAX_RETRIES) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000 * retryCount)); // Exponential backoff
|
|
||||||
await sendMessage(adminChatId, `Network error, retrying chunk upload... (${retryCount}/${MAX_RETRIES})`, botToken);
|
|
||||||
} else {
|
|
||||||
await sendMessage(adminChatId, `Network error uploading chunk after ${MAX_RETRIES} retries: ${error.message}`, botToken);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
await sendMessage(adminChatId, `Failed to upload chunk after ${MAX_RETRIES} retries`, botToken);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getUploadStatus(resumableUploadUrl, jwtToken) {
|
|
||||||
try {
|
|
||||||
const statusResponse = await fetch(resumableUploadUrl, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${jwtToken}`,
|
|
||||||
'Content-Range': 'bytes */*',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (statusResponse.status === 308) {
|
|
||||||
const rangeHeader = statusResponse.headers.get('Range');
|
|
||||||
if (rangeHeader) {
|
|
||||||
const rangeMatch = rangeHeader.match(/bytes=0-(\d+)/);
|
|
||||||
if (rangeMatch) {
|
|
||||||
return parseInt(rangeMatch[1]) + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
} catch (error) {
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
|
|
||||||
describe('File Upload Tests', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// Mock global fetch
|
|
||||||
global.fetch = vi.fn();
|
|
||||||
global.FormData = vi.fn(() => ({
|
|
||||||
append: vi.fn(),
|
|
||||||
}));
|
|
||||||
global.Blob = vi.fn();
|
|
||||||
global.crypto = {
|
|
||||||
subtle: {
|
|
||||||
importKey: vi.fn(),
|
|
||||||
sign: vi.fn(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use regular upload for files under 20MB', async () => {
|
|
||||||
// Mock a 10MB file
|
|
||||||
const mockFileData = new ArrayBuffer(10 * 1024 * 1024);
|
|
||||||
|
|
||||||
// Mock fetch responses
|
|
||||||
global.fetch
|
|
||||||
.mockResolvedValueOnce({
|
|
||||||
json: () => Promise.resolve({ ok: true, result: { file_path: 'test/file.jpg' } }),
|
|
||||||
})
|
|
||||||
.mockResolvedValueOnce({
|
|
||||||
arrayBuffer: () => Promise.resolve(mockFileData),
|
|
||||||
})
|
|
||||||
.mockResolvedValueOnce({
|
|
||||||
json: () => Promise.resolve({ access_token: 'test_token' }),
|
|
||||||
})
|
|
||||||
.mockResolvedValueOnce({
|
|
||||||
json: () => Promise.resolve({ id: 'test_file_id' }),
|
|
||||||
});
|
|
||||||
|
|
||||||
// This would test the uploadToDrive function
|
|
||||||
// Since we can't easily import the function due to the export default structure,
|
|
||||||
// this serves as a placeholder for testing structure
|
|
||||||
expect(mockFileData.byteLength).toBeLessThan(20 * 1024 * 1024);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use resumable upload for files over 20MB', async () => {
|
|
||||||
// Mock a 25MB file
|
|
||||||
const mockFileData = new ArrayBuffer(25 * 1024 * 1024);
|
|
||||||
|
|
||||||
expect(mockFileData.byteLength).toBeGreaterThan(20 * 1024 * 1024);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle chunk sizes correctly', () => {
|
|
||||||
const CHUNK_SIZE = 10 * 1024 * 1024;
|
|
||||||
const totalSize = 25 * 1024 * 1024;
|
|
||||||
|
|
||||||
// Calculate expected number of chunks
|
|
||||||
const expectedChunks = Math.ceil(totalSize / CHUNK_SIZE);
|
|
||||||
expect(expectedChunks).toBe(3);
|
|
||||||
|
|
||||||
// Test chunk boundaries
|
|
||||||
const chunks = [];
|
|
||||||
for (let i = 0; i < totalSize; i += CHUNK_SIZE) {
|
|
||||||
const start = i;
|
|
||||||
const end = Math.min(i + CHUNK_SIZE, totalSize);
|
|
||||||
chunks.push({ start, end, size: end - start });
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(chunks).toHaveLength(3);
|
|
||||||
expect(chunks[0].size).toBe(CHUNK_SIZE);
|
|
||||||
expect(chunks[1].size).toBe(CHUNK_SIZE);
|
|
||||||
expect(chunks[2].size).toBe(totalSize - 2 * CHUNK_SIZE);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in new issue
Block a user