在C#服务端实现支持断点续传的文件上传API,核心思路是通过分片上传、记录上传进度、校验已上传分片的方式,让客户端在中断后可以从上次停止的位置继续上传,避免重复传输已完成的文件内容。

断点续传的核心原理
断点续传的实现依赖三个关键步骤:
- 客户端将大文件拆分为固定大小的分片,按顺序上传每个分片
- 服务端接收分片后,将分片临时存储,并记录当前文件已上传的分片信息
- 上传中断后,客户端先向服务端查询已上传的分片列表,跳过已上传的部分,继续上传剩余分片
所有分片上传完成后,服务端将临时分片合并为完整的原始文件,完成整个上传流程。
API接口设计
我们需要设计两个核心接口,分别用于查询上传进度和上传文件分片,接口参数定义如下:
| 接口功能 | 请求方式 | 请求路径 | 核心参数 |
|---|---|---|---|
| 查询上传进度 | GET | /api/upload/progress | fileId:文件的唯一标识,由客户端生成 |
| 上传文件分片 | POST | /api/upload/chunk | fileId:文件唯一标识,chunkIndex:当前分片索引,totalChunks:总分片数,chunkData:分片二进制数据 |
服务端核心实现代码
存储目录配置
首先定义临时分片存储目录和最终文件存储目录,确保目录存在:
using Microsoft.AspNetCore.Mvc;
using System.IO;
using System.Collections.Generic;
using System.Linq;
namespace BreakpointUploadDemo.Controllers
{
[ApiController]
[Route("api/upload")]
public class UploadController : ControllerBase
{
// 临时分片存储目录
private readonly string _tempChunkDir = Path.Combine(Directory.GetCurrentDirectory(), "UploadTemp", "Chunks");
// 最终文件存储目录
private readonly string _finalFileDir = Path.Combine(Directory.GetCurrentDirectory(), "UploadTemp", "Final");
public UploadController()
{
// 确保目录存在,不存在则创建
if (!Directory.Exists(_tempChunkDir))
{
Directory.CreateDirectory(_tempChunkDir);
}
if (!Directory.Exists(_finalFileDir))
{
Directory.CreateDirectory(_finalFileDir);
}
}
}
}
查询上传进度接口实现
该接口根据fileId查询已上传的分片索引,返回给客户端:
[HttpGet("progress")]
public IActionResult GetUploadProgress([FromQuery] string fileId)
{
if (string.IsNullOrEmpty(fileId))
{
return BadRequest(new { code = 400, message = "fileId不能为空" });
}
// 获取该文件对应的所有分片文件
var chunkFiles = Directory.GetFiles(_tempChunkDir, $"{fileId}_*.chunk");
// 提取已上传的分片索引
List<int> uploadedChunks = new List<int>();
foreach (var chunkFile in chunkFiles)
{
var fileName = Path.GetFileName(chunkFile);
// 文件名格式为 fileId_分片索引.chunk
var indexStr = fileName.Replace($"{fileId}_", "").Replace(".chunk", "");
if (int.TryParse(indexStr, out int chunkIndex))
{
uploadedChunks.Add(chunkIndex);
}
}
// 排序后返回
uploadedChunks.Sort();
return Ok(new { code = 200, uploadedChunks });
}
上传分片接口实现
接收客户端上传的分片,保存到临时目录,当所有分片上传完成后自动合并文件:
[HttpPost("chunk")]
public IActionResult UploadChunk([FromForm] string fileId, [FromForm] int chunkIndex, [FromForm] int totalChunks, IFormFile chunkData)
{
if (string.IsNullOrEmpty(fileId) || chunkData == null || chunkIndex < 0 || totalChunks <= 0)
{
return BadRequest(new { code = 400, message = "参数不完整" });
}
try
{
// 分片文件命名规则:fileId_分片索引.chunk
string chunkFileName = $"{fileId}_{chunkIndex}.chunk";
string chunkFilePath = Path.Combine(_tempChunkDir, chunkFileName);
// 保存分片到临时目录
using (var stream = new FileStream(chunkFilePath, FileMode.Create))
{
chunkData.CopyTo(stream);
}
// 检查是否所有分片都已上传完成
var allChunkFiles = Directory.GetFiles(_tempChunkDir, $"{fileId}_*.chunk");
if (allChunkFiles.Length == totalChunks)
{
// 所有分片上传完成,执行合并操作
string finalFileName = $"{fileId}_{Path.GetFileName(chunkData.FileName)}";
string finalFilePath = Path.Combine(_finalFileDir, finalFileName);
// 按分片索引顺序合并文件
using (var finalStream = new FileStream(finalFilePath, FileMode.Create))
{
for (int i = 0; i < totalChunks; i++)
{
string currentChunkPath = Path.Combine(_tempChunkDir, $"{fileId}_{i}.chunk");
using (var chunkStream = new FileStream(currentChunkPath, FileMode.Open))
{
chunkStream.CopyTo(finalStream);
}
// 合并完成后删除临时分片
System.IO.File.Delete(currentChunkPath);
}
}
return Ok(new { code = 200, message = "文件上传完成", filePath = finalFilePath });
}
return Ok(new { code = 200, message = $"分片{chunkIndex}上传成功" });
}
catch (System.Exception ex)
{
return StatusCode(500, new { code = 500, message = $"上传失败:{ex.Message}" });
}
}
关键注意事项
- fileId需要保证唯一性,客户端可以使用文件名+文件大小+修改时间的哈希值生成,避免不同文件冲突
- 分片大小建议设置为1-5MB,过小会增加请求次数,过大会降低断点续传的效率
- 可以定期清理超过一定时间的临时分片文件,避免占用过多磁盘空间
- 如果需要对上传文件做大小限制,可以在接口中添加校验逻辑,超过限制直接返回错误
断点续传的核心是进度持久化,上述示例将进度存储在本地文件系统,生产环境中可以替换为Redis等存储,提升并发场景下的性能。