fix: implement chunked file uploading to bypass proxy size limits
Build and Deploy / build-and-push (push) Successful in 27s
Build and Deploy / build-and-push (push) Successful in 27s
This commit is contained in:
@@ -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}`);
|
||||
|
||||
+56
-13
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user