大文件的断点续传

小占时光 2025-01-06 09:49:21 561


介绍

在软件开发过程中,文件上传是一项非常常见的功能,尤其是在涉及用户数据交互的场景中。通常情况下,我们通过一个简单的 <input> 标签实现文件选择,并配合提交按钮完成上传。这种方式操作简便,适用于小型文件上传需求。然而,当面对大文件上传时,这种传统方法往往会受到系统或网络的限制,导致无法一次性完成上传。尤其是在企业级应用中,用户上传的视频、数据包或其他大型文件可能会突破常规限制,给开发者带来额外的挑战。

针对大文件上传,IIS 等服务器会默认设定上传大小限制,通常为几十兆,超过这一限制的文件会被直接拒绝处理。此外,网络不稳定或上传中断的情况也可能导致用户体验不佳甚至数据丢失。因此,为了解决这些问题,我们需要引入更先进的文件上传技术,比如分片上传和断点续传。这些技术不仅能绕过文件大小限制,还能显著提升上传的可靠性和效率。

实现断点续传的核心思路

  • 文件分片:将大文件分成多个小的固定大小的块(例如 1MB)。
  • 分片上传:逐块上传文件分片,记录每个分片的上传状态。
  • 断点恢复:在上传中断后,从中断的位置继续上传未完成的分片。
  • 分片合并:所有分片上传完成后,服务器端将分片合并成完整文件。

实现代码

断点续传一般都需要前后端一起配合,下面的示例代码只是简单的流程实现,如果在生产环境,还需要在提交参数时,增加校验参数,比如增加token验证数据是否合法。

HTML

<div class="mt-5">
    <div class="my-5">文件断点续传</div>

    <div class="mb-3 col-md-4">
        <input class="form-control" type="file" id="fileInput">
    </div>

    <button class="btn btn-primary" id="uploadButton">上传</button>
    <div id="progress"></div>
</div>

页面效果如下:

Js代码实现

const chunkSize = 1024 * 1024; // 每个分片大小 1MB
let file, fileName, totalChunks;

// 点击上传按钮
$("#uploadButton").click(async function () {
    file = $("#fileInput")[0].files[0];
    if (!file) {
        alert("请选择文件!");
        return;
    }

    fileName = file.name;
    totalChunks = Math.ceil(file.size / chunkSize);

    for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
        const start = chunkIndex * chunkSize;
        const end = Math.min(file.size, start + chunkSize);
        const fileChunk = file.slice(start, end);

        // 检查后端是否已经存在当前分片
        const isUploaded = await checkChunkUploaded(chunkIndex);
        if (isUploaded) {
            updateProgress(chunkIndex + 1);
            continue;
        }

        await uploadChunk(fileChunk, chunkIndex);
        updateProgress(chunkIndex + 1);
    }

    await mergeFile(fileName, totalChunks);

    alert("文件上传完成!");
});

// 检查片段
function checkChunkUploaded(chunkIndex) {
    return $.ajax({
        url:  'Home/CheckChunkUploaded',
        type: "GET",
        data: { fileName, chunkIndex },
    }).then((res) => res.isUploaded);
}

// 上传片段
function uploadChunk(fileChunk, chunkIndex) {
    const formData = new FormData();
    formData.append("fileChunk", fileChunk);
    formData.append("fileName", fileName);
    formData.append("chunkIndex", chunkIndex);

    return $.ajax({
        url: 'Home/UploadChunk',
        type: "POST",
        data: formData,
        contentType: false,
        processData: false,
    });
}

// 进度条更新
function updateProgress(completedChunks) {
    const percentage = Math.round((completedChunks / totalChunks) * 100);
    $("#progress").text(`上传进度: ${percentage}%`);
}

// 合并文件
function mergeFile(fileName, totalChunks) {
    return $.ajax({
        url: 'Home/MergeFile?fileName=' + fileName + '&totalChunks=' + totalChunks,
        type: "POST",
        contentType: "application/json",
    });
}

js 主要是异步提交文件,后端需要提供对应的接口代码,用于片段检查,上传和合并。

后端接口实现

前端js做好交互后,后端提供相应的接口即可。

检查片段API接口

 public IActionResult CheckChunkUploaded(string fileName, int chunkIndex)
 {
     if(!Directory.Exists(UploadFolder))
         Directory.CreateDirectory(UploadFolder);

     var filePath = Path.Combine(UploadFolder, $"{fileName}.{chunkIndex}");
     return Ok(new { isUploaded = System.IO.File.Exists(filePath) });
 }

文件片段上传逻辑API接口

 public async Task<IActionResult> UploadChunk(IFormFile fileChunk, string fileName, int chunkIndex)
 {
     if (fileChunk == null || string.IsNullOrEmpty(fileName))
     {
         return BadRequest("Invalid file chunk or file name.");
     }

     var chunkPath = Path.Combine(UploadFolder, $"{fileName}.{chunkIndex}");
     using (var stream = new FileStream(chunkPath, FileMode.Create))
     {
         await fileChunk.CopyToAsync(stream);
     }
     return Ok();
 }

片段合并逻辑API代码

  public IActionResult MergeFile(string fileName, int totalChunks)
  {
      var finalFilePath = Path.Combine(UploadFolder, fileName);
      if (System.IO.File.Exists(finalFilePath))
      {
          return BadRequest("File already exists.");
      }

      using (var finalFile = new FileStream(finalFilePath, FileMode.Create))
      {
          for (int i = 0; i < totalChunks; i++)
          {
              var chunkPath = Path.Combine(UploadFolder, $"{fileName}.{i}");
              if (!System.IO.File.Exists(chunkPath))
              {
                  return BadRequest($"Missing chunk: {i}");
              }

              using (var chunkFile = new FileStream(chunkPath, FileMode.Open))
              {
                  chunkFile.CopyTo(finalFile);
              }

              System.IO.File.Delete(chunkPath); // 删除分片
          }
      }
      return Ok("File merged successfully.");
  }

大文件会分成很多片上传,提交上全后得到的是文件切片的结果。如果上传过程断网或者其他原因中断,用户可以重新上传,后端检查之前已经上传的片段,则会跳过,没有上传的则会继续上传,这样就大大节省了时间。

上传完成后,调用合并接口,得到的就是完整的文件。

总结

断点续传通过分片上传和合并的方式,解决了大文件上传效率低、失败后需重传的问题。在实际开发中可以结合前端的状态记录和后端的校验机制,实现更稳定可靠的文件上传系统。

如果对实现细节或优化有更多需求,欢迎讨论!

 

 

 

最后一次修改 : 2025/1/23 上午3:45:33

优秀
123
分享
123
奶茶
123
文章版权声明:版权归原作者(小占时光)所有。未经明确书面许可,严禁任何个人或组织以任何形式进行商业性或非商业性的使用、转载或抄袭。
评论