From 3a3ccceb9683a712c9377681eb09e5799786dbca Mon Sep 17 00:00:00 2001 From: AI Bot Date: Wed, 2 Sep 2026 07:39:14 +0530 Subject: [PATCH] fix: implement chunked file uploading to bypass proxy size limits --- backend/server.js | 55 ++++++++++++++++++++++++++++++++ src/lib/api.ts | 69 +++++++++++++++++++++++++++++++++-------- src/lib/storageUtils.ts | 8 +++-- 3 files changed, 117 insertions(+), 15 deletions(-) diff --git a/backend/server.js b/backend/server.js index e6effde..3b141f2 100644 --- a/backend/server.js +++ b/backend/server.js @@ -216,6 +216,61 @@ app.post('/api/upload', authenticate, upload.single('file'), (req, res) => { res.json({ url }); }); +// 10b. Chunked Upload - Receive chunk +app.post('/api/upload/chunk', authenticate, upload.single('file'), (req, res) => { + const { uploadId, chunkIndex, totalChunks } = req.body; + if (!req.file || !uploadId || !chunkIndex || !totalChunks) { + return res.status(400).json({ error: 'Missing chunk data' }); + } + + const tempDir = path.join(uploadsDir, 'temp', uploadId); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + + const chunkPath = path.join(tempDir, chunkIndex); + fs.renameSync(req.file.path, chunkPath); + + res.json({ success: true }); +}); + +// 10c. Chunked Upload - Complete +app.post('/api/upload/complete', authenticate, (req, res) => { + const { uploadId, fileName, totalChunks } = req.body; + if (!uploadId || !fileName || !totalChunks) { + return res.status(400).json({ error: 'Missing completion data' }); + } + + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + const finalFileName = uniqueSuffix + '-' + fileName; + const finalPath = path.join(uploadsDir, finalFileName); + const tempDir = path.join(uploadsDir, 'temp', uploadId); + + try { + const writeStream = fs.createWriteStream(finalPath); + for (let i = 0; i < totalChunks; i++) { + const chunkPath = path.join(tempDir, i.toString()); + if (!fs.existsSync(chunkPath)) { + throw new Error(`Chunk ${i} is missing`); + } + const data = fs.readFileSync(chunkPath); + writeStream.write(data); + } + writeStream.end(); + + // Clean up temp dir + fs.rmSync(tempDir, { recursive: true, force: true }); + + const url = `/uploads/${finalFileName}`; + res.json({ url }); + } catch (error) { + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + res.status(500).json({ error: error.message }); + } +}); + const PORT = process.env.PORT || 3005; app.listen(PORT, () => { console.log(`Backend server running on port ${PORT}`); diff --git a/src/lib/api.ts b/src/lib/api.ts index f65109a..295b402 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -112,19 +112,62 @@ export const api = { // Storage uploadAsset: async (fileBlob) => { - const formData = new FormData(); - formData.append('file', fileBlob); - + const chunkSize = 10 * 1024 * 1024; // 10MB chunks const token = localStorage.getItem('auth_token'); - const headers = token ? { Authorization: `Bearer ${token}` } : {}; - - const res = await fetch(`${API_URL}/upload`, { - method: 'POST', - headers, - body: formData - }); - const data = await res.json(); - if (!res.ok) throw new Error(data.error); - return data.url; + const authHeaders = token ? { Authorization: `Bearer ${token}` } : {}; + + if (fileBlob.size <= chunkSize) { + // Standard upload for small files + const formData = new FormData(); + formData.append('file', fileBlob); + + const res = await fetch(`${API_URL}/upload`, { + method: 'POST', + headers: authHeaders, + body: formData + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error); + return data.url; + } else { + // Chunked upload for large files + const uploadId = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + const totalChunks = Math.ceil(fileBlob.size / chunkSize); + + for (let i = 0; i < totalChunks; i++) { + const chunk = fileBlob.slice(i * chunkSize, (i + 1) * chunkSize); + const formData = new FormData(); + formData.append('file', chunk); + formData.append('uploadId', uploadId); + formData.append('chunkIndex', i.toString()); + formData.append('totalChunks', totalChunks.toString()); + + const res = await fetch(`${API_URL}/upload/chunk`, { + method: 'POST', + headers: authHeaders, + body: formData + }); + if (!res.ok) throw new Error(`Failed to upload chunk ${i}`); + } + + const completeHeaders = { + 'Content-Type': 'application/json', + ...authHeaders + }; + + const res = await fetch(`${API_URL}/upload/complete`, { + method: 'POST', + headers: completeHeaders, + body: JSON.stringify({ + uploadId, + fileName: fileBlob.name || 'uploaded_file', + totalChunks + }) + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.error); + return data.url; + } } }; diff --git a/src/lib/storageUtils.ts b/src/lib/storageUtils.ts index 1be4b0b..e03219d 100644 --- a/src/lib/storageUtils.ts +++ b/src/lib/storageUtils.ts @@ -5,8 +5,12 @@ export const uploadAssetToStorage = async (file: Blob | File, bucket: string, pa const url = await api.uploadAsset(file); return url; } catch (err) { - console.warn("Storage upload exception, falling back to data URL.", err); - return await fileToDataUrl(file); + console.warn("Storage upload exception.", err); + if (file.size < 5 * 1024 * 1024) { + console.warn("Falling back to data URL for small file."); + return await fileToDataUrl(file); + } + throw err; } };