本次,我们来实现一个单个大文件上传,并且把后台对上传文件的处理进度通过ASP.NET CORE SignalR反馈给前端展示,比如上传一个大的zip压缩包文件,后台进行解压缩,并且对压缩包中的文件进行md5校验,同时要求前台可以实时(实际情况看网络情况)展示后台对压缩包的处理进度(解压、校验文件)。

在前端上传文件的组件选择上,采用了WebUploader(http://fex.baidu.com/webuploader/)这个优秀的前端组件,下面是来自它的官网介绍:

WebUploader是由Baidu WebFE(FEX)团队开发的一个简单的以HTML5为主,FLASH为辅的现代文件上传组件。在现代的浏览器里面能充分发挥HTML5的优势,同时又不摒弃主流IE浏览器,沿用原来的FLASH运行时,兼容IE6+,iOS 6+, android 4+。两套运行时,同样的调用方式,可供用户任意选用。

采用大文件分片并发上传,极大的提高了文件上传效率。

WebUploader的功能很多,本次只使用它的上传前文件MD5校验、并发分片上传、分片MD5校验三个主要功能,分别来实现类似网盘中的文件【秒传】,浏览器多线程上传文件和文件的断点续传。

阅读参考此文章前,请先看一下https://blog.51cto.com/u_15127517/3321823

此文章是上一篇的功能扩展,一些基本的程序模块逻辑都已经在上一篇文章中做了介绍,这里就不再重复。

在正式使用WebUploader进行上传文件之前,先对它的执行流程和触发的事件做个大致的介绍(如有不对的地方请指正),我们可以通过它触发的事件来做相应的流程或业务上的预处理,比如文件秒传,重复文件检测等。

当WebUploader正确加载完成后,会触发它的ready事件;

当点击文件选择框的时候(其它方式传入文件所触发的事件请参考官方文档),会触发它的dialogOpen事件;

当选择文件完成后,触发事件的流程为:beforeFileQueued ==> fileQueued ==> filesQueued;

当点击(开始)上传的时候,触发事件的流程为:

1、正常文件上传流程

startUpload(如秒传(后台通过文件的md5判断返回)秒传则触发UploadSkip) ==> uploadStart ==> uploadBeforeSend ==> uploadProgress ==> uploadAccept(接收服务器处理分块传输后的返回信息) ==> uploadSuccess ==> uploadComplete ==> uploadFinished

2、文件秒传或续传流程

startUpload ==> uploadStart(触发秒传或文件续传) ==> uploadSkip ==> uploadSuccess ==> uploadComplete ==> uploadFinished

现在,我们在上一次项目的基础上做一些改造升级,最终实现我们本次的功能。

先看效果(GIF录制时间略长,请耐心等待一下)

ASP.NET CORE使用WebUploader对大文件分片上传,并通过ASP.NET CORE SignalR实时反馈后台处理进度给前端展示_WebUploader

首先,我们引用大名鼎鼎的WebUploader组件库。在项目上右键==>添加==>客户端库 的界面中选择unpkg然后输入webuploader 

ASP.NET CORE使用WebUploader对大文件分片上传,并通过ASP.NET CORE SignalR实时反馈后台处理进度给前端展示_WebUploader_02

为了实现压缩文件的解压缩操作,我们在Nuget中引用SharpZipLib组件

ASP.NET CORE使用WebUploader对大文件分片上传,并通过ASP.NET CORE SignalR实时反馈后台处理进度给前端展示_WebUploader_03

 然后我们在appsettings.json中增加一个配置用来保存上传文件。

 1 { 2   "Logging": { 3     "LogLevel": { 4       "Default": "Information", 5       "Microsoft": "Warning", 6       "Microsoft.Hosting.Lifetime": "Information" 7     } 8   }, 9   "FileUpload": {10     "TempPath": "temp",//临时文件保存目录11     "FileDir": "upload",//上传完成后的保存目录12     "FileExt": "zip,rar"//允许上传的文件类型13   },14   "AllowedHosts": "*"15 }

在项目中新建一个Model目录,用来实现上传文件的相关配置,建立相应的多个类文件 

FileUploadConfig.cs 服务器用来接受和保存文件的配置

 1 using System; 2  3 namespace signalr.Model 4 { 5     ///  6     /// 上传文件配置类 7     ///  8     [Serializable] 9     public class FileUploadConfig10     {11         /// 12         /// 临时文件夹目录名13         /// 14         public string TempPath { get; set; }15         /// 16         /// 上传文件保存目录名17         /// 18         public string FileDir { get; set; }19         /// 20         /// 允许上传的文件扩展名21         /// 22         public string FileExt { get; set; }23     }24 }

UploadFileWholeModel.cs 前台开始传输前会对文件进行一次MD5算法,这里可以通过文件MD5值传递给后台来通过比对已上传的文件MD5值列表来实现秒传功能

 1 namespace signalr.Model 2 { 3     ///  4     /// 文件秒传检测前台传递参数 5     ///  6     public class UploadFileWholeModel 7     { 8         ///  9         /// 请求类型,这里固定为:whole10         /// 11         public string CheckType { get; set; }12         /// 13         /// 文件的MD514         /// 15         public string FileMd5 { get; set; }16         /// 17         /// 前台文件的唯一标识18         /// 19         public string FileGuid { get; set; }20         /// 21         /// 前台上传文件名22         /// 23         public string FileName { get; set; }24         /// 25         /// 文件大小26         /// 27         public int? FileSize { get; set; }28     }29 }

UploadFileChunkModel.cs 前台文件分块传输的时候会对分块传输内容进行MD5计算,并且分块传输的时候会传递当前分块的一些信息,这里对应的后台接收实体类。

我们可以通过分块传输的MD5值来实现文件续传功能(如文件的某块MD5已存在则返回给前台跳过当前块)

 1 namespace signalr.Model 2 { 3     ///  4     /// 文件分块(续传)传递参数 5     ///  6     public class UploadFileChunkModel 7     { 8         ///  9         /// 文件分块传输检测类型,这里固定为chunk10         /// 11         public string CheckType { get; set; }12         /// 13         /// 文件的总大小14         /// 15         public long? FileSize { get; set; }16         /// 17         /// 当前块所属文件编号18         /// 19         public string FileId { get; set; }20         /// 21         /// 当前块基于文件的开始偏移量22         /// 23         public long? ChunkStart { get; set; }24         /// 25         /// 当前块基于文件的结束偏移量26         /// 27         public long? ChunkEnd { get; set; }28         /// 29         /// 当前块的大小30         /// 31         public long? ChunkSize { get; set; }32         /// 33         /// 当前块编号34         /// 35         public string ChunkIndex { get; set; }36         /// 37         /// 当前文件分块总数38         /// 39         public string ChunkCount { get; set; }40         /// 41         /// 当前块的编号42         /// 43         public string ChunkId { get; set; }44         /// 45         /// 当前块的md546         /// 47         public string Md5 { get; set; }48     }49 }

FormData.cs 这是分块传输时传递的当前块的信息配置

 1 using System; 2  3 namespace signalr.Model 4 { 5     ///  6     /// 上传文件时的附加信息 7     ///  8     [Serializable] 9     public class FormData10     {11         /// 12         /// 当前请求类型 分片传输是:chunk13         /// 14         public string Checktype { get; set; }15         /// 16         /// 文件总字节数17         /// 18         public int? Filesize { get; set; }19         /// 20         /// 文件唯一编号21         /// 22         public string Fileid { get; set; }23         /// 24         /// 分片数据大小25         /// 26         public int? Chunksize { get; set; }27         /// 28         /// 当前分片编号29         /// 30         public int? Chunkindex { get; set; }31         /// 32         /// 分片起始编译量33         /// 34         public int? Chunkstart { get; set; }35         /// 36         /// 分片结束编译量37         /// 38         public int? Chunkend { get; set; }39         /// 40         /// 分片总数量41         /// 42         public int? Chunkcount { get; set; }43         /// 44         /// 当前分片唯一编号45         /// 46         public string Chunkid { get; set; }47         /// 48         /// 当前块MD5值49         /// 50         public string Md5 { get; set; }51     }52 }

UploadFileModel.cs 每次上传文件的时候,前台都会传递这些参数给服务器,服务器可以根据参数做相应的处理

 1 using System; 2 using Microsoft.AspNetCore.Mvc; 3  4 namespace signalr.Model 5 { 6     ///  7     /// WebUploader上传文件实体类 8     ///  9     [Serializable]10     public class UploadFileModel11     {12         /// 13         /// 前台WebUploader的ID14         /// 15         public string Id { get; set; }16         /// 17         /// 当前文件(块)的前端计算的md518         /// 19         public string FileMd5 { get; set; }20         /// 21         /// 当前文件块号22         /// 23         public string Chunk { get; set; }24         /// 25         /// 原始文件名26         /// 27         public string Name { get; set; }28         /// 29         /// 文件类型(如:image/png)30         /// 31         [FromForm(Name = "type")]32         public string FileType { get; set; }33         /// 34         /// 当前文件(块)的大小35         /// 36         public long? Size { get; set; }37         /// 38         /// 前台给此文件分配的唯一编号39         /// 40         public string Guid { get; set; }41         /// 42         /// 附件信息43         /// 44         public FormData FromData { get; set; }45         /// 46         /// Post过来的数据容器47         /// 48         public byte[] FileData { get; set; }49     }50 }

UploadFileMergeModel.cs 当所有块传输完成后,传递给后台一个合并文件的请求,后台通过参数中的信息把分块保存的文件合并成一个完整的文件

 1 namespace signalr.Model 2 { 3     ///  4     /// 文件合并请求参数类 5     ///  6     public class UploadFileMergeModel 7     { 8         ///  9         /// 请求类型10         /// 11         public string CheckType { get; set; }12         /// 13         /// 前台检测到的文件大小14         /// 15         public long? FileSize { get; set; }16         /// 17         /// 前台返回文件总块数18         /// 19         public int? ChunkNumber { get; set; }20         /// 21         /// 前台返回文件的md5值22         /// 23         public string FileMd5 { get; set; }24         /// 25         /// 前台返回上传文件唯一标识26         /// 27         public string FileName { get; set; }28         /// 29         /// 文件扩展名,不包含.30         /// 31         public string FileExt { get; set; }32     }33 }

为了实现【秒传】和分块传输时的【断点续传】功能,我们在Class目录中定义一个UploadFileList.cs类,用来模拟持久化保存服务器所接收到的文件MD5校验列表和已接收的分块MD5值信息,这里我们使用了并发线程安全的ConcurrentDictionary和ConcurrentBag

 1 using System; 2 using System.Collections.Concurrent; 3  4 namespace signalr.Class 5 { 6     public class UploadFileList 7     { 8         private static readonly Lazy<ConcurrentDictionary<string, string>> _serverUploadFileList = new Lazy<ConcurrentDictionary<string, string>>(); 9         private static readonly Lazy<ConcurrentDictionary<string, ConcurrentBag<string>>> _uploadChunkFileList =10             new Lazy<ConcurrentDictionary<string, ConcurrentBag<string>>>();11         public UploadFileList()12         {13             ServerUploadFileList = _serverUploadFileList;14             UploadChunkFileList = _uploadChunkFileList;15         }16 17         /// 18         /// 服务器上已经存在的文件,key为文件的Md5,value为文件路径19         /// 20         public readonly Lazy<ConcurrentDictionary<string, string>> ServerUploadFileList;21         /// 22         /// 客户端分配上传文件时的记录信息,key为上传文件的唯一id,value为文件分片后的当前段的md523         /// 24         public readonly Lazy<ConcurrentDictionary<string, ConcurrentBag<string>>> UploadChunkFileList;25     }26 }

扩展一下HubInterface/IChatClient.cs 用来推送给前台展示后台处理的信息

public interface IChatClient
    {        /// 
        /// 客户端接收数据触发函数名        /// 
        /// 消息实体类
        ///         Task ReceiveMessage(ClientMessageModel clientMessageModel);        /// 
        /// Echart接收数据触发函数名        /// 
        /// JSON格式的可以被Echarts识别的data数据
        ///         Task EchartsMessage(Array data);        /// 
        /// 客户端获取自己登录后的UID        /// 
        /// 消息实体类
        ///         Task GetMyId(ClientMessageModel clientMessageModel);        /// 
        /// 上传成功后服务器处理数据时通知前台的信息内容        /// 
        /// 消息实体类
        ///         Task UploadInfoMessage(ClientMessageModel clientMessageModel);
    }

扩展一下Class/ClientMessageModel.cs

    /// 
    /// 服务端发送给客户端的信息    ///     [Serializable]    public class ClientMessageModel
    {        /// 
        /// 接收用户编号        /// 
        public string UserId { get; set; }        /// 
        /// 组编号        /// 
        public string GroupName { get; set; }        /// 
        /// 发送的内容        /// 
        public string Context { get; set; }        /// 
        /// 自定义的响应编码        /// 
        public string Code { get; set; }
    }

我们在Startup.cs中注入上传文件的配置,同时把前文的XSRF防护去掉,我们在前台请求的时候带上防护认证信息。

public void ConfigureServices(IServiceCollection services)
        {
            services.AddSignalR();
            services.AddRazorPages()
            services.AddSingleton();//服务器上传的文件信息保存在内存中            services.AddOptions()
                .Configure(Configuration.GetSection("FileUpload"));//服务器上传文件配置
        }

在项目的wwwroot/js下新建一个uploader.js

ASP.NET CORE使用WebUploader对大文件分片上传,并通过ASP.NET CORE SignalR实时反馈后台处理进度给前端展示_WebUploader_04 

"use strict";var connection = new signalR.HubConnectionBuilder()
    .withUrl("/chatHub")
    .withAutomaticReconnect()
    .configureLogging(signalR.LogLevel.Debug)
    .build();var user = "";

connection.on("GetMyId", function (data) {
    user = data.userId;
});
connection.on("ReceiveMessage", function (data) {
    console.log(data.userId + data.context);
});

connection.on("UploadInfoMessage", function (data) {    switch (data.code) {    case "200":
        $('.modal-body').append($("" + data.context + ""));//当后台返回处理完成或出错时,前台显示内容,同时显示关闭按钮
        $(".modal-content").append($("Close"));        break;    case "300":    case "500":
        $('.modal-body').append($("" + data.context + ""));//展示后台返回信息        break;    case "400":        if ($("#process").length == 0) {//展示后台推送的文件处理进度
            $('.modal-body').append($("" + data.context + ""));
        }
        $('#process').text(data.context);        break;
    }
});

connection.start().then(function () {
    console.log("服务器已连接");
}).catch(function (err) {    return console.error(err.toString());
});

在项目的Pages/Shared中新建一个Razor布局页_LayoutUpload.cshtml

ASP.NET CORE使用WebUploader对大文件分片上传,并通过ASP.NET CORE SignalR实时反馈后台处理进度给前端展示_WebUploader_05

<!DOCTYPE html><html><head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width" />
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/lib/webuploader/dist/webuploader.css" />
    <script type="text/javascript" src="~/lib/jquery/dist/jquery.min.js">script>
    <script type="text/javascript" src="~/lib/webuploader/dist/webuploader.js">script>
    <script type="text/javascript" src="~/lib/bootstrap/dist/js/bootstrap.min.js">script>
    <title>@ViewBag.Titletitle>
    @await RenderSectionAsync("Scripts", required: false)head><body>@RenderBody()body>html>

在Pages目录下新建一个upload目录,然后在它下面新建一个index.cshtml,这个文件中实现了Webuploader中我们所要使用的事件监测、文件上传功能。

  1 @page "{handler?}"  2 @model MediatRStudy.Pages.upload.IndexModel  3 @{  4     ViewBag.Title = "WebUploader";  5     Layout = "_LayoutUpload";  6 }  7 @section Scripts  8 {  9 <script src="~/js/signalr/dist/browser/signalr.js">script> 10 <script src="~/js/uploader.js">script> 11  12 <script> 13     // 每次分片文件大小限制为5M 14     var chunkSize = 5 * 1024 * 1024; 15     // 全部文件限制10G大小 16     var fileTotalSize = 10 * 1024 * 1024 * 1024; 17     // 单文件限制5G大小 18     var fileSingleSize = 5 * 1024 * 1024 * 1024; 19     jQuery(function() { 20         var $ = jQuery, 21             $list = $('#thelist'), 22             $btn = $('#ctlBtn'), 23             state = 'pending', 24             md5s = {},//分块传输时的各个块的md5值 25             dataState,//当前状态 26             Token,//可以做用户验证 27             uploader;//webUploader的实例 28         var fileExt = ["zip", "rar"];//允许上传的类型 29         Token = '@ViewData["Token"]'; 30         if (Token == '' || Token == 'undefined') { 31             $("#uploader").hide(); 32             alert("登录超时,请重新登录。"); 33         } 34  35  36  37  38         //注册Webuploader要监听的上传文件时的三个事件 39         //before-send-file 在执行文件上传前先执行这个;before-send在开始往服务器发送文件前执行;after-send-file所有文件上传完毕后执行 40  41         window.WebUploader.Uploader.register({ 42                 "before-send-file": "beforeSendFile", 43                 "before-send": "beforeSend", 44                 "after-send-file": "afterSendFile" 45             }, 46             { 47                 //第一步,开始上传前校验文件,并传递给服务器当前文件的MD5,服务器可根据MD5来实现类似秒传效果 48                 beforeSendFile: function(file) { 49                     var owner = this.owner; 50                     md5s.length = 0; 51                     var deferred = window.WebUploader.Deferred(); 52                     owner.md5File(file, 0, file.size) 53                         .progress(function(percentage) { 54                             console.log("文件MD5计算进度:", percentage); 55                         }) 56                         .fail(function() { 57                             deferred.reject(); 58                             console.log("文件MD5获取失败"); 59                         }) 60                         .then(function(md5) { 61                             console.log("文件MD5:", md5); 62                             file.md5 = md5; 63                             var params = { 64                                 "checktype": "whole", 65                                 "filesize": file.size, 66                                 "filemd5": md5 67                                 ,"filename":file.name 68                                 ,"fileguid":file.guid 69                             }; 70                             $.ajax({ 71                                 url: '/upload/FileWhole', //通过md5校验实现文件秒传 72                                 type: 'POST', 73                                 headers: {//请求的时候传递进去防CSRF攻击的认证信息 74                                     RequestVerificationToken: 75                                         $('input:hidden[name="__RequestVerificationToken"]').val() 76                                 }, 77                                 data: params, 78                                 contentType: 'application/x-www-form-urlencoded', 79                                 async: true, // 开启异步请求 80                                 dataType: 'JSON', 81                                 success: function(data) { 82                                     data = (typeof data) == 'string' ? JSON.parse(data) : data; 83                                     if (data.code != '200') { 84                                         dataState = data; 85                                         //服务器返回错误信息 86                                         alert('错误:' + data.msg); 87                                         deferred.reject();//取消后续上传 88                                     } 89                                     if (data.isExist) { 90                                         // 跳过当前文件并标记文件状态为上传完成 91                                         dataState = data; 92                                         owner.skipFile(file, window.WebUploader.File.Status.COMPLETE); 93                                         deferred.resolve(); 94                                         $('#' + file.id).find('p.state').text('上传成功【秒传】'); 95  96                                     } else { 97                                         deferred.resolve(); 98                                     } 99                                 },100                                 error: function(xhr, status) {101                                     $('#' + file.id).find('p.state').text('上传失败:'+status);102                                     console.log("上传失败:", status);103                                 }104                             });105                         });106 107                     return deferred.promise();108                 },109                 //上传事件第二步:分块上传时,每个分块触发上传前执行110                 beforeSend: function(block) {111                     var deferred = window.WebUploader.Deferred();112                     var owner = this.owner;113                     owner.md5File(block.file, block.start, block.end)114                         .progress(function(percentage) {115                             console.log("当前分块内容的MD5计算进度:", percentage);116                         })117                         .fail(function() {118                             deferred.reject();119                         })120                         .then(function(md5) {121                             //计算当前块的MD5值并写入数组122                             md5s[block.blob.uid] = md5;123                             deferred.resolve();124                         });125                     return deferred.promise();126                 },127                 //时间点3:所有分块上传成功后调用此函数128                 afterSendFile: function(file) {129                     var deferred = $.Deferred();130                     $('#' + file.id).find('p.state').text('执行最后一步');131                     console.log(file);132                     if (file.skipped) {133                         deferred.resolve();134                         console.log("执行服务器合并分块文件操作");135                         return deferred.promise();136                     }137                     var chunkNumber = Math.ceil(file.size / chunkSize);//总块数138                     var params = {139                         "checktype": "merge",140                         "filesize": file.size,141                         "chunknumber": chunkNumber,142                         "filemd5": file.md5,143                         "filename": file.guid,144                         "fileext": file.ext//扩展名145                     };146                     $.ajax({147                         type: "POST",148                         url: "/upload/FileMerge",149                         headers: {150                             RequestVerificationToken:151                                 $('input:hidden[name="__RequestVerificationToken"]').val(),152                             userid:user //传递SignalR分配的编号153                         },154                         data: params,155                         async: true,156                         success: function(response) {157                             if (response.code == 200) {158                                 //服务器合并完成分块传输的文件后执行159                                 dataState = response;160                                 $("#myModal").modal('show');161                             } else {162                                 alert(response.msg);163                             }164                             deferred.resolve();165                         },166                         error: function() {167                             dataState = undefined;168                             deferred.reject();169                         }170                     });171                     return deferred.promise();172                 }173             });174         uploader = window.WebUploader.create({175             resize: false,176             fileNumLimit: 1,177             swf: '/lib/webuploader/dist/Uploader.swf',178             server: '/upload/FileSave',179             pick: { id: '#picker', multiple: false },180             chunked: true,181             chunkSize: chunkSize,182             chunkRetry: 3,183             fileSizeLimit: fileTotalSize,184             fileSingleSizeLimit: fileSingleSize,185             formData: {186             }187         });188         uploader.on('beforeFileQueued',189             function(file) {190                 var isAdd = false;191                 for (var i = 0; i < fileExt.length; i++) {192                     if (file.ext == fileExt[i]) {193                         file.guid = window.WebUploader.Base.guid();194                         isAdd = true;195                         break;196                     }197                 }198                 return isAdd;199             });200         //每次上传前,如果分块传输,则带上分块信息参数201         uploader.on('uploadBeforeSend',202             function(block, data, headers) {203                 var params = {204                     "checktype": "chunk",205                     "filesize": block.file.size,206                     "fileid": block.blob.ruid,207                     "chunksize": block.blob.size,208                     "chunkindex": block.chunk,209                     "chunkstart": block.start,210                     "chunkend": block.end,211                     "chunkcount": block.chunks,212                     "chunkid": block.blob.uid,213                     "md5": md5s[block.blob.uid]214                 };215                 data.formData = JSON.stringify(params);216 217                 headers.Authorization = Token;218                 headers.RequestVerificationToken = $('input:hidden[name="__RequestVerificationToken"]').val();219                 data.guid = block.file.guid;220             });221         // 当有文件添加进来的时候222         uploader.on('fileQueued',223             function(file) {224                 $list.append('<div id="' +225                     file.id +226                     '" class="item">' +227                     '<h4 class="info">' +228                     file.name +229                     'h4>' +230                     '<input type="hidden" id="h_' +231                     file.id +232                     '" value="' +233                     file.guid +234                     '" />' +235                     '<p class="state">等待上传...p>' +236                     'div>');237             });238 239         // 文件上传过程中创建进度条实时显示。240         uploader.on('uploadProgress',241             function(file, percentage) {242                 var $li = $('#' + file.id),243                     $percent = $li.find('.progress .progress-bar');244                 // 避免重复创建245                 if (!$percent.length) {246                     $percent = $('<div class="progress progress-striped active">' +247                         '<div class="progress-bar" role="progressbar" style="width: 0%">' +248                         'div>' +249                         'div>').appendTo($li).find('.progress-bar');250                 }251                 $li.find('p.state').text('上传中');252 253                 $percent.css('width', percentage * 100 + '%');254             });255 256         uploader.on('uploadSuccess',257             function(file) {258                 if (dataState == undefined) {259                     $('#' + file.id).find('p.state').text('上传失败');260                     $('#' + file.id).find('button').remove();261                     $('#' + file.id).find('p.state').before('<button id="retry" type="button" class="btn btn-primary fright retry pbtn">重新上传button>');262                     file.setStatus('error');263                     return;264                 }265                 if (dataState.success == true) {266                     if (dataState.miaochuan == true) {267                         $('#' + file.id).find('p.state').text('上传成功[秒传]');268                     } else {269                         $('#' + file.id).find('p.state').text('上传成功');270                     }271                     $('#' + file.id).find('button').remove();272                     return;273 274                 } else {275                     $('#' + file.id).find('p.state').text('服务器未能成功接收,状态:' + dataState.success);276                     return;277                 }278             });279 280         uploader.on('uploadError',281             function(file) {282                 $('#' + file.id).find('p.state').text('上传出错');283             });284         //分块传输后,可以在这个事件中获取到服务器返回的信息,同时这里可以实现文件续传(块文件的MD5存在时,后台可以跳过保存步骤)285         uploader.on('uploadAccept',286             function(file, response, reject) {287                 if (response.code !== 200) {288                     alert("上传出错:" + response.msg);289                     return false;290                 }291                 return true;292             });293         uploader.on('uploadComplete',294             function(file) {295                 $('#' + file.id).find('.progress').fadeOut();296             });297 298         uploader.on('all',299             function(type) {300                 if (type === 'startUpload') {301                     state = 'uploading';302                 } else if (type === 'stopUpload') {303                     state = 'paused';304                 } else if (type === 'uploadFinished') {305                     state = 'done';306                 }307                 if (state === 'done') {308                     $btn.text('继续上传');309                 } else if (state === 'uploading') {310                     $btn.text('暂停上传');311                 } else {312                     $btn.text('开始上传');313                 }314             });315         $btn.on('click',316             function() {317                 if (state === 'uploading') {318                     uploader.stop();319                 } else if (state == 'done') {320                     window.location.reload();321                 } else {322                     uploader.upload();323                 }324             });325     });326 script>327 }328 <div class="container">329     <div class="row">330         <div id="uploader" class="wu-example">331             <span style="color: red">请上传压缩包span>332             <div class="form-group" id="thelist">333             div>334             <div class="form-group">335                 <form method="post">336                     <div id="picker" class="webuploader-container">337                         <div class="webuploader-pick">选择文件div>338                         <div style="position: absolute; top: 0; left: 0; width: 88px; height: 34px; overflow: hidden; bottom: auto; right: auto;">339                             <input type="file" name="file" class="webuploader-element-invisible" />340                             <label style="-ms-opacity: 0; opacity: 0; width: 100%; height: 100%; display: block; cursor: pointer; background: rgb(255, 255, 255);">label>341                         div>342                     div>343                     <button id="ctlBtn" class="btn btn-success" type="button">开始上传button>344                 form>345             div>346         div>347     div>348 div>349 350 <div class="modal fade" id="myModal" tabindex="-1" aria-labelledby="exampleModalScrollableTitle" style="display: none;" data-backdrop="static" aria-hidden="true">351     <div class="modal-dialog modal-dialog-scrollable">352         <div class="modal-content">353             <div class="modal-header">354                 <h5 class="modal-title" id="exampleModalScrollableTitle">正在处理。。。h5>355                 <button type="button" class="close" data-dismiss="modal" aria-label="Close">356         357                 button>358             div>359             <div class="modal-body">360                 <p>服务器正在处理数据,请不要关闭和刷新此页面。p>361             div>362         div>363     div>364 div>

index.cshtml的代码文件如下

本示例只能解压缩zip文件,并且密码是123456,友情提示,不要用QQ浏览器调试,否则会遇到选择文件后DEBUG停止运行。

本示例只能解压缩zip文件,并且密码是123456,友情提示,不要用QQ浏览器调试,否则会遇到选择文件后DEBUG停止运行。

本示例只能解压缩zip文件,并且密码是123456,友情提示,不要用QQ浏览器调试,否则会遇到选择文件后DEBUG停止运行。 

ASP.NET CORE使用WebUploader对大文件分片上传,并通过ASP.NET CORE SignalR实时反馈后台处理进度给前端展示_WebUploader_06ASP.NET CORE使用WebUploader对大文件分片上传,并通过ASP.NET CORE SignalR实时反馈后台处理进度给前端展示_WebUploader_07

  1 using ICSharpCode.SharpZipLib.Zip;  2 using Microsoft.AspNetCore.Http;  3 using Microsoft.AspNetCore.Mvc;  4 using Microsoft.AspNetCore.Mvc.RazorPages;  5 using Microsoft.AspNetCore.SignalR;  6 using Microsoft.Extensions.Options;  7 using signalr.Class;  8 using signalr.HubInterface;  9 using signalr.Hubs; 10 using signalr.Model; 11 using System; 12 using System.Collections.Concurrent; 13 using System.Diagnostics; 14 using System.IO; 15 using System.Linq; 16 using System.Text.Json; 17 using System.Threading.Tasks; 18  19 namespace signalr.Pages.upload 20 { 21     public class IndexModel : PageModel 22     { 23         private readonly IOptionsSnapshot _fileUploadConfig; 24         private readonly IOptionsSnapshot _fileList; 25         private readonly string[] _fileExt; 26         private readonly IHubContext _hubContext; 27         public IndexModel(IOptionsSnapshotfileUploadConfig, IOptionsSnapshotfileList, IHubContext hubContext) 28         { 29             _fileUploadConfig = fileUploadConfig; 30             _fileList = fileList; 31             _fileExt = _fileUploadConfig.Value.FileExt.Split(',').ToArray(); 32             _hubContext = hubContext; 33         } 34         public IActionResult OnGet() 35         { 36             ViewData["Token"] = "666"; 37             return Page(); 38         } 39  40         #region 上传文件 41  42         ///  43         /// 上传文件 44         ///  45         ///  46         public async Task OnPostFileSaveAsync(IFormFile file, UploadFileModel model) 47         { 48             if (_fileUploadConfig.Value == null) 49             { 50                 return new JsonResult(new { code = 400, msg = "服务器配置不正确" }); 51             } 52  53             if (file == null || file.Length < 1) 54             { 55                 return new JsonResult(new { code = 404, msg = "没有接收到要保存的文件" }); 56             } 57             Request.EnableBuffering(); 58             var formData = Request.Form["formData"]; 59             if (model == null || string.IsNullOrWhiteSpace(formData)) 60             { 61                 return new JsonResult(new { code = 401, msg = "没有接收到必要的参数" }); 62             } 63  64             var request = model; 65             long.TryParse(Request.Form["size"], out var fileSize); 66             request.Size = fileSize; 67             try 68             { 69                 request.FromData = JsonSerializer.Deserialize(formData, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); 70             } 71             catch (Exception e) 72             { 73                 Debug.WriteLine(e); 74             } 75  76             if (request.FromData == null) 77             { 78                 return new JsonResult(new { code = 402, msg = "参数错误" }); 79             } 80  81 #if DEBUG 82             Debug.WriteLine($"原文件名:{request.Name},文件编号:{request.Guid},文件块编号:{request.Chunk},文件Md5:{request.FileMd5},当前块UID:{request.FromData?.Chunkid},当前块MD5:{request.FromData?.Md5}"); 83 #endif 84             var fileExt = request.Name.Substring(request.Name.LastIndexOf('.') + 1).ToLowerInvariant(); 85             if (!_fileExt.Contains(fileExt)) 86             { 87                 return new JsonResult(new { code = 403, msg = "文件类型不在允许范围内" }); 88             } 89             if (_fileList.Value.UploadChunkFileList.Value.ContainsKey(request.Guid)) 90             { 91                 if (!_fileList.Value.UploadChunkFileList.Value[request.Guid].Any(x => string.Equals(x, request.FromData.Md5, StringComparison.OrdinalIgnoreCase))) 92                 { 93                     _fileList.Value.UploadChunkFileList.Value[request.Guid].Add(request.FromData.Md5); 94                 } 95 #if DEBUG 96                 else 97                 { 98                     Debug.WriteLine($"ContainsKey{request.FromData.Chunkindex}存在校验值{request.FromData.Md5}"); 99                     return new JsonResult(new { code = 200, msg = "成功接收", miaochuan = true });100                 }101 #endif102             }103             else104             {105                 return new JsonResult(new { code = 405, msg = "接收失败,因为服务器没有找到此文件的容器,请重新上传" });106             }107 108             var dirPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _fileUploadConfig.Value.TempPath, request.Guid);109             if (!Directory.Exists(dirPath))110             {111                 Directory.CreateDirectory(dirPath);112             }113 114             var tempFile = string.Concat(dirPath, "\\", request.FromData.Chunkindex.ToString().PadLeft(4, '0'), ".", fileExt);115             try116             {117 118                 await using var fs = System.IO.File.OpenWrite(tempFile);119                 request.FileData = new byte[Convert.ToInt32(request.FromData.Chunksize ?? 0)];120 121                 await using var memStream = new MemoryStream();122                 await file.CopyToAsync(memStream);123 124                 request.FileData = memStream.ToArray();125 126                 await fs.WriteAsync(request.FileData, 0, request.FileData.Length);127                 await fs.FlushAsync();128             }129             catch (Exception e)130             {131 #if DEBUG132                 Debug.WriteLine($"White Error:{e}");133 #endif134                 _fileList.Value.UploadChunkFileList.Value.TryRemove(request.Guid, out _);135             }136             return new JsonResult(new { code = 200, msg = "成功接收", miaochuan = false });137         }138 139         #endregion140 141         #region 合并上传文件142 143         /// 144         /// 合并分片上传的文件145         /// 146         /// 前台传递的请求合并的参数147         /// 148         public async Task OnPostFileMergeAsync(UploadFileMergeModel mergeModel)149         {150             return await Task.Run(async () =>151             {152                 if (mergeModel == null || string.IsNullOrWhiteSpace(mergeModel.FileName) ||153                     string.IsNullOrWhiteSpace(mergeModel.FileMd5))154                 {155                     return new JsonResult(new { code = 300, success = false, count = 0, size = 0, msg = "合并失败,参数不正确。" });156                 }157                 if (!_fileExt.Contains(mergeModel.FileExt.ToLowerInvariant()))158                 {159                     return new JsonResult(new { code = 403, success = false, msg = "文件类型不在允许范围内" });160                 }161 162                 var fileSavePath = "";163                 if (!_fileList.Value.ServerUploadFileList.Value.ContainsKey(mergeModel.FileMd5))164                 {165                     //合并块文件、删除临时文件166                     var chunks = Directory.GetFiles(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _fileUploadConfig.Value.TempPath, mergeModel.FileName), "*.*");167                     if (!chunks.Any())168                     {169                         return new JsonResult(new { code = 302, success = false, count = 0, size = 0, msg = "未找到文件块信息,请重试。" });170                     }171                     var dirPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _fileUploadConfig.Value.FileDir);172                     if (!Directory.Exists(dirPath))173                     {174                         Directory.CreateDirectory(dirPath);175                     }176                     fileSavePath = Path.Combine(_fileUploadConfig.Value.FileDir,177                         string.Concat(mergeModel.FileName, ".", mergeModel.FileExt));178                     await using var fs =179                         new FileStream(Path.Combine(dirPath, string.Concat(mergeModel.FileName, ".", mergeModel.FileExt)), FileMode.Create);180                     foreach (var file in chunks.OrderBy(x => x))181                     {182                         //Debug.WriteLine($"File==>{file}");183                         var bytes = await System.IO.File.ReadAllBytesAsync(file);184                         await fs.WriteAsync(bytes.AsMemory(0, bytes.Length));185                     }186                     //Directory.Delete(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _fileUploadConfig.Value.TempPath, mergeModel.FileName), true);187 188 189                     if (!_fileList.Value.ServerUploadFileList.Value.TryAdd(mergeModel.FileMd5, fileSavePath))190                     {191                         return new JsonResult(new { code = 301, success = false, count = 0, size = 0, msg = "服务器保存文件失败,请重试。" });192                     }193                 }194                 var user = Request.Headers["userid"];195                 //调用解压文件196                 if (string.Equals(mergeModel.FileExt.ToLowerInvariant(), "zip"))197                 {198                     DoUnZip(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileSavePath), user.ToString());199                 }200                 else201                 {202                     await SentMessage(user.ToString(), "服务器只能解压缩zip格式文件。", "200");203                 }204                 return new JsonResult(new { code = 200, success = true, count = 0, size = 0, msg = "上传成功", url = fileSavePath });205             });206 207         }208 209         #endregion210 211         #region 文件秒传检测、文件类型允许范围检测212         public JsonResult OnPostFileWholeAsync(UploadFileWholeModel model)213         {214             if (model == null || string.IsNullOrWhiteSpace(model.FileMd5))215             {216                 return new JsonResult(new { Code = 300, IsExist = false, success = false, FileUrl = "", Msg = "参数不正确" });217             }218             var fileExt = model.FileName.Substring(model.FileName.LastIndexOf('.') + 1).ToLowerInvariant();219             if (!_fileExt.Contains(fileExt))220             {221                 return new JsonResult(new { code = 403, success = false, msg = "文件类型不在允许范围内" });222             }223             if (_fileList.Value.ServerUploadFileList.Value.ContainsKey(model.FileMd5))224             {225                 return new JsonResult(new { Code = 200, IsExist = true, success = true, FileUrl = _fileList.Value.ServerUploadFileList.Value[model.FileMd5], miaochuan = true });226             }227             //检测的时候创建待上传文件的分块MD5容器228             _fileList.Value.UploadChunkFileList.Value.TryAdd(model.FileGuid, new ConcurrentBag<string>());229 230             return new JsonResult(new { Code = 200, IsExist = false, FileUrl = "" });231         }232         #endregion233 234         #region 文件块秒传检测235         public JsonResult OnPostFileChunkAsync(UploadFileChunkModel model)236         {237             if (model == null || string.IsNullOrWhiteSpace(model.Md5) || string.IsNullOrWhiteSpace(model.FileId))238             {239                 return new JsonResult(new { Code = 300, IsExist = false, success = false, FileUrl = "", Msg = "参数不正确" });240             }241 242             if (!_fileList.Value.UploadChunkFileList.Value.ContainsKey(model.FileId))243             {244                 return new JsonResult(new { Code = 200, IsExist = false, FileUrl = "" });245             }246 247             if (!_fileList.Value.UploadChunkFileList.Value[model.FileId].Contains(model.Md5))248             {249                 return new JsonResult(new { Code = 200, IsExist = false, FileUrl = "" });250             }251             return new JsonResult(new { Code = 200, IsExist = true, success = true, miaochuan = true });252         }253         #endregion254 255         #region 解压、校验文件256 257         private void DoUnZip(string zipFile, string user)258         {259             Task.Factory.StartNew(async () =>260             {261                 if (!System.IO.File.Exists(zipFile))262                 {263                     //发送一条文件不存在的消息264                     await SentMessage(user, "访问上传的压缩包失败");265                     return;266                 }267                 var fastZip = new FastZip268                 {269                     Password = "123456",270                     CreateEmptyDirectories = true271                 };272                 try273                 {274                     var zipExtDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ZipEx", "601018");275                     //删除现有文件夹276                     if (Directory.Exists(zipExtDir))277                         Directory.Delete(zipExtDir, true);278                     //发送开始解压缩信息279                     await SentMessage(user, "开始解压缩文件。。。");280 #if DEBUG281                     Debug.WriteLine("开始解压缩文件。。。");282 #endif283                     fastZip.ExtractZip(zipFile, zipExtDir, "");284 #if DEBUG285                     Debug.WriteLine("解压缩文件成功。。。");286 #endif287                     await SentMessage(user, "解压缩文件成功,开始校验。。。");288                     //发送解压成功并开始校验文件信息289                     var zipFiles = Directory.GetFiles(zipExtDir, "*.jpg", SearchOption.AllDirectories);290                     for (var i = 0; i < zipFiles.Length; i++)291                     {292                         var file = zipFiles[i];293                         var i1 = i + 1;294                         await Task.Delay(100);//模拟文件处理需要100毫秒295                         //发送进度 i/length296                         await SentMessage(user, $"校验进度==>{i1}/{zipFiles.Length}", "400");297 #if DEBUG298                         Debug.WriteLine($"当前进度:{i1},总数:{zipFiles.Length}");299 #endif300                     }301                     await SentMessage(user, "校验完成", "200");302                 }303                 catch (Exception exception)304                 {305                     //发送解压缩失败信息306                     await SentMessage(user, $"解压缩文件失败:{exception}", "500");307 #if DEBUG308                     Debug.WriteLine($"解压缩文件失败:{exception}");309 #endif310                 }311             }, TaskCreationOptions.LongRunning);312         }313 314         #endregion315 316         #region 消息推送前台317 318         private async Task SentMessage(string user, string content, string code = "300")319         {320 321             await _hubContext.Clients.Client(user).UploadInfoMessage(new ClientMessageModel322             {323                 UserId = user,324                 GroupName = "upload",325                 Context = content,326                 Code = code327             });328         }329 330         #endregion331     }332 }

View Code

未能完善的地方:

1、上传几百兆或更大的文件,webuploader计算md5时间太长;

2、后台处理错误的时候,前台接收消息后没能出现关闭按钮;

3、分块传输时文件断点续传没有具体实现(理论上是没问题的)