如何在Django中实现视频分片上传以突破Cloudflare 100M POST限制
Cloudflare 大文件分片上传实现方案
核心思路
将大文件拆分为多个小于100MB的分片,逐个上传至Cloudflare后端(如Workers+R2存储),所有分片上传完成后,再请求后端合并分片为完整文件。
前端实现步骤
文件拆分与标识
- 设定分片大小(建议90MB,预留请求头、表单数据的空间,避免触发100MB限制)
- 生成文件唯一标识(可通过
文件名+文件大小+最后修改时间拼接,确保同一文件的分片能被正确关联) - 计算总分片数,遍历拆分文件,给每个分片标记索引
分片上传
- 为每个分片构建FormData,携带分片数据、索引、总分片数、文件唯一标识
- 逐个或限制并发数上传分片(并发数建议3-5,避免触发Cloudflare请求频率限制)
- 处理上传失败的情况,实现重试逻辑
触发合并请求
- 所有分片上传完成后,发送合并请求至后端,携带文件唯一标识和原文件名
前端示例代码
async function uploadLargeFile(file) { const chunkSize = 90 * 1024 * 1024; // 90MB分片 const totalChunks = Math.ceil(file.size / chunkSize); const fileId = `${file.name}-${file.size}-${file.lastModified}`; // 唯一文件标识 // 可选:先查询已上传分片,实现断点续传 // const uploadedChunks = await fetch(`/check-chunks?fileId=${fileId}`).then(res => res.json()); for (let i = 0; i < totalChunks; i++) { // 跳过已上传分片(断点续传逻辑) // if (uploadedChunks.includes(i)) continue; const start = i * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); const formData = new FormData(); formData.append('chunk', chunk); formData.append('index', i); formData.append('total', totalChunks); formData.append('fileId', fileId); // 上传分片,失败重试3次 let retry = 3; while (retry > 0) { try { await fetch('/upload-chunk', { method: 'POST', body: formData }); break; } catch (err) { retry--; if (retry === 0) throw new Error(`分片${i}上传失败`); await new Promise(resolve => setTimeout(resolve, 1000)); } } } // 发送合并请求 const mergeRes = await fetch('/merge-file', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileId, fileName: file.name }) }); return mergeRes.json(); }
Cloudflare 后端实现(Workers + R2)
分片接收与存储
- 解析上传请求的FormData,提取分片数据、索引、文件唯一标识
- 将分片以
{fileId}_part_{index}的命名规则存储到R2存储桶
分片合并
- 接收合并请求后,根据文件唯一标识列出所有关联的分片
- 按分片索引排序,依次读取分片内容并拼接为完整文件
- 将完整文件存入R2,删除临时分片,返回文件访问地址
Workers示例代码
export default { async fetch(request, env) { const url = new URL(request.url); // 处理分片上传 if (url.pathname === '/upload-chunk') { try { const formData = await request.formData(); const chunk = formData.get('chunk'); const index = formData.get('index'); const fileId = formData.get('fileId'); // 存储分片到R2 await env.FILE_UPLOAD_BUCKET.put(`${fileId}_part_${index}`, chunk); return new Response('分片上传成功', { status: 200 }); } catch (err) { return new Response('分片上传失败', { status: 500 }); } } // 处理合并请求 if (url.pathname === '/merge-file') { try { const { fileId, fileName } = await request.json(); const prefix = `${fileId}_part_`; const chunkList = []; // 获取所有分片并按索引排序 const r2List = await env.FILE_UPLOAD_BUCKET.list({ prefix }); const sortedChunks = r2List.objects.sort((a, b) => { const idxA = parseInt(a.key.split('_').pop()); const idxB = parseInt(b.key.split('_').pop()); return idxA - idxB; }); // 读取所有分片内容 for (const chunkObj of sortedChunks) { const chunk = await env.FILE_UPLOAD_BUCKET.get(chunkObj.key); chunkList.push(await chunk.arrayBuffer()); // 删除临时分片 await env.FILE_UPLOAD_BUCKET.delete(chunkObj.key); } // 合并并存储完整文件 const combinedFile = new Blob(chunkList); await env.FILE_UPLOAD_BUCKET.put(fileName, combinedFile); // 返回文件访问URL(需配置R2自定义域名) return new Response(JSON.stringify({ status: 'success', fileUrl: `https://your-r2-domain.com/${fileName}` }), { status: 200 }); } catch (err) { return new Response('文件合并失败', { status: 500 }); } } // 断点续传:查询已上传分片 if (url.pathname === '/check-chunks') { const fileId = url.searchParams.get('fileId'); const prefix = `${fileId}_part_`; const r2List = await env.FILE_UPLOAD_BUCKET.list({ prefix }); const uploadedIndexes = r2List.objects.map(obj => parseInt(obj.key.split('_').pop())); return new Response(JSON.stringify(uploadedIndexes), { status: 200 }); } return new Response('接口不存在', { status: 404 }); } };
注意事项
- 分片大小:务必小于100MB,建议90MB,避免请求头、表单数据等额外内容导致总请求体积超标
- 并发控制:前端并发上传数不要过高,建议3-5个,防止触发Cloudflare的请求频率限制
- 错误处理:添加重试机制,上传失败后延迟重试;合并失败时清理已上传的临时分片
- 断点续传:通过查询已上传分片接口,跳过已上传的部分,提升大文件上传体验
内容的提问来源于stack exchange,提问作者MrLonely
相关产品推荐
相关产品推荐

