做航空航天相关系统时,我遇到过一类看上去很基础、实际上非常折腾的需求:用户要把本地一个很大的资料文件夹整体传到服务器上,而且不是“把文件选出来传上去”那么简单,必须把文件夹的层级结构、文件名、校验结果全部保留下来,还能随时看到传了几成、失败了哪些、能不能续传。我当时用的是 ASP.NET MVC(.NET Framework 4.7.2)部署在 IIS 上,整体打磨下来,发现“文件夹上传的精确控制”远不是拖一个上传组件就完事,里面藏着请求验证、IIS 限制、断点状态、路径穿越、并发写入这些一道道坎。
这篇就围绕航空航天场景里的实际约束,把 ASP.NET 下做文件夹上传精确控制的方法、配置和踩坑写清楚。主要内容包括:怎么在浏览器端拿到目录树和相对路径,怎么用分片上传把大目录拆成可控小任务,后端怎么保证目录结构不丢、文件不坏、中途断了能续,还有 IIS 和 web.config 那些真正会卡住你的限制项。适合正在用 ASP.NET / ASP.NET Core 做资料管理、试验数据回传、技术文档入库这类系统的开发者看,尤其是项目里需要处理成百上千个文件、单个文件可能到 GB 级的情况。
1. 航空场景里,“文件夹上传”先别急着写代码
1.1 精确控制的第一层:目录结构和相对路径不能丢
先问一个问题:为什么不能通过普通的上传控件,把所有文件平铺传到服务器上,再由一个人手工整理成目录?答案在航空航天这种场景里非常直白:资料如果失去了原始目录,基本等于废了一半。
举个例子,一次试飞或一次发动机孔探检查产生的数据,往往已经是按“型号/批次/测试项目/日期”组织好的目录结构。里面文件命名很规范,比如 TBO-2024-03-18_LH-01.mp4,但如果全部平铺到一个目录里,文件重名、批次混淆、人工二次归类错误就可能发生。数据合规审计时,要求看到的是“原始目录结构 + 原始文件名 + 校验值”,而不是一个被压平到不辨来源的文件池。
所以文件夹上传的第一层精确控制,就是完整保留相对路径。用户在本地选中的如果是 E:\batch_2024\TBO\images\engine_1\photo_01.jpg,上传到服务器后,它应该落在“上传根目录/batch_2024/TBO/images/engine_1/photo_01.jpg”,而不是只落一个 photo_01.jpg。这意味着前端不能只把文件一股脑抛给后端,必须连同“相对路径”一起传;后端也必须按相对路径逐级创建目录,再落文件。
1.2 精确控制的第二层:可校验、可断点、可审计
如果仅仅是“把文件传完”,那些带进度条的单文件上传控件也能凑合。但航空场景下,网络环境不总是理想。我接触过的部署现场,有的在办公室局域网,有的在临时搭建的测试工位,有 1Gbps 的高速环境,也有延迟高、偶尔断一下的无线网。一次上传几百 GB 的数据时,中间任何一秒断网都可能导致整批重来,这在工作流程上是不能接受的。
因此精确控制至少要包含三个能力:
- 可校验。服务器不能只信客户端说“这个文件传完了”,最好在完成后能核对文件大小、分片数量、最终文件哈希,至少保证文件在传输链路中没有缺损。
- 可断点续传。每个文件按固定大小切块,已经传完的块在服务器端有记录,重试时跳过,而不是从头再来。
- 可审计。谁、什么时候、哪个批次、哪个源目录、传了哪些文件、走完没有,都要留下日志。航空航天系统的质量流程里,资料上传本身就是有追溯要求的。
1.3 先约定好这几个边界指标
我建议任何项目在编码前,先和业务方把边界条件定清楚,而不是等开发到一半才谈。我在这个项目里实际的约定是这样的:
| 边界项 | 我的约定 | 理由 |
|---|---|---|
| 单次任务总大小 | 单目录不超过 100GB | 再大就建议分批次,避免一个任务拖太久 |
| 单文件大小 | 不超过 8GB | 再大应走离线介质导入,而不是在线传 |
| 分片大小 | 4MB | 对 IIS 和 ASP.NET 的请求体相对友好 |
| 单任务文件数上限 | 5000 个 | 防止一个文件夹里有几十万个零碎小文件 |
| 上传会话有效期 | 48 小时 | 超过时限自动清理临时目录 |
| 校验级别 | 完整 SHA256 校验 | 每分片校验一次,合成后再校验一次 |
这些数字并不绝对,但必须让前端、后端、部署和安全人员心里都有数。没有这些约束,“精确控制”只是一个口号。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 选型:为什么普通文件上传在 ASP.NET 里撑不住
2.1 浏览器如何“看到”文件夹
现代浏览器其实早就支持用户选择整个文件夹,而不是只能选文件。最简单的实现是给 <input> 加上 webkitdirectory 和 directory 属性:
html复制<input type="file" id="folderPicker" webkitdirectory directory multiple />
用户点开这个控件时,弹出的就是文件夹选择对话框,选完之后,input.files 里返回的是文件夹内所有文件的列表,而且每个 File 对象上都带有 webkitRelativePath 属性,例如 batch_2024/TBO/images/engine_1/photo_01.jpg。这个属性就是我们要保留的相对路径。
拖拽上传也可以实现。把文件夹拖到页面上的时候,使用 DataTransferItem.webkitGetAsEntry() 拿到目录项,再递归遍历目录树。这个递归逻辑不复杂,但要注意入口文件和目录两种情况,它的价值在于能实现“拖一个文件夹上来,界面实时显示目录树和文件数量”这类体验。
无论哪种方式,浏览器都是处于沙箱里的,它只能读取用户主动选择的目录,不可能遍历服务器目录,也不可能偷偷上传任何东西。这一点上航空信息安全的要求没有冲突——所有文件先出现在用户面前,需要确认后才会进入上传队列。
2.2 经典 HttpPostedFileUpload 的局限性
很多老系统里,ASP.NET 上传文件是这么写的:Request.Files 里拿到 HttpPostedFileBase,然后调用 SaveAs()。这种方式处理单文件、小文件很顺手,但放到“文件夹整体上传”的背景下,有几个硬伤很明显:
- 没有“目录结构”概念。一次只能拿到一个文件,而且默认不携带原始相对路径。
- 整个文件一次性进入请求体。IIS 和 ASP.NET 的限制直接摆在那,请求体太大会被拦。
- 没有进度感知。服务器端很难向前端反馈“当前第 3 个文件,共 250 个”。
- 没有断点能力。一个 5GB 的文件传到 90% 断了,前端只能重新整传。
所以开发到中后期我基本把思路定为:想要精确控制,就不让“一个大请求”承担所有事情,而是把整个上传流程拆成多个可控的小步骤。
2.3 分片上传:解决的不只是体积问题
分片上传常被误解为“只解决大文件超限的问题”。实际上,它带来的是一个更重要的能力:把上传过程变成可观测、可恢复的小事务。
比如一个 500MB 的文件,按 4MB 切分,会得到 125 个分片。每个分片都是一次独立的 HTTP POST 请求,服务器收到一个分片就存储一个分片文件,并记录状态。这样:
- 中间某几个分片传失败了,只需要重传失败的分片。
- 网络打断后,客户端可以查询服务器“这个文件已经上传了哪些分片”。
- 多个分片可以异步并发,提高带宽利用率。
- 每个分片体积小,IIS 默认请求体限制不容易触发。
文件夹层面的控制,就变成了“文件列表 + 每个文件的分片列表”的双层任务管理。这也是航空场景里更可接受的上传方式:没有不可恢复的整文件请求,只有可重试的分片。
3. 用 ASP.NET MVC 实现精确文件夹上传
3.1 前端递归遍历并锁定相对路径
先说前端。我偏向先用 input.webkitdirectory 拿到文件列表,再把它转成上传队列。核心代码如下:
javascript复制const blockSize = 4 * 1024 * 1024; // 4MB
const queue = [];
function collectFiles(fileList) {
for (const file of fileList) {
// 保留相对路径,例如 batch_2024/TBO/images/engine_1/photo_01.jpg
const relPath = file.webkitRelativePath || file.name;
queue.push({
relPath: relPath,
name: file.name,
size: file.size,
file: file,
uploadedBlocks: 0,
totalBlocks: Math.ceil(file.size / blockSize)
});
}
}
拿到队列之后,最好先做一次前端预处理:过滤掉空目录、检查文件类型、统计总大小、给用户展示一个目录清单,让用户点击“开始上传”后再真正发起请求。这一步很重要,因为文件夹里的文件可能非常多,一旦用户点错了目录,直接全量上传会浪费大量带宽和时间。
然后就可以逐个文件、逐个分片调用后台上传接口了。这里我用了简单循环配合 await,保证每次只并发有限个请求,避免把服务器连接池打满:
javascript复制async function uploadFile(item) {
const total = item.totalBlocks;
for (let blockIndex = 0; blockIndex < total; blockIndex++) {
const start = blockIndex * blockSize;
const end = Math.min(item.size, start + blockSize);
const blob = item.file.slice(start, end);
const form = new FormData();
form.append('filename', item.relPath);
form.append('blockIndex', blockIndex);
form.append('totalBlocks', total);
form.append('fileTotalSize', item.size);
form.append('file', blob, item.name);
form.append('uploadId', currentUploadId);
const resp = await fetch('/Upload/UploadBlock', {
method: 'POST',
body: form
});
const result = await resp.json();
if (!result.success) {
// 记录失败块,稍后统一重试
}
}
}
3.2 后端按相对路径落盘并防路径穿越
有一个前端比较容易忽略的细节:不要给后端传一个“目标路径的字符串”就直接被后端拼到服务器路径里,那很容易触发路径穿越。完整路径必须由后端基于一个受控的“上传根目录”来拼接,并且对相对路径做白名单式校验。
我的做法是这样的:每次上传都会先创建唯一任务 ID,比如 batch_2024_20240318_001,在服务器 App_Data/UploadTemp/{uploadId} 下保存所有文件。每个分片到达后,根据相对路径生成该文件的临时文件路径,然后写入分片块。
下面这个 Action 的核心处理逻辑可以当成模板:
csharp复制[HttpPost]
[ValidateInput(false)]
public JsonResult UploadBlock()
{
var uploadId = Request.Form["uploadId"];
var relPath = Request.Form["filename"];
var blockIndex = Convert.ToInt32(Request.Form["blockIndex"]);
var totalBlocks = Convert.ToInt32(Request.Form["totalBlocks"]);
var file = Request.Files["file"];
// 1. 固定根目录
string uploadRoot = Server.MapPath(Path.Combine("~/App_Data/UploadTemp", uploadId));
if (!Directory.Exists(uploadRoot))
Directory.CreateDirectory(uploadRoot);
// 2. 基于根目录计算完整路径,并强制确认没跳出去
string targetPath = Path.GetFullPath(Path.Combine(uploadRoot, relPath));
string rootFull = Path.GetFullPath(uploadRoot + Path.DirectorySeparatorChar);
if (!targetPath.StartsWith(rootFull, StringComparison.OrdinalIgnoreCase))
return Json(new { success = false, message = "非法路径" });
// 3. 创建目录,避免同一文件不同分片并发时目录不存在
var dir = Path.GetDirectoryName(targetPath);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
// 4. 保存当前分片到独立块文件
string blockFile = targetPath + $".{blockIndex}.part";
file.SaveAs(blockFile);
return Json(new { success = true, blockIndex = blockIndex });
}
注意几个关键点:
Path.Combine后必须再做一次Path.GetFullPath,否则relPath里的..\\..\\可能把路径引到根目录之外。- 分片文件不直接覆盖最终文件。每个分片先存成
文件名.序号.part,全部完整后再合并。这既是断点续传的基础,也能避免中间坏一个块就把整个文件写坏。 - 同一个文件多个分片并发上传时,目录创建和文件写入要保证原子性,避免出现目录还没建好就写入的竞态。
3.3 分片和断点状态维护
如果只是把分片落到磁盘,服务器还无法回答“这个文件传到哪个分片了”。所以还需要维护任务状态。最简单的方案是:在每个任务的根目录放一个 manifest.json,每收到一个分片就更新它。
json复制{
"uploadId": "batch_2024_20240318_001",
"totalFiles": 150,
"files": [
{
"relPath": "TBO/images/engine_1/photo_01.jpg",
"size": 104857600,
"totalBlocks": 25,
"doneBlocks": [0,1,2,4]
}
]
}
每次收到分片,检查该分片对应块文件是否存在,存在就标记 done。客户端发起续传请求时,后端扫描 manifest,返回“缺失块编号列表”,客户端只重传缺失部分。
我在实现中不会为每个分片都重写整个 manifest,因为文件多时会变成严重的写冲突。更稳妥的做法是:用与分片文件同名的 .done 标记文件,或者用数据库表记录 (uploadId, relPath, blockIndex, status)。块级状态文件天生支持并发,数据库记录适合后续审计。
3.4 Request 验证误伤:路径特殊字符与“潜在危险值”
这是 ASP.NET 老项目里非常常见的一个坑,搜索热词里也有“webconfig检测到有潜在危险的 request.querystring 值”。为什么上传文件夹会和它扯上关系?因为早期很多上传控件会把文件名直接放到 URL 或 QueryString 里,例如:
code复制/Upload/ProcessFile?filePath=xxx/yyy/..\..\zzz&fileName=a+b<>.jpg
ASP.NET 的请求验证看到 . 反斜杠、尖括号这类字符,直接判定为潜在危险请求,抛出 HttpRequestValidationException。尤其是文件夹路径里如果出现长路径、中文、百分号编码、双反斜杠,更容易触发。
要处理这个问题,首先明确原则:我们不要让文件路径出现在 URL 里。所有相对路径和元数据都应该放进 POST 请求体或 FormData 中,而不是查询字符串。如果因为历史原因无法改调用方式,可以用特性局部关闭验证,而不是关掉整个站点的验证:
csharp复制[HttpPost]
[ValidateInput(false)]
public JsonResult UploadBlock()
{
// 内部再手动校验路径,不让任何输入直接拼进文件系统
}
web.config 里可以设置:
xml复制<system.web>
<httpRuntime targetFramework="4.8" requestValidationMode="4.5" />
</system.web>
在现代 ASP.NET MVC 中,验证器是逐步执行的,配合 [ValidateInput(false)] 只放开这一个 Action 是比较克制的做法。千万不要为了省事把整个 pages validateRequest 关掉,否则其他页面等于裸奔。
3.5 别急着把 requestValidation 整个关闭
我会特别提醒一句:搜到报错信息后,很多人第一反应是去 web.config 把 validateRequest 改成 false,然后再把 requestValidationMode 改成 2.0。这个方案在遗留 WebForm 项目里可能“能跑通”,但它会让全站所有页面都停止请求验证,相当于为了一个上传接口废掉了一整面安全墙。
正确顺序是:
- 先判断是不是路径真的放在了 URL 或 QueryString 里。
- 能改前端就改成 POST body 传参。
- 必须保留 QueryString 时,给该控制器加
[ValidateInput(false)],并在代码里手动校验。 - 只有进入路径拼接时,才用白名单逻辑过滤非法字符,不允许任何用户输入原样进入
Path.Combine。
这样既解决了“潜在危险值”的错误,又不牺牲系统其它部分的安全性。
4. IIS、web.config 对上传大小的精确限制
4.1 IIS、ASP.NET 两套限制各管一段
很多开发者在本地调试时一切正常,部署到 IIS 后传大文件直接报错,这是因为没意识到上传大小限制其实被两套机制共同管理。
| 配置位置 | 默认值 | 单位 | 超过后的表现 |
|---|---|---|---|
<requestFiltering><requestLimits maxAllowedContentLength> |
30000000 | 字节 | 请求被 IIS 拦截,返回 404.13 |
<httpRuntime maxRequestLength> |
4096 | KB | ASP.NET 抛出请求长度超限 |
<httpRuntime executionTimeout> |
110 | 秒 | 执行超时,连接被断开 |
理论上它们都调大就行,但真正的问题是“调哪里”。IIS 层管的是 http.sys 接收到的请求内容长度,ASP.NET 层管的是运行时允许处理的请求。两层必须都配置。我当时按每分片 4MB(4096KB)来设计,所以把上传请求体限制放宽到 100MB,给多分片请求和冗余留足余地:
xml复制<system.web>
<httpRuntime targetFramework="4.8"
maxRequestLength="102400"
executionTimeout="3600" />
</system.web>
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="104857600" />
</requestFiltering>
</security>
</system.webServer>
如果单文件达到 GB 级,仍然走的是分片上传,不依赖把 maxRequestLength 调到几 GB。因为即使调大,服务器同步接收一个超大请求体,内存和超时压力都会变得不可控。
4.2 配置切不生效?多半是“层级”问题
最常见的问题不是不知道配置节,而是配置节写错了地方,或者被更上一层的配置覆盖。一个典型的层级关系是:
machine.config里的默认值- 站点根目录 web.config
- 具体应用程序目录下的 web.config
- IIS 的 applicationHost.config 里的 requestFiltering
如果 IIS 管理员在 applicationHost.config 里设置了更小的 maxAllowedContentLength,网站根目录写再大也没用。调试时可以从 IIS 管理器的“配置编辑器”里直接查看生效值。
另一个细节:<system.webServer> 节下的内容在 IIS 7 集成模式下才能生效,如果应用池是经典模式,有些配置项行为会有差异。建议统一使用集成模式。
4.3 ASP.NET Core Web API 发布到 IIS 的差异
现在新的系统越来越多直接用 ASP.NET Core Web API 发布到 IIS。如果要迁移,不能照抄上面那套经典 ASP.NET 配置,因为 Core 应用在 IIS 下运行时,web.config 只负责让 IIS 把请求转交给 Kestrel 或进程内托管,不再直接控制 System.Web 的请求大小。
Core 里需要同时处理两个层面:
- IIS 的 requestFiltering 依然有效,还是能拦超大的
Content-Length。 - Kestrel 自身有
MaxRequestBodySize限制,默认是 30MB 左右。
所以要在 Program.cs 或启动配置里放开请求体限制:
csharp复制builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 104857600; // 100MB
});
控制器层面也可以加特性:
csharp复制[RequestSizeLimit(104857600)]
[HttpPost]
public async Task<IActionResult> UploadBlock(IFormFile file)
{
// ...
}
发布到 IIS 时,web.config 长这样,重点不在 system.web,而是 aspNetCore 节:
xml复制<configuration>
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet"
arguments=".\YourApp.dll"
stdoutLogEnabled="false"
hostingModel="inprocess" />
</system.webServer>
</configuration>
在 IIS 上如果出现“本地调得好好的,发布后还是 413 或 404.13”,最优先检查的就是 requestFiltering 的 maxAllowedContentLength。它拦在 Kestrel 之前,你的应用代码根本不会被执行。
5. 参考开源方案时的取舍
5.1 从 GitHub 项目里能借鉴什么
搜索这类需求时,常会看到 GitHub 上各种表单上传组件、文件管理示例。很多项目把前端上传交互做得很完整,比如拖拽上传、文件夹选择、进度条、分片、秒传。直接引入可以让项目快速立起来。
但从工程角度,我会建议只借鉴几个核心模块,而不是整套黑盒引进:
- 分片切片逻辑。看它是如何用
file.slice()把文件切成均匀块的,这个逻辑几乎所有语言都一样。 - 并发控制。看并发数是固定 3 还是指数退避,能否自定义。
- 状态记录与恢复。看是否能在刷新页面后,通过后端接口恢复未完成的任务。
- 文件秒传与哈希。看它是用 MD5 还是 SHA256 计算文件指纹,计算是一口气读完整文件还是分块算。
这里有个误区:很多开源上传组件本身不带“服务端收文件的完整方案”,它给的是前端和部分 Node/Java 示例。如果后端是 ASP.NET,你要自己实现接收分片、落盘、状态管理、目录重建。指望一个前端组件解决全部服务端逻辑,不太现实。
5.2 参考资产管理系统时的“血”与“肉”
如果你参考一个现成的资产管理系统或文档管理系统的源码,会发现它最多做到“多文件选择上传”,很难做到“文件夹上传精确控制”。资产管理系统关心的更多是文件与资产条目的关联,而不是把离线目录结构原封不动地搬到服务器。
所以,从这类项目里可以借鉴表单校验、用户权限控制、审计日志的架构,但“文件夹上传精确控制”这部分还得自己把骨头填上。这个“骨头”包括任务清单、分片状态、合并校验和清理逻辑。
5.3 大文件夹不是入库素材,别照搬“推送”模式
顺带说一个很容易让人走偏的搜索词:GitHub 怎么上传整个文件夹。很多开发者会把“把代码仓库里的整个目录推到 GitHub”和“Web 网页上传文件夹”混为一谈。前者用的是仓库客户端,它没有可复用的浏览器上传接口,也没有进度恢复机制,和我们要做的系统完全不是一回事。
真正需要借鉴的,是那些解决“超大目录跨网络传输”的思维:先拆小、再并发、定期核对缺失块。这种思想在很多系统里都成立,哪怕是航空资料分发也一样。只要你决定用网页上传,就逃不开“用户选目录 -> 生成文件清单 -> 逐文件/逐分片上传 -> 服务器校验合并”这条主线。
6. 故障排查与实测心得
6.1 传上去的目录乱了:相对路径编码问题
我在联调时遇到过一个问题:后端收到的 filename 是好的,但 Path.Combine 之后总在个别中文文件名上乱码。后来定位到是前端 FormData 提交时,没有明确设置字符编码,导致某些浏览器把文件名以非 UTF-8 方式发送。
解决方法是统一前端编码,并在后端用 Request.Form 读取时明确解码行为。如果是 jQuery $.ajax 方式提交,注意 contentType 不要手动设置成 application/x-www-form-urlencoded,让浏览器自动处理 multipart 编码更靠谱。
6.2 传大文件报 404.13 或 413:看的是 IIS 不是代码
最典型的排查过程是:本地开发环境里直接传 200MB 文件没问题,部署到 Windows Server 后 100MB 就 404。打开 IIS 日志,错误码是 404.13,这代表 maxAllowedContentLength 没生效。
这时不要改代码,先用浏览器直接 POST 一个稍大的请求,如果 IIS 还没走到应用层就被拦,就是 requestFiltering 配置的问题。确认改完后可以重启一下应用池或执行 iisreset,因为部分 IIS 配置项不是立即热加载的。
6.3 分片并发太多反而把应用池拖垮
上传大文件夹时,追求速度很容易让人把并发数调到 10 甚至 20。实测下来,并发太高时小文件反而变慢,因为每个分片都要创建 TCP 连接、写 IIS 请求体、服务端落盘。AWS、Azure 等虽然有并发优势,但在自建 IIS 上,并发 3-5 是比较平稳的区间。
真正提高吞吐量的方法,不是无限增加并发,而是做两个优化:
- 大文件异步处理。多个分片写完后,触发一个后台合并任务,不阻塞上传接口。
- 文件落盘和数据库状态解耦。上传状态先写本地 JSON 或内存队列,最终由后台任务批量刷新数据库,避免每个分片都同步操作数据库。
6.4 一个小技巧:上传前先做“预检”
我在实际项目中加了一个很简单但很实用的接口:PrepareUpload。前端在真正传文件之前,先提交整个文件清单和后端预检:
- 服务端返回哪些文件已经存在且哈希一致(秒传跳过)。
- 哪些文件存在断点,返回分片缺失列表。
- 哪些文件类型不允许上传。
- 总大小是否超限。
有了预检,用户点击上传前就能看到“将跳过 12 个文件,继续上传 38 个文件”,心里非常有底。对于几百 GB 的数据,这个预检能省下大量无效传输。
6.5 合并与清理不能拖
所有分片传完后,服务端要做一个合并动作:把多个 .part 文件按顺序写入最终文件,然后计算整文件的 SHA256,和前端上报的值比对。不一致就标记失败,允许前端重传该文件。
合并动作往往是最后暴露问题的地方。我遇到过临时目录空间不足、分片块缺失、文件名过长导致最终路径超长等一堆问题。所以上传临时目录要放在磁盘空间充足的盘符上,并且定期清理超过保留期的任务目录。测试环境里如果空间被瞬时分片占满,可能就是清理任务没跑起来。
最后分享一条我个人的实际体会:做这类系统的核心难点,不是把代码写出来,而是把“什么算传完、什么算失败、失败后怎么恢复”定义清楚。你如果分片、断点、校验、权限日志这些环节都有明确策略,那这套上传功能即使界面朴素一点,用起来也比一个花哨但状态混乱的控件可靠得多。后期如果还要扩展,通常是把文件存档路径接入对象存储,再把上传任务从“本机临时目录”改成“对象存储分片接口”,但前面这套任务状态机依然是地基。
