.NET程序员的血泪奋斗史:从0到1搞定大文件上传(含IE8兼容)

咱福建.NET仔最近接了个外包活,客户是做政府资料管理的,需求就一句话:“搞个大文件上传功能,20G文件随便传,文件夹要留层级,IE8也能用,加密传输存储,断点续传不能丢进度!”

客户还补刀:“网上那些开源组件都是垃圾,WebUploader停更了,H5方案IE8跑不动,我要原生JS!预算100块以内,源码要全,文档要细,不然换人!”

得,硬着头皮上吧!以下是我踩坑一周后总结的全栈解决方案,含前后端代码、IE8兼容技巧、加密和断点续传实现,直接复制就能用!


一、需求拆解:客户要的到底是个啥?

用大白话翻译客户需求:

  1. 大文件上传:20G文件分片传,断点续传(关浏览器/重启电脑不丢进度)。
  2. 文件夹保留层级:上传/部门/2024项目/数据.xlsx,下载时也得是这个结构(别给我打包成zip!)。
  3. 加密:传输用SM4/AES,存储也加密(客户说“数据比命重要”)。
  4. 兼容IE8:Win7+IE8的老机器必须能用(甲方爸爸的机器不能碰)。
  5. 后端ASP.NET WebForm:客户不让换技术栈(只能硬啃WebForm)。

二、前端:原生JS + IE8兼容的“土味”方案

IE8不支持File API,不能用H5的分片上传,只能用Flashiframe表单模拟异步。但客户要原生JS,所以选iframe+表单方案(Flash已被淘汰,且客户可能禁用)。

1. 前端核心代码(Vue3兼容IE8?不存在的,IE8只认ES5)

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="UploadPage.aspx.cs" Inherits="BigFileUpload.UploadPage" %>




    大文件上传(IE8兼容版)
    
        // 全局变量:存储上传任务、进度、加密密钥
        var uploadTasks = {}; // 格式:{ fileId: { progress: 0, chunks: [], totalChunks: 0 } }
        var sm4Key = '客户给的SM4密钥(实际从后端动态获取)'; // 加密密钥(硬编码仅示例,实际要安全存储)

        // 初始化:绑定文件选择按钮
        window.onload = function() {
            document.getElementById('btnSelect').onclick = function() {
                document.getElementById('fileInput').click(); // 触发文件选择
            };
            document.getElementById('fileInput').onchange = handleFileSelect;
        };

        // 处理文件选择(支持文件夹?IE8不支持webkitdirectory,只能手动输入路径)
        function handleFileSelect(e) {
            var file = e.target.files[0];
            if (!file) return;

            // 生成唯一fileId(防重复)
            var fileId = 'file_' + Date.now();
            uploadTasks[fileId] = {
                progress: 0,
                chunks: [],
                totalChunks: Math.ceil(file.size / 2 * 1024 * 1024), // 分片大小2MB
                fileName: file.name,
                fileSize: file.size
            };

            // 计算文件MD5(用于断点校验,IE8用SparkMD5的兼容版)
            calculateFileMD5(file, fileId);
        }

        // 计算文件MD5(IE8兼容版,用SparkMD5的旧版)
        function calculateFileMD5(file, fileId) {
            var chunkSize = 2 * 1024 * 1024; // 2MB/片
            var chunks = Math.ceil(file.size / chunkSize);
            var spark = new SparkMD5.ArrayBuffer();
            var reader = new FileReader();

            // 分片读取文件(IE8用FileReader的同步模式)
            function loadNextChunk(currentChunk) {
                var start = currentChunk * chunkSize;
                var end = Math.min(start + chunkSize, file.size);
                reader.readAsArrayBuffer(file.slice(start, end)); // IE8可能不支持slice,改用substring?
            }

            reader.onload = function(e) {
                spark.append(e.target.result);
                var currentChunk = Math.floor(e.target.result.byteLength / (2 * 1024 * 1024));
                if (currentChunk < chunks - 1) {
                    loadNextChunk(currentChunk);
                } else {
                    var md5 = spark.end();
                    uploadTasks[fileId].md5 = md5; // 记录MD5(断点校验用)
                    startUpload(fileId); // 开始上传
                }
            };

            loadNextChunk(0);
        }

        // 开始上传(分片+断点续传)
        function startUpload(fileId) {
            var task = uploadTasks[fileId];
            var chunkIndex = task.chunks.length; // 下一个要传的分片序号

            // 检查是否已上传过该分片(从localStorage读进度)
            var savedProgress = localStorage.getItem('upload_' + fileId);
            if (savedProgress) {
                task.chunks = JSON.parse(savedProgress).chunks;
                chunkIndex = task.chunks.length;
            }

            // 上传当前分片(IE8用iframe模拟异步)
            var formData = new FormData();
            formData.append('fileId', fileId);
            formData.append('chunkIndex', chunkIndex);
            formData.append('totalChunks', task.totalChunks);
            formData.append('fileName', task.fileName);
            formData.append('chunk', task.file.slice(chunkIndex * 2 * 1024 * 1024, (chunkIndex + 1) * 2 * 1024 * 1024));

            var iframe = document.createElement('iframe');
            iframe.style.display = 'none';
            iframe.src = 'UploadHandler.ashx?action=upload&fileId=' + fileId + '&chunkIndex=' + chunkIndex;
            document.body.appendChild(iframe);

            // 监听iframe加载完成(上传结果)
            iframe.onload = function() {
                var response = JSON.parse(iframe.contentWindow.document.body.innerText);
                if (response.code === 200) {
                    task.chunks.push(chunkIndex);
                    task.progress = (task.chunks.length / task.totalChunks) * 100;
                    localStorage.setItem('upload_' + fileId, JSON.stringify(task)); // 保存进度到localStorage
                    if (task.chunks.length === task.totalChunks) {
                        mergeChunks(fileId); // 合并所有分片
                    }
                } else {
                    alert('上传失败:' + response.msg);
                }
                document.body.removeChild(iframe);
            };
        }

        // 合并分片(调用后端接口)
        function mergeChunks(fileId) {
            var xhr = new XMLHttpRequest();
            xhr.open('POST', 'UploadHandler.ashx?action=merge&fileId=' + fileId);
            xhr.onload = function() {
                var response = JSON.parse(xhr.responseText);
                if (response.code === 200) {
                    alert('上传完成!文件路径:' + response.filePath);
                    localStorage.removeItem('upload_' + fileId); // 清除进度
                } else {
                    alert('合并失败:' + response.msg);
                }
            };
            xhr.send();
        }
    


    
        
            
            选择文件(支持20G)
        
    


2. 前端关键技巧(IE8兼容)
  • 分片上传:用File.slice()(IE8不支持,改用file.substring()?实际测试IE8的File对象不完整,可能需要用ActiveXObject读取文件流,这里简化处理)。
  • 进度保存:用localStorage存上传进度(IE8支持),但容量有限(建议分块存储,比如每5%存一次)。
  • 加密预处理:前端用SM4加密分片(示例未写,需引入sm-crypto库的ES5兼容版)。

三、后端:ASP.NET WebForm(C#)实现分片合并+加密存储

后端需要处理分片接收、断点校验、合并文件、加密存储,还要兼容SQL Server。

1. 后端核心代码(UploadHandler.ashx)
// UploadHandler.ashx(处理上传请求)
public class UploadHandler : IHttpHandler, IRequiresSessionState
{
    private string _uploadDir = HttpContext.Current.Server.MapPath("~/Uploads/Temp/"); // 临时分片存储路径
    private string _encryptKey = "客户给的SM4密钥(从配置或数据库读取)"; // 加密密钥

    public void ProcessRequest(HttpContext context)
    {
        context.Response.ContentType = "application/json";
        string action = context.Request["action"];

        switch (action)
        {
            case "upload": // 接收分片
                UploadChunk(context);
                break;
            case "merge": // 合并分片
                MergeChunks(context);
                break;
            default:
                context.Response.Write(JsonConvert.SerializeObject(new { code = 400, msg = "无效操作" }));
                break;
        }
    }

    // 接收分片并存储(带SM4加密)
    private void UploadChunk(HttpContext context)
    {
        string fileId = context.Request["fileId"];
        int chunkIndex = int.Parse(context.Request["chunkIndex"]);
        int totalChunks = int.Parse(context.Request["totalChunks"]);
        string fileName = context.Request["fileName"];

        // 校验分片是否已上传(防重复)
        if (IsChunkUploaded(fileId, chunkIndex))
        {
            context.Response.Write(JsonConvert.SerializeObject(new { code = 200, msg = "分片已上传" }));
            return;
        }

        // 读取分片内容(加密传输)
        HttpPostedFile chunkFile = context.Request.Files["chunk"];
        byte[] encryptedChunk = new byte[chunkFile.ContentLength];
        chunkFile.InputStream.Read(encryptedChunk, 0, chunkFile.ContentLength);

        // SM4解密分片(与前端加密对应)
        byte[] decryptedChunk = SM4Decrypt(encryptedChunk, _encryptKey);

        // 保存分片到临时目录
        string tempDir = Path.Combine(_uploadDir, fileId);
        if (!Directory.Exists(tempDir)) Directory.CreateDirectory(tempDir);
        string chunkPath = Path.Combine(tempDir, $"chunk_{chunkIndex}");
        File.WriteAllBytes(chunkPath, decryptedChunk);

        // 记录分片元数据到数据库(SQL Server)
        SaveChunkMetadata(fileId, chunkIndex, totalChunks, fileName);

        context.Response.Write(JsonConvert.SerializeObject(new { code = 200, msg = "分片上传成功" }));
    }

    // 合并分片并存储到目标路径
    private void MergeChunks(HttpContext context)
    {
        string fileId = context.Request["fileId"];
        string tempDir = Path.Combine(_uploadDir, fileId);
        string fileName = GetFileNameFromMetadata(fileId); // 从数据库获取文件名
        string targetPath = Path.Combine(HttpContext.Current.Server.MapPath("~/Uploads/"), fileName);

        // 合并所有分片(流式写入,避免内存溢出)
        using (FileStream fs = new FileStream(targetPath, FileMode.Create))
        {
            for (int i = 0; i < GetTotalChunksFromMetadata(fileId); i++)
            {
                string chunkPath = Path.Combine(tempDir, $"chunk_{i}");
                byte[] decryptedChunk = SM4Decrypt(File.ReadAllBytes(chunkPath), _encryptKey);
                fs.Write(decryptedChunk, 0, decryptedChunk.Length);
                File.Delete(chunkPath); // 删除临时分片
            }
        }

        // 清理临时目录
        Directory.Delete(tempDir);

        // 保存文件元数据到数据库(SQL Server)
        SaveFileMetadata(fileId, fileName, targetPath);

        context.Response.Write(JsonConvert.SerializeObject(new { code = 200, msg = "合并成功", filePath = targetPath }));
    }

    // SM4加密工具方法(示例,需引入SM4库)
    private byte[] SM4Encrypt(byte[] data, string key)
    {
        // 实际使用BouncyCastle或专用SM4库实现
        return data; // 简化示例
    }

    private byte[] SM4Decrypt(byte[] data, string key)
    {
        // 实际使用BouncyCastle或专用SM4库实现
        return data; // 简化示例
    }

    // 数据库操作(示例,使用SqlClient)
    private void SaveChunkMetadata(string fileId, int chunkIndex, int totalChunks, string fileName)
    {
        using (SqlConnection conn = new SqlConnection("Server=.;Database=FileDB;User Id=sa;Password=123456;"))
        {
            conn.Open();
            string sql = @"INSERT INTO FileChunks (FileId, ChunkIndex, TotalChunks, FileName) 
                          VALUES (@FileId, @ChunkIndex, @TotalChunks, @FileName)";
            SqlCommand cmd = new SqlCommand(sql, conn);
            cmd.Parameters.AddWithValue("@FileId", fileId);
            cmd.Parameters.AddWithValue("@ChunkIndex", chunkIndex);
            cmd.Parameters.AddWithValue("@TotalChunks", totalChunks);
            cmd.Parameters.AddWithValue("@FileName", fileName);
            cmd.ExecuteNonQuery();
        }
    }
}
2. 后端关键逻辑
  • 分片校验:通过数据库记录已上传的分片,避免重复上传。
  • 加密存储:分片传输时用SM4加密,后端解密后存储(密钥需安全管理,建议从配置中心获取)。
  • 文件夹结构保留:前端上传时传递文件夹路径(如/部门/2024项目/),后端根据路径创建目录(示例未写,需在前端fileName中包含路径,后端解析后Directory.CreateDirectory)。

四、部署与调试(IE8必看)

  1. 环境准备

    • 服务器装IIS,启用ASP.NET WebForm支持(.NET Framework 4.8)。
    • SQL Server创建数据库FileDB,执行以下脚本建表:
      CREATE TABLE FileChunks (
          Id INT PRIMARY KEY IDENTITY,
          FileId NVARCHAR(50) NOT NULL,
          ChunkIndex INT NOT NULL,
          TotalChunks INT NOT NULL,
          FileName NVARCHAR(255) NOT NULL,
          UNIQUE (FileId, ChunkIndex)
      );
      
      CREATE TABLE FileMetadata (
          FileId NVARCHAR(50) PRIMARY KEY,
          FileName NVARCHAR(255) NOT NULL,
          FilePath NVARCHAR(500) NOT NULL,
          UploadTime DATETIME DEFAULT GETDATE()
      );
      
  2. IE8兼容调试

    • 用虚拟机装Win7+IE8,测试上传功能(重点测试分片续传、文件夹路径)。
    • 前端fileInputwebkitdirectory属性在IE8无效,需手动输入文件夹路径(如/部门/2024项目/),后端解析后创建目录。

五、源代码与支持

以上代码是简化版,实际需要:

  • 引入SM4加密库(如BouncyCastle的C#版本)。
  • 处理IE8的File对象兼容问题(可能需要用ActiveXObject读取文件流)。
  • 优化分片上传的并发控制(IE8不支持Promise,用setTimeout模拟异步)。

需要完整源码(含加密、文件夹路径处理、SQL Server脚本),可以加群374992201,我免费发你(群里还有红包和接单资源)。

(PS:客户说“能跑起来就行”,但我争取做到“稳如老狗”——毕竟这是吃饭的本事,冲就完事了!)

设置框架

安装.NET Framework 4.7.2
https://dotnet.microsoft.com/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
相关参考:
文件保存位置

效果预览

文件上传

文件上传

文件刷新续传

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

文件夹上传

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

下载完整示例

下载完整示例

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐