1. 项目概述
在Web开发中,大文件上传一直是个令人头疼的问题。特别是当我们需要上传整个文件夹时,传统的单文件上传方式往往会因为网络不稳定、服务器限制或浏览器内存不足而失败。我在最近的一个企业文档管理系统项目中就遇到了这个挑战 - 客户需要批量上传包含数百个文件的工程文件夹,单个上传效率太低,而直接上传整个压缩包又失去了文件结构的可视性。
.NET WebForm虽然是个"老将",但在处理这类问题时依然有其独特的优势。通过分片上传技术,我们可以将大文件或文件夹拆分为多个小块分别传输,即使中途断网也能从断点续传,大大提升了上传的可靠性和用户体验。下面我就分享一套经过实战检验的完整实现方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心设计思路
2.1 前端分片处理流程
文件夹上传的前端处理比单文件复杂得多,需要遍历文件夹结构并保持原始目录关系。我们使用HTML5的File API配合自定义算法实现:
javascript复制// 递归读取文件夹内容
function readDirectoryEntries(directoryReader, path, fileList) {
directoryReader.readEntries(function(entries) {
entries.forEach(function(entry) {
if (entry.isFile) {
entry.file(function(file) {
file.relativePath = path; // 保存相对路径
fileList.push(file);
});
} else if (entry.isDirectory) {
var subPath = path + entry.name + "/";
readDirectoryEntries(entry.createReader(), subPath, fileList);
}
});
});
}
关键点在于:
- 使用webkitRelativePath属性保留文件原始路径
- 对每个文件计算MD5作为唯一标识
- 按固定大小(如5MB)进行文件分片
- 记录分片索引和总片数
2.2 后端接收与重组方案
ASP.NET WebForm处理分片上传需要解决几个特殊问题:
- 大文件缓冲区配置:
xml复制<system.web>
<httpRuntime maxRequestLength="2147483647" executionTimeout="3600" />
</system.web>
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="2147483648" />
</requestFiltering>
</security>
</system.webServer>
- 分片数据接收:
csharp复制HttpPostedFile file = context.Request.Files[i];
string chunkIndex = context.Request.Form["chunk"];
string totalChunks = context.Request.Form["chunks"];
string fileGuid = context.Request.Form["fileGuid"];
string tempPath = Server.MapPath("~/UploadTemp/" + fileGuid);
if (!Directory.Exists(tempPath))
{
Directory.CreateDirectory(tempPath);
}
file.SaveAs(Path.Combine(tempPath, chunkIndex));
- 文件重组逻辑:
当收到最后一片时,按索引顺序合并所有分片:
csharp复制if (int.Parse(chunkIndex) == int.Parse(totalChunks) - 1)
{
string finalPath = Server.MapPath("~/Uploads/" + originalFileName);
using (var fs = new FileStream(finalPath, FileMode.Create))
{
for (int i = 0; i < int.Parse(totalChunks); i++)
{
string chunkPath = Path.Combine(tempPath, i.ToString());
byte[] buffer = File.ReadAllBytes(chunkPath);
fs.Write(buffer, 0, buffer.Length);
File.Delete(chunkPath);
}
}
Directory.Delete(tempPath);
}
3. 完整实现步骤
3.1 前端实现细节
- 拖拽上传区域:
html复制<div id="dropArea" style="border:2px dashed #ccc; padding:20px; text-align:center;">
<p>拖拽文件夹到此处或点击选择</p>
<input type="file" id="fileInput" webkitdirectory directory multiple />
</div>
- 分片上传控制:
javascript复制function uploadFile(file, chunkSize) {
var chunks = Math.ceil(file.size / chunkSize);
var uploaded = 0;
for(var i=0; i<chunks; i++){
var start = i * chunkSize;
var end = Math.min(file.size, start + chunkSize);
var chunk = file.slice(start, end);
var formData = new FormData();
formData.append('file', chunk);
formData.append('name', file.name);
formData.append('size', file.size);
formData.append('chunk', i);
formData.append('chunks', chunks);
formData.append('relativePath', file.relativePath);
$.ajax({
url: 'UploadHandler.ashx',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function() {
uploaded++;
if(uploaded === chunks){
console.log(file.name + '上传完成');
}
}
});
}
}
3.2 后端处理程序
创建泛型处理程序(UploadHandler.ashx):
csharp复制public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/plain";
try
{
if (context.Request.Files.Count > 0)
{
HttpPostedFile file = context.Request.Files[0];
string fileName = context.Request.Form["name"];
string relativePath = context.Request.Form["relativePath"];
string chunk = context.Request.Form["chunk"];
string chunks = context.Request.Form["chunks"];
// 创建按GUID命名的临时文件夹
string fileGuid = CalculateMD5(fileName + relativePath);
string tempFolder = context.Server.MapPath("~/UploadTemp/" + fileGuid);
if (!Directory.Exists(tempFolder))
Directory.CreateDirectory(tempFolder);
// 保存分片
string chunkPath = Path.Combine(tempFolder, chunk);
file.SaveAs(chunkPath);
// 检查是否所有分片都已上传
if (int.Parse(chunk) == int.Parse(chunks) - 1)
{
// 重建目录结构
string destPath = context.Server.MapPath("~/Uploads/" + relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(destPath));
// 合并文件
using (FileStream fs = new FileStream(destPath, FileMode.Create))
{
for (int i = 0; i < int.Parse(chunks); i++)
{
string tempFilePath = Path.Combine(tempFolder, i.ToString());
byte[] buffer = File.ReadAllBytes(tempFilePath);
fs.Write(buffer, 0, buffer.Length);
File.Delete(tempFilePath);
}
}
Directory.Delete(tempFolder);
context.Response.Write("{\"status\":\"ok\",\"path\":\"" + relativePath + "\"}");
}
else
{
context.Response.Write("{\"status\":\"chunk_uploaded\"}");
}
}
}
catch (Exception ex)
{
context.Response.Write("{\"status\":\"error\",\"message\":\"" + ex.Message + "\"}");
}
}
private string CalculateMD5(string input)
{
using (MD5 md5 = MD5.Create())
{
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
byte[] hashBytes = md5.ComputeHash(inputBytes);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hashBytes.Length; i++)
{
sb.Append(hashBytes[i].ToString("x2"));
}
return sb.ToString();
}
}
4. 关键问题与优化方案
4.1 内存优化技巧
大文件上传最容易出现内存溢出问题,我们通过以下方式优化:
- 流式处理替代全内存操作:
csharp复制// 合并文件时使用流式处理
using (FileStream output = new FileStream(destPath, FileMode.Create))
{
for (int i = 0; i < chunks; i++)
{
string tempFile = Path.Combine(tempFolder, i.ToString());
using (FileStream input = File.OpenRead(tempFile))
{
input.CopyTo(output);
}
File.Delete(tempFile);
}
}
- 上传限速控制:
前端通过setTimeout控制上传并发:
javascript复制var activeUploads = 0;
var maxConcurrent = 3;
function uploadNextChunk() {
if(activeUploads < maxConcurrent && chunksQueue.length > 0){
activeUploads++;
var chunk = chunksQueue.shift();
// 上传逻辑...
}
}
// 每个分片上传完成后
activeUploads--;
uploadNextChunk();
4.2 断点续传实现
要实现可靠的断点续传,需要:
- 服务端记录已接收的分片:
csharp复制// 在Application_Start中初始化字典
Application["UploadProgress"] = new ConcurrentDictionary<string, List<int>>();
// 上传处理中记录
var progress = (ConcurrentDictionary<string, List<int>>)Application["UploadProgress"];
progress.AddOrUpdate(fileGuid,
new List<int> { int.Parse(chunk) },
(key, existing) => {
existing.Add(int.Parse(chunk));
return existing;
});
- 前端在上传前先查询缺失的分片:
javascript复制function checkUploadStatus(file, callback) {
$.get('UploadHandler.ashx?action=check&name=' + encodeURIComponent(file.name)
+ '&relativePath=' + encodeURIComponent(file.relativePath),
function(response) {
var missingChunks = [];
for(var i=0; i<response.totalChunks; i++){
if(response.receivedChunks.indexOf(i) === -1){
missingChunks.push(i);
}
}
callback(missingChunks);
});
}
4.3 文件夹结构保持
保持原始文件夹结构的关键在于:
- 前端获取完整相对路径:
javascript复制// 通过webkitRelativePath获取完整路径
var relativePath = file.webkitRelativePath ||
(file.relativePath ? file.relativePath : file.name);
- 服务端重建目录:
csharp复制string destDirectory = Path.GetDirectoryName(destPath);
if (!Directory.Exists(destDirectory))
{
Directory.CreateDirectory(destDirectory);
}
5. 安全与异常处理
5.1 文件类型校验
csharp复制// 允许的文件扩展名白名单
string[] allowedExtensions = { ".doc", ".docx", ".pdf", ".xls", ".xlsx" };
string fileExt = Path.GetExtension(fileName).ToLower();
if (!allowedExtensions.Contains(fileExt))
{
context.Response.Write("{\"status\":\"error\",\"message\":\"不支持的文件类型\"}");
return;
}
5.2 防重复上传
使用数据库记录已上传文件:
csharp复制// 检查文件是否已存在
string fileHash = CalculateFileHash(destPath);
using (var conn = new SqlConnection(connectionString))
{
var cmd = new SqlCommand("SELECT COUNT(*) FROM Files WHERE FileHash=@hash", conn);
cmd.Parameters.AddWithValue("@hash", fileHash);
conn.Open();
int count = (int)cmd.ExecuteScalar();
if (count > 0)
{
context.Response.Write("{\"status\":\"error\",\"message\":\"文件已存在\"}");
return;
}
}
5.3 上传超时处理
调整IIS和ASP.NET的超时设置:
xml复制<system.web>
<httpRuntime executionTimeout="3600" /> <!-- 单位:秒 -->
</system.web>
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="4294967295" /> <!-- 单位:字节 -->
</requestFiltering>
</security>
</system.webServer>
6. 性能优化建议
- 压缩分片数据:
javascript复制// 前端使用pako库进行gzip压缩
var compressedChunk = pako.gzip(chunk);
formData.append('file', new Blob([compressedChunk]), file.name);
- 服务端解压:
csharp复制using (var compressedStream = new GZipStream(file.InputStream, CompressionMode.Decompress))
using (var decompressedStream = new MemoryStream())
{
compressedStream.CopyTo(decompressedStream);
byte[] buffer = decompressedStream.ToArray();
// 处理解压后的数据
}
- 分布式存储方案:
对于超大规模应用,可以考虑使用分布式文件系统:
csharp复制// 使用云存储API替代本地文件操作
var cloudStorage = new CloudStorageProvider();
cloudStorage.UploadChunk(fileGuid, chunk, chunkIndex);
- 前端进度显示优化:
javascript复制// 使用XMLHttpRequest的progress事件
xhr.upload.addEventListener("progress", function(e) {
if (e.lengthComputable) {
var percent = Math.round((e.loaded / e.total) * 100);
updateProgress(file.id, percent);
}
}, false);
这套方案在我负责的多个企业级项目中稳定运行,单次成功上传过超过10GB的设计图纸文件夹。关键在于分片大小的合理设置 - 经过测试,2-5MB的分片在大多数网络环境下表现最佳,既能充分利用带宽,又不会因为单个分片失败导致大量数据重传。
