You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Django中实现视频分片上传以突破Cloudflare 100M POST限制

Cloudflare 大文件分片上传实现方案

核心思路

将大文件拆分为多个小于100MB的分片,逐个上传至Cloudflare后端(如Workers+R2存储),所有分片上传完成后,再请求后端合并分片为完整文件。


前端实现步骤

  1. 文件拆分与标识

    • 设定分片大小(建议90MB,预留请求头、表单数据的空间,避免触发100MB限制)
    • 生成文件唯一标识(可通过文件名+文件大小+最后修改时间拼接,确保同一文件的分片能被正确关联)
    • 计算总分片数,遍历拆分文件,给每个分片标记索引
  2. 分片上传

    • 为每个分片构建FormData,携带分片数据、索引、总分片数、文件唯一标识
    • 逐个或限制并发数上传分片(并发数建议3-5,避免触发Cloudflare请求频率限制)
    • 处理上传失败的情况,实现重试逻辑
  3. 触发合并请求

    • 所有分片上传完成后,发送合并请求至后端,携带文件唯一标识和原文件名

前端示例代码

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)

  1. 分片接收与存储

    • 解析上传请求的FormData,提取分片数据、索引、文件唯一标识
    • 将分片以{fileId}_part_{index}的命名规则存储到R2存储桶
  2. 分片合并

    • 接收合并请求后,根据文件唯一标识列出所有关联的分片
    • 按分片索引排序,依次读取分片内容并拼接为完整文件
    • 将完整文件存入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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 21:01:44