1. 大文件上传的核心挑战与解决方案选型
在Web开发中处理大文件上传是个经典难题。最近在重构一个内部文档管理系统时,我需要实现用户能够通过浏览器上传单个超过2GB的设计文件,同时还要支持整个文件夹的批量传输。传统的<input type="file">方式在Chrome浏览器下最大只支持约4GB文件,且文件夹上传需要额外处理,更别提网络中断时的重传问题了。
经过多轮技术验证,最终采用了基于分片上传(Chunked Upload)结合前端文件夹遍历的方案。这个方案的核心优势在于:
- 分片机制将大文件切割成多个小块,每个分片2MB大小,通过并行上传提升速度
- 每个分片都有独立校验,网络中断后只需重传失败的分片
- 前端递归读取文件夹结构,保持原始目录层级
- 服务端采用临时文件合并策略,避免内存溢出
2. 前端实现关键技术解析
2.1 文件夹选取与文件遍历
现代浏览器提供了webkitdirectory属性,配合<input type="file">可以实现文件夹选择:
html复制<input type="file" id="folderUpload" webkitdirectory directory multiple>
获取文件列表后需要递归处理子文件夹。这里我封装了一个文件树构建方法:
csharp复制// 前端TypeScript代码
interface FileTreeNode {
path: string;
files: File[];
children: FileTreeNode[];
}
async function buildFileTree(fileList: FileList): Promise<FileTreeNode> {
const root: FileTreeNode = { path: '', files: [], children: [] };
for (let i = 0; i < fileList.length; i++) {
const file = fileList[i];
const pathParts = file.webkitRelativePath.split('/');
let currentLevel = root;
for (let j = 0; j < pathParts.length - 1; j++) {
const part = pathParts[j];
let child = currentLevel.children.find(n => n.path === part);
if (!child) {
child = { path: part, files: [], children: [] };
currentLevel.children.push(child);
}
currentLevel = child;
}
currentLevel.files.push(file);
}
return root;
}
2.2 分片上传实现细节
每个文件的上传需要经过三个关键步骤:
- 初始化上传:向服务端注册新上传任务,获取唯一uploadId
- 分片处理:将文件按2MB大小切割,计算每个分片的MD5值
- 并行上传:使用Promise.all同时上传多个分片,控制并发数量
核心上传代码如下:
javascript复制async function uploadFile(file: File, basePath: string) {
const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB
const chunkCount = Math.ceil(file.size / CHUNK_SIZE);
const uploadId = await initUploadSession(file.name, file.size);
const uploadPromises = [];
for (let i = 0; i < chunkCount; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(file.size, start + CHUNK_SIZE);
const chunk = file.slice(start, end);
uploadPromises.push(
uploadChunk(uploadId, i, await calculateMD5(chunk), chunk)
);
// 控制并发数为5
if (uploadPromises.length >= 5) {
await Promise.all(uploadPromises);
uploadPromises.length = 0;
}
}
// 上传剩余分片
if (uploadPromises.length > 0) {
await Promise.all(uploadPromises);
}
await completeUpload(uploadId);
}
重要提示:前端计算MD5是非常耗时的操作,对于大文件可能导致页面卡顿。解决方案是使用Web Worker在后台线程进行计算,或者改用更简单的CRC校验。
3. 服务端C#实现方案
3.1 接收分片的API设计
ASP.NET Core中需要设计三个关键接口:
csharp复制[ApiController]
[Route("api/upload")]
public class UploadController : ControllerBase
{
[HttpPost("init")]
public IActionResult InitUpload([FromBody] FileInfoModel model)
{
var uploadId = Guid.NewGuid().ToString();
// 在数据库或缓存中记录上传任务
return Ok(new { uploadId });
}
[HttpPost("chunk/{uploadId}/{chunkIndex}")]
public async Task<IActionResult> UploadChunk(
string uploadId,
int chunkIndex,
[FromForm] IFormFile chunk)
{
// 验证分片MD5
// 存储分片到临时目录
return Ok();
}
[HttpPost("complete/{uploadId}")]
public async Task<IActionResult> CompleteUpload(string uploadId)
{
// 合并所有分片
// 清理临时文件
return Ok(new { path = "/final/path" });
}
}
3.2 分片存储与合并策略
为了避免内存溢出,我们采用文件流的方式处理分片:
csharp复制private async Task SaveChunk(string uploadId, int chunkIndex, IFormFile chunk)
{
var chunkDir = Path.Combine(_tempPath, uploadId);
Directory.CreateDirectory(chunkDir);
var chunkPath = Path.Combine(chunkDir, $"{chunkIndex}.part");
using (var stream = new FileStream(chunkPath, FileMode.Create))
{
await chunk.CopyToAsync(stream);
}
}
合并文件时采用增量追加模式:
csharp复制private async Task MergeChunks(string uploadId, string fileName)
{
var chunkDir = Path.Combine(_tempPath, uploadId);
var finalPath = Path.Combine(_uploadPath, fileName);
using (var finalStream = new FileStream(finalPath, FileMode.Create))
{
var chunkFiles = Directory.GetFiles(chunkDir)
.OrderBy(f => int.Parse(Path.GetFileNameWithoutExtension(f)));
foreach (var chunkFile in chunkFiles)
{
using (var chunkStream = new FileStream(chunkFile, FileMode.Open))
{
await chunkStream.CopyToAsync(finalStream);
}
System.IO.File.Delete(chunkFile);
}
}
Directory.Delete(chunkDir);
}
4. 性能优化与异常处理
4.1 上传加速策略
- 并行上传:前端同时上传多个分片,实测将并发数设为5时效率最佳
- 压缩分片:对文本类文件使用Brotli压缩,平均减少30%传输量
- 智能分片:根据网络质量动态调整分片大小(2MB-10MB)
4.2 断点续传实现
关键是在服务端记录上传进度:
csharp复制public class UploadSession
{
public string UploadId { get; set; }
public string FileName { get; set; }
public long FileSize { get; set; }
public List<int> UploadedChunks { get; set; } = new List<int>();
public DateTime LastActivity { get; set; } = DateTime.UtcNow;
}
前端在上传前先查询已上传的分片:
javascript复制async function getUploadedChunks(uploadId) {
const response = await fetch(`/api/upload/progress/${uploadId}`);
return await response.json(); // [0, 1, 2]
}
4.3 常见问题排查
-
跨域问题:确保服务端配置CORS
csharp复制services.AddCors(options => { options.AddPolicy("UploadPolicy", builder => { builder.WithOrigins("https://yourdomain.com") .AllowAnyMethod() .AllowAnyHeader(); }); }); -
超时设置:调整Kestrel默认超时
csharp复制webBuilder.ConfigureKestrel(serverOptions => { serverOptions.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(10); serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(10); }); -
文件权限:确保临时目录有写入权限
bash复制icacls "C:\temp\upload" /grant "IIS_IUSRS:(OI)(CI)F"
5. 安全防护措施
-
文件校验:合并完成后验证整体MD5
csharp复制using var md5 = MD5.Create(); using var stream = System.IO.File.OpenRead(filePath); var hash = md5.ComputeHash(stream); var actualMd5 = BitConverter.ToString(hash).Replace("-", ""); -
类型限制:检查文件签名而非扩展名
csharp复制private static readonly Dictionary<string, byte[]> _fileSignatures = new() { [".png"] = new byte[] { 0x89, 0x50, 0x4E, 0x47 }, [".pdf"] = new byte[] { 0x25, 0x50, 0x44, 0x46 } }; -
病毒扫描:集成Windows Defender
csharp复制var scanner = new Process { StartInfo = new ProcessStartInfo { FileName = "MpCmdRun.exe", Arguments = $"-Scan -ScanType 3 -File \"{filePath}\" -DisableRemediation", RedirectStandardOutput = true } };
在实际项目中,这套方案成功支持了单文件8GB的设计稿上传,文件夹层级深度达到15级也不会出现内存问题。一个关键经验是:一定要在前端限制并发上传的分片数量,否则浏览器可能会因内存不足而崩溃。我建议根据用户设备性能动态调整这个值,可以通过navigator.hardwareConcurrency获取CPU核心数作为参考基准。