.NET MVC大文件夹上传实战:分片、断点续传与目录保真

发布时间:2026/10/9 12:42:30
.NET MVC大文件夹上传实战:分片、断点续传与目录保真 1. 为什么工程建筑行业的文件夹上传总是卡在几百兆这道坎做工程建筑行业的系统前端上传功能从来不是加一个input typefile就能交差的。我们项目里用户要传的是施工图DWG、建筑信息模型RVT、现场影像资料、工程量清单往往一次就是一个完整的项目文件夹里面套着01-勘察设计→施工图→结构→图纸文件一个目录层级能到四五层文件数量几百上千个总大小动不动几个GB。这种需求用普通的上传组件跑十次有八次要出问题传一半连接断开、浏览器直接无响应、服务器返回404.13或者文件虽然传上去了目录结构全丢了一堆图纸平铺在一个目录里根本没法看。为什么普通上传扛不住先说几个最基础的瓶颈后面再讲具体怎么破。第一HTTP请求体大小限制。ASP.NET默认允许的请求体大小是4MB旧版到28.6MB不同版本有差异IIS的maxAllowedContentLength默认约30MB。超过这个数请求还没到你的控制器代码就被服务器挡在门外了。所以很多人说我明明改了maxRequestLength为什么还是401/404多半是只改了一层。第二同步上传与浏览器内存。使用传统表单或XMLHttpRequest整体上传一个大文件浏览器会把整个文件读进内存再往外发几百MB还好超过1GB页面基本就卡死了用户体验是传着传着浏览器变白屏。第三目录结构的上传协议不支持。HTML原生的文件选择框可以加webkitdirectory属性让用户选整个文件夹前端也拿得到File对象和webkitRelativePath但HTTP上传本身是文件级别的并没有文件夹这个概念。如果不在前端自行把相对路径编码进上传参数、不在后端按路径重建目录用户选了一个文件夹最后收到就是一堆文件名目录树丢得干干净净。第四连接稳定性。工程现场的网络环境比写字楼差得多设计院、工地项目部、异地分支机构经常是慢速上行、高延迟、还时不时断线。一次性传一个大文件中间断一次就得从头再来这种体验用户是不可能接受的。所以.NET MVC要支持工程建筑行业的大文件夹上传和目录结构本质上要解决的不是一个上传接口怎么写而是一整套方案服务端阈值放开、前端目录树数据采集、分片上传、后端临时目录管理、目录重建落盘、断点续传与秒传。这篇文章就按我们实际项目踩坑的顺序把每一步讲透。2. .NET MVC服务端的上传阈值配置web.config和IIS两层都要动2.1 先搞清ASP.NET管道的两层限制ASP.NET MVC运行在IIS之上一个上传请求要通过两层关卡每一层都有自己的限制只改其中一层问题依旧。第一层是ASP.NET运行时。配置项在web.config的httpRuntime节点里system.web httpRuntime targetFramework4.7.2 maxRequestLength2147483647 executionTimeout3600 requestValidationMode2.0 / /system.webmaxRequestLength单位是KB默认40964MB。这里调到2147483647对应.NET层允许的最大值约2GB。为什么是2GB因为这个配置项内部换算成字节后是int.MaxValue级别的值超过这个上限.NET Framework的请求缓冲机制会出现不可预期的问题。如果业务确实要单文件超过2GB请务必走分片上传不要指望这个配置。executionTimeout单位是秒默认110秒。传大文件时请求执行时间远超110秒一旦超时ASP.NET会直接终止请求表现为上传到一半页面报错。工程文件上行慢这里建议直接设置3600秒起。requestValidationMode2.0这行很关键。.NET 4.0以后默认开启请求数据验证上传请求的数据可能被当作潜在危险输入拦截。上传大文件时如果不处理部分请求会被HttpRequestValidationException打回来。第二层是IIS请求过滤。从IIS 7开始即使ASP.NET运行时放行了IIS自身的Request Filtering模块还会拦一道。报错状态码通常是404.13或413。system.webServer security requestFiltering requestLimits maxAllowedContentLength2147483648 / /requestFiltering /security /system.webServermaxAllowedContentLength单位是字节默认约30000000约28.6MB。这里设置成21474836482GB理由同上超过2GB的内容长度IIS的过滤模块处理起来会有性能和稳定性风险更稳妥的做法是分片。2.2 改完配置还要动什么改完web.config先确认应用池用的是Integrated模式Classic模式对请求过滤的行为有差异我们踩过一次经典模式下requestFiltering的配置排优先级和集成模式不同导致改了半天没生效。然后有一个容易被忽略的地方应用池超时回收。IIS应用池默认空闲超时20分钟如果上传过程中没有新的请求进来应用池可能把工作进程收掉正在传的分片全部作废。在IIS的应用池高级设置里把空闲超时改成0永不超时回收选项里的固定时间间隔也可以适当拉长或者配合分片上传的心跳机制解决后面细说。2.3 配置生效后如何验证不要一上来就传几个GB先用一个100MB文件试探再用500MB文件逐步加压。如果返回404.13说明IIS层没过检查system.webServer如果返回500或Maximum request length exceeded说明ASP.NET层没过检查system.web。如果返回404.8或者直接被请求过滤截断多半是URL路径本身包含非法字符——这个在工程文件里尤其常见因为DWG文件名里可能出现#、%、空格这些字符在URL编码不完整时会触发IIS的URL过滤规则。提示如果项目部署在负载均衡后面代理服务器Nginx、HAProxy的client_max_body_size也要一并调整否则请求会在到达IIS之前就被代理层拦截。我们贵阳那个项目就是这样IIS配置全改完了前端依然传不上最后排查到Nginx默认1MB的限制。3. 保住目录结构前端选型与文件相对路径的采集服务端配置放开只是第一步对工程建筑行业来说目录结构保真才是真正的核心需求。图纸管理系统里一个项目的文件夹结构往往是组织了几十年的管理规范什么文件放在什么目录下是有约定的比如施工图/建筑专业/一层平面图.dwg和施工图/结构专业/一层梁配筋图.dwg系统收到后必须按原目录落位不然后续的图纸检索、版本管理全乱套。3.1 浏览器目录选择的三种数据获取方式前端拿到目录结构的方式现在主流有三类。方式一input webkitdirectoryinput typefile webkitdirectory multiple idfolderPicker /用户选择整个文件夹后input.files里是所有文件的File对象每个对象带webkitRelativePath属性像这样施工图/建筑专业/一层平面图.dwg 施工图/结构专业/一层梁配筋图.dwg 现场照片/2025-03-12/南侧立面.jpg这个方案最省事Chrome、Edge、Firefox都支持IE彻底不用考虑因为现在工程行业的B端系统基本已经从IE迁移到Chromium内核浏览器了。方式二showDirectoryPicker()这是File System Access API里的方法Chrome和Edge支持可以拿到目录句柄后逐层遍历创建文件句柄再逐个读取。它的优势是可以拿到目录本身的信息比如空目录也能识别出来还能做拖拽目录上传。劣势是浏览器兼容性不如webkitdirectory而且实现代码量多不少。方式三自研拖拽上传解析DataTransferItem把整个文件夹从系统资源管理器拖到页面上通过dt.items递归遍历也能拿到目录树。这个方案交互最好但兼容性最差需要比较多的polyfill。我们最终选的是webkitdirectory为主、拖拽识别为辅。对工程行业用户来说让他点一下选择文件夹比教他拖拽更靠谱而且webkitRelativePath生态成熟后台上传组件WebUploader、Plupload都有现成的字段映射。3.2 上传组件怎么把目录结构带回去工程上用的比较多的开源方案是WebUploader和Plupload。我们需要确认两件事分片能力、以及分片上传时能否携带自定义路径参数。WebUploader的分片参数是chunked: truechunkSize按业务设比如2MB一片。关键在formData里塞目录信息uploader.on(fileQueued, function (file) { // file.relativePath 来自 webkitRelativePath 或自定义计算 uploader.opt(formData)[relativePath] file.relativePath; });但要注意WebUploader在分片请求时formData是跟着每一片一起提交的。也就是说后端每次收到分片都能拿到这个文件相对路径、当前分片序号、总片数这就够了。目录树无需一次性传过去只要每个文件的分片能准确告诉后端我是哪个路径下的第几片后端就能逐步重建。如果你不想用现成的上传组件、自研一套目录结构的数据设计也很简单。前端遍历fileList后为每个文件生成一个描述对象{ relativePath: 施工图/结构专业/一层梁配筋图.dwg, fileSize: 805306368, chunkSize: 2097152, chunks: 384, chunkIndex: 0, fileMd5: d41d8cd98f00b204e9800998ecf8427e, chunkMd5: c4ca4238a0b923820dcc509a6f75849b }3.3 空目录怎么处理工程项目的文件夹里经常有空目录比如施工图/给排水专业/待深化图纸这种还没放任何文件的占位目录。webkitdirectory遍历时只产生文件项空目录不会出现在fileList里。处理方式有两个要么在前端遍历时额外请求目录树接口拿到所有空目录列表一并提交要么干脆接受空目录不建的现状在上传完成后用户在系统里手动新建目录。B端系统多数接受后者毕竟空目录价值不大。但如果你的业务严格要求目录结构完全一致建议前端额外做一次目录句柄遍历把空目录的路径列表保存在一个JSON里跟随第一个分片一起提交后端重建目录时先建空目录再落文件。4. 后端目录重建与文件落盘分片接收、临时目录与合并4.1 控制器层接收参数约定服务端我建议把上传接口设计成两个/api/upload/init上传前初始化。前端传文件相对路径、文件大小、分片大小、文件Md5后端返回一个上传会话IDuploadId并检查该文件是否已存在秒传逻辑。/api/upload/chunk接收单个分片。参数包括uploadId、chunkIndex、totalChunks、相对路径文件本体是HttpPostedFileBase。为什么要有init接口一方面做秒传判断另一方面可以提前在服务端创建该文件的目录占位避免边传边建目录带来的并发创建冲突。控制器的核心代码大概是这样[HttpPost] public ActionResult UploadChunk() { var relativePath Request.Form[relativePath]; var uploadId Request.Form[uploadId]; var chunkIndex int.Parse(Request.Form[chunkIndex]); var totalChunks int.Parse(Request.Form[totalChunks]); var file Request.Files[file]; // 安全校验路径必须相对且合法禁止 .. 和盘符 if (!IsSafeRelativePath(relativePath)) { return Json(new { ok false, msg 非法路径 }); } // 每个文件的分片统一落在 临时目录/{uploadId} 下按 chunkIndex 命名 var chunkDir Server.MapPath($~/App_Data/_uploading/{uploadId}); Directory.CreateDirectory(chunkDir); file.SaveAs(Path.Combine(chunkDir, chunkIndex.ToString())); // 最后一篇到达时触发合并 if (chunkIndex totalChunks - 1) { var mergeResult MergeFile(uploadId, relativePath, totalChunks); return Json(mergeResult); } return Json(new { ok true, received chunkIndex }); }分片文件命名我直接用了序号没有把文件名带进去。因为uploadId已经隔离了每个文件的所有分片序号就够了。带文件名反而增加路径拼接时被注入的风险。4.2 目录安全校验必须做不能跳过工程文件名五花八门..、反斜杠、盘符都可能出现。在文件落盘之前绝对禁止直接把前端传的路径拼到服务器路径后面。我们项目里吃过一次亏用户传了一个文件夹里面有个文件名带了..拼接后文件直接被写到了web根目录下虽然当时没造成实质性破坏但这是个很吓人的教训。安全校验函数至少要做这些检查private bool IsSafeRelativePath(string relativePath) { if (string.IsNullOrWhiteSpace(relativePath)) return false; // 统一替换反斜杠 relativePath relativePath.Replace(\\, /); // 不允许绝对路径、不允许盘符、不允许上级目录 if (Path.IsPathRooted(relativePath)) return false; if (relativePath.Contains(:)) return false; if (relativePath.Split(/).Any(seg seg ..)) return false; // 过滤非法文件名字符 var invalidChars Path.GetInvalidFileNameChars(); foreach (var segment in relativePath.Split(/)) { if (segment.IndexOfAny(invalidChars) 0) return false; } return true; }对B端系统来说校验不通过的直接拒绝上传比偷偷改文件名更稳妥因为目录结构是业务约束用户需要知道自己的文件哪里不合规。4.3 最后一篇到达时的合并策略合并文件建议用FileStream按顺序逐片写入目标文件不要一次性把所有分片读进内存private ActionResult MergeFile(string uploadId, string relativePath, int totalChunks) { var chunkDir Server.MapPath($~/App_Data/_uploading/{uploadId}); var rootDir Server.MapPath(~/Uploads); // 完整的目标路径 上传根目录 相对路径 var safeRelPath CleanRelativePath(relativePath); var targetFullPath Path.Combine(rootDir, safeRelPath); var targetDir Path.GetDirectoryName(targetFullPath); Directory.CreateDirectory(targetDir); using (var fs new FileStream(targetFullPath, FileMode.Create, FileAccess.Write)) { for (var i 0; i totalChunks; i) { var chunkPath Path.Combine(chunkDir, i.ToString()); if (!System.IO.File.Exists(chunkPath)) { return Json(new { ok false, msg $分片缺失: {i} }); } using (var input new FileStream(chunkPath, FileMode.Open, FileAccess.Read)) { input.CopyTo(fs); } // 合并完一片删一片避免磁盘瞬间翻倍 System.IO.File.Delete(chunkPath); } } // 合并完成后校验文件大小然后清理临时目录 var fi new FileInfo(targetFullPath); if (fi.Length ! RequestedFileSize(uploadId)) { return Json(new { ok false, msg 文件大小校验失败请重新上传 }); } Directory.Delete(chunkDir, true); return Json(new { ok true, path safeRelPath }); }这里有一个设计细节合并时逐片删除分片文件。如果不删一个5GB的BIM模型会被拆成2GB分片占用临时目录加上目标文件5GB服务器磁盘瞬间需要7GB以上。逐片删能把临时占用控制在单分片大小范围内这是我们在实际项目里反复优化后的做法。4.4 合并与验证的边界情况合并结束后一定要校验文件大小。前端初始化接口传了fileSize后端在init时把它存在内存缓存或数据库里合并后对比。大小一致不保证文件一定完整但对磁盘IO出问题的情况是有效的预警。如果项目对完整性要求更高可以在前端流式读取文件时边算整体MD5分片上传时带chunkMd5合并后再对整个文件算一次MD5对比。代价是CPU和耗时工程文件普遍较大看业务接受度我们只在单个文件大于1GB或者客户有明确要求时才启用全量MD5校验。5. 大文件夹上传中那些不大不小却容易整崩的坑5.1 并发分片连接池把服务端压垮文件夹里有几百个文件每个文件再来几十个分片前端如果开启并发上传瞬间会发起大量HTTP请求。IIS默认的maxConcurrentRequestsPerCpu、应用池队列长度、TCP端口耗尽任何一个被打满上传就会表现为前几个文件正常后面的全部排队超时。我们第一次实测的时候前端设了threads: 3同时3个分片请求上传一个包含460个文件的目录跑到第200个文件左右应用池CPU 100%请求全面超时。排查了很久发现不是服务器性能不够而是每个分片请求都在做Request.Files的文件流解析加上SaveAs磁盘写操作高频并发下IO队列堆积。解决方案分两部分一是前端限流。不管文件夹里有多少文件建议设置全局并发数为2~3并且每个文件内部的分片串行上传。这样虽然速度会慢一些但不会把服务端打崩。对工程场景来说稳定比快重要。二是后端限流与异步化。在global.asax里给控制器加上[AsyncTimeout]把耗时操作放到异步方法里避免线程池饥饿。另外如果条件允许分片文件落盘可以走内存映射或直接写在独立磁盘上避免和系统盘、日志盘抢IO。5.2 超时链路浏览器、代理服务器、应用池、负载均衡上传一个1.8GB的文件夹即使分片上传整个上传过程可能持续30分钟以上。如果某个环节有超时限制中间就会被掐断。我们梳理出的超时点至少有四个浏览器页面长时间挂起浏览器可能在请求层超时。分片上传天然规避了这个问题——每一片都是独立请求单片通常几秒到十几秒就完成不会触发浏览器超时。代理/负载均衡Nginx的proxy_read_timeout默认60秒分片上传单片如果超过60秒代理就会断开。工程现场上行带宽不足时2MB分片也可能传很久。建议把client_body_timeout、proxy_read_timeout都调大到300秒以上并且根据现场网络实际情况调整分片大小。ASP.NET执行超时executionTimeout已经在第2节调大过。应用池空闲回收这个坑比较隐蔽。用户传完一个分片后可能停顿了一会才传下一片如果超过应用池空闲超时工作进程被回收所有临时目录里的分片还在但内存缓存里的uploadId和文件大小信息全丢了。解决方式把闲置超时设为0或者不要依赖内存缓存把上传会话信息写到数据库或Redis。5.3 临时目录磁盘爆炸工程文件动辄几个GB到几十GB如果每个人上传过程中都占用一份临时目录磁盘很快会满。我们有个项目上线第一周服务器100GB数据盘被打满原因就是一批用户同时传大文件临时目录里堆积了数不清的分片。应对方案定时清理任务写一个后台任务建议用Hangfire或Windows服务定期扫描App_Data/_uploading目录删除最后修改时间超过24小时且未完成合并的目录。合并后及时清理第4节已经提到逐片删除。独立磁盘分区上传临时目录和大文件存储目录单独挂盘不要和系统盘、数据库盘共用。哪怕是云服务器也要单独购买数据盘挂载。5.4 断点续传和秒传的实际体验分片上传天然支持断点续传但要做对核心在init接口的状态判断[HttpPost] public ActionResult InitUploadFile() { var relativePath Request.Form[relativePath]; var fileSize long.Parse(Request.Form[fileSize]); var fileMd5 Request.Form[fileMd5]; var targetFullPath Path.Combine(UploadRoot, relativePath); // 秒传目标文件已存在且大小一致 if (System.IO.File.Exists(targetFullPath)) { var existsFile new FileInfo(targetFullPath); if (existsFile.Length fileSize) { return Json(new { ok true, uploadId , needUpload false }); } } // 断点续传临时目录里已有部分分片 var uploadId ComputeUploadId(fileMd5, relativePath); var chunkDir Server.MapPath($~/App_Data/_uploading/{uploadId}); var uploadedChunks new Listint(); if (Directory.Exists(chunkDir)) { foreach (var chunkFile in Directory.GetFiles(chunkDir)) { uploadedChunks.Add(int.Parse(Path.GetFileName(chunkFile))); } } return Json(new { ok true, uploadId uploadId, needUpload true, uploadedChunks uploadedChunks }); }前端拿到uploadedChunks已经传过的分片直接跳过没传的继续这样用户中途关掉页面、重新打开、重新选同一个文件夹就能从断点继续。判断同一个文件夹靠fileMd5和relativePath组合所以前端在遍历文件时一定要稳定计算文件Md5不能每次都变。工程行业一个常见的误解是用户把文件夹A传了一半把文件夹A改名成A2再传服务端会不会认为是新任务会。因为relativePath变了uploadId也会变。所以断点续传要求用户重传时选择的目录路径不能变。6. 实测验证与调优建议6.1 一组实测数据我们的项目环境.NET Framework 4.7.2 MVC 5部署在两台Windows Server 20194核8G千兆内网系统盘和数据盘分离前端WebUploader分片2MB全局并发3。单文件850MB的RVT模型分片512片总耗时约12分钟受限于现场上行带宽中间断开两次断点续传后成功。一个包含942个文件、总大小约5.6GB的勘察设计文件夹目录层级5层首次上传耗时约58分钟期间服务端处理后端消息队列无积压应用池CPU稳定在30%~50%。一次极端测试模拟客户端在上传过程中拔网线临时目录中残留分片。重启上传后init接口返回已有分片列表跳过已传分片最终完整落盘目标文件MD5与源文件一致。这些数据说明方案是可行的但要注意瓶颈通常在客户端上行带宽和服务端磁盘IO代码层面可优化的空间反而没有想象中大。6.2 分片大小的选择分片大小直接影响两个指标单片的超时风险以及分片请求的数量。我们测试过1MB、2MB、4MB、8MB四种规格分片大小单文件850MB的分片数弱网环境下单片失败率服务端IO压力1MB850较低但总请求数过多高2MB512低中4MB213低中8MB107弱网下单片超时概率上升较低我们最终选了2MB。原因是2MB分片在4G、Wi-Fi信号不佳的环境下单片传输时间一般在10~30秒内既不会频繁超时也不会因为请求数量太多造成服务端压力。如果你的用户主要走内网环境可以放宽到4MB或8MB。6.3 上传进度怎么算才让用户信服文件夹上传的进度不能只算一个文件的进度用户关心的是整个文件夹传了百分之多少。计算公式建议这样总进度 已传文件的总大小 / 所有文件的总大小 × 70% 当前文件已传大小 / 当前文件总大小 × 30%仅计算当前正在上传的文件这个权重不是固定的可以根据平均文件大小调整。文件数量多且单个文件偏小的时候每个文件完成数对进度的贡献更大文件大且数量少的时候当前文件进度占比应该提高。我们在工程文件夹场景大量DWG图纸文件平均每份30MB~80MB用的就是70%和30%的权重用户普遍反馈进度条跟得上真实速度。6.4 上传完成后的目录结构校验我们遇到过一种情况分片全部上传完成、文件也合并了但有个别文件因为文件名非法被安全校验拦截导致整个目录缺了一个子目录。用户并没有第一时间发现等检索图纸的时候才发现少了文件。所以在上传完成后前端拿到所有relativePath列表请求一个校验接口[HttpPost] public ActionResult ValidateUploadedFiles(Liststring expectedPaths) { var missing new Liststring(); foreach (var relPath in expectedPaths) { var safeRelPath CleanRelativePath(relPath); var fullPath Path.Combine(UploadRoot, safeRelPath); if (!System.IO.File.Exists(fullPath)) { missing.Add(relPath); } } return Json(new { ok true, missing missing }); }前端拿到missing列表后给用户一个明确提示这些文件上传未完成请重新选择目录补传并把缺失文件单独列出来。这样一个闭环下来目录结构丢失基本可以被拦住。最后再分享一个实际操作中的体会我在这个项目之前一直以为大文件夹上传的核心是并发、是速度、是服务端性能做完之后才意识到对工程建筑行业来说目录结构保真和安全校验的优先级远高于上传速度。用户宁愿多等几分钟也不希望传到系统里的图纸目录一团乱麻。所有降速、限流、校验的机制都是为了让最终落盘的文件树和用户本地的文件夹一致。另外有一个小技巧上传完成后的落盘目录不要直接暴露给用户访问中间加一层虚拟路径映射或者受控下载接口。工程图纸是敏感数据直接通过静态文件URL访问的话URL一旦泄漏整个项目文件夹都能被遍历下载。上传和下载权限分开控制是这类系统上线前最值得检查的一件事。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询