zoukankan      html  css  js  c++  java
  • ASP.NET CORE使用WebUploader对大文件分片上传,并通过ASP.NET CORE SignalR实时反馈后台处理进度给前端展示

    本次,我们来实现一个单个大文件上传,并且把后台对上传文件的处理进度通过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://www.cnblogs.com/wdw984/p/14645614.html

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

    在正式使用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录制时间略长,请耐心等待一下)

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

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

     然后我们在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     /// <summary>
     6     /// 上传文件配置类
     7     /// </summary>
     8     [Serializable]
     9     public class FileUploadConfig
    10     {
    11         /// <summary>
    12         /// 临时文件夹目录名
    13         /// </summary>
    14         public string TempPath { get; set; }
    15         /// <summary>
    16         /// 上传文件保存目录名
    17         /// </summary>
    18         public string FileDir { get; set; }
    19         /// <summary>
    20         /// 允许上传的文件扩展名
    21         /// </summary>
    22         public string FileExt { get; set; }
    23     }
    24 }

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

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

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

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

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

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

     1 using System;
     2 
     3 namespace signalr.Model
     4 {
     5     /// <summary>
     6     /// 上传文件时的附加信息
     7     /// </summary>
     8     [Serializable]
     9     public class FormData
    10     {
    11         /// <summary>
    12         /// 当前请求类型 分片传输是:chunk
    13         /// </summary>
    14         public string Checktype { get; set; }
    15         /// <summary>
    16         /// 文件总字节数
    17         /// </summary>
    18         public int? Filesize { get; set; }
    19         /// <summary>
    20         /// 文件唯一编号
    21         /// </summary>
    22         public string Fileid { get; set; }
    23         /// <summary>
    24         /// 分片数据大小
    25         /// </summary>
    26         public int? Chunksize { get; set; }
    27         /// <summary>
    28         /// 当前分片编号
    29         /// </summary>
    30         public int? Chunkindex { get; set; }
    31         /// <summary>
    32         /// 分片起始编译量
    33         /// </summary>
    34         public int? Chunkstart { get; set; }
    35         /// <summary>
    36         /// 分片结束编译量
    37         /// </summary>
    38         public int? Chunkend { get; set; }
    39         /// <summary>
    40         /// 分片总数量
    41         /// </summary>
    42         public int? Chunkcount { get; set; }
    43         /// <summary>
    44         /// 当前分片唯一编号
    45         /// </summary>
    46         public string Chunkid { get; set; }
    47         /// <summary>
    48         /// 当前块MD5值
    49         /// </summary>
    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     /// <summary>
     7     /// WebUploader上传文件实体类
     8     /// </summary>
     9     [Serializable]
    10     public class UploadFileModel
    11     {
    12         /// <summary>
    13         /// 前台WebUploader的ID
    14         /// </summary>
    15         public string Id { get; set; }
    16         /// <summary>
    17         /// 当前文件(块)的前端计算的md5
    18         /// </summary>
    19         public string FileMd5 { get; set; }
    20         /// <summary>
    21         /// 当前文件块号
    22         /// </summary>
    23         public string Chunk { get; set; }
    24         /// <summary>
    25         /// 原始文件名
    26         /// </summary>
    27         public string Name { get; set; }
    28         /// <summary>
    29         /// 文件类型(如:image/png)
    30         /// </summary>
    31         [FromForm(Name = "type")]
    32         public string FileType { get; set; }
    33         /// <summary>
    34         /// 当前文件(块)的大小
    35         /// </summary>
    36         public long? Size { get; set; }
    37         /// <summary>
    38         /// 前台给此文件分配的唯一编号
    39         /// </summary>
    40         public string Guid { get; set; }
    41         /// <summary>
    42         /// 附件信息
    43         /// </summary>
    44         public FormData FromData { get; set; }
    45         /// <summary>
    46         /// Post过来的数据容器
    47         /// </summary>
    48         public byte[] FileData { get; set; }
    49     }
    50 }

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

     1 namespace signalr.Model
     2 {
     3     /// <summary>
     4     /// 文件合并请求参数类
     5     /// </summary>
     6     public class UploadFileMergeModel
     7     {
     8         /// <summary>
     9         /// 请求类型
    10         /// </summary>
    11         public string CheckType { get; set; }
    12         /// <summary>
    13         /// 前台检测到的文件大小
    14         /// </summary>
    15         public long? FileSize { get; set; }
    16         /// <summary>
    17         /// 前台返回文件总块数
    18         /// </summary>
    19         public int? ChunkNumber { get; set; }
    20         /// <summary>
    21         /// 前台返回文件的md5值
    22         /// </summary>
    23         public string FileMd5 { get; set; }
    24         /// <summary>
    25         /// 前台返回上传文件唯一标识
    26         /// </summary>
    27         public string FileName { get; set; }
    28         /// <summary>
    29         /// 文件扩展名,不包含.
    30         /// </summary>
    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         /// <summary>
    18         /// 服务器上已经存在的文件,key为文件的Md5,value为文件路径
    19         /// </summary>
    20         public readonly Lazy<ConcurrentDictionary<string, string>> ServerUploadFileList;
    21         /// <summary>
    22         /// 客户端分配上传文件时的记录信息,key为上传文件的唯一id,value为文件分片后的当前段的md5
    23         /// </summary>
    24         public readonly Lazy<ConcurrentDictionary<string, ConcurrentBag<string>>> UploadChunkFileList;
    25     }
    26 }

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

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

    扩展一下Class/ClientMessageModel.cs

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

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

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

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

     

    "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($("<p>" + data.context + "</p>"));//当后台返回处理完成或出错时,前台显示内容,同时显示关闭按钮
            $(".modal-content").append($("<div class="modal-footer"><button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button></div>"));
            break;
        case "300":
        case "500":
            $('.modal-body').append($("<p>" + data.context + "</p>"));//展示后台返回信息
            break;
        case "400":
            if ($("#process").length == 0) {//展示后台推送的文件处理进度
                $('.modal-body').append($("<p id='process'>" + data.context + "</p>"));
            }
            $('#process').text(data.context);
            break;
        }
    });
    
    connection.start().then(function () {
        console.log("服务器已连接");
    }).catch(function (err) {
        return console.error(err.toString());
    });

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

    <!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.Title</title>
        @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=" 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;  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;  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停止运行。 

      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> _fileUploadConfig;
     24         private readonly IOptionsSnapshot<UploadFileList> _fileList;
     25         private readonly string[] _fileExt;
     26         private readonly IHubContext<ChatHub, IChatClient> _hubContext;
     27         public IndexModel(IOptionsSnapshot<FileUploadConfig> fileUploadConfig, IOptionsSnapshot<UploadFileList> fileList, IHubContext<ChatHub, IChatClient> 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         /// <summary>
     43         /// 上传文件
     44         /// </summary>
     45         /// <returns></returns>
     46         public async Task<JsonResult> 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>(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 #endif
    102             }
    103             else
    104             {
    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             try
    116             {
    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 DEBUG
    132                 Debug.WriteLine($"White Error:{e}");
    133 #endif
    134                 _fileList.Value.UploadChunkFileList.Value.TryRemove(request.Guid, out _);
    135             }
    136             return new JsonResult(new { code = 200, msg = "成功接收", miaochuan = false });
    137         }
    138 
    139         #endregion
    140 
    141         #region 合并上传文件
    142 
    143         /// <summary>
    144         /// 合并分片上传的文件
    145         /// </summary>
    146         /// <param name="mergeModel">前台传递的请求合并的参数</param>
    147         /// <returns></returns>
    148         public async Task<JsonResult> 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                 else
    201                 {
    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         #endregion
    210 
    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         #endregion
    233 
    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         #endregion
    254 
    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 FastZip
    268                 {
    269                     Password = "123456",
    270                     CreateEmptyDirectories = true
    271                 };
    272                 try
    273                 {
    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 DEBUG
    281                     Debug.WriteLine("开始解压缩文件。。。");
    282 #endif
    283                     fastZip.ExtractZip(zipFile, zipExtDir, "");
    284 #if DEBUG
    285                     Debug.WriteLine("解压缩文件成功。。。");
    286 #endif
    287                     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/length
    296                         await SentMessage(user, $"校验进度==>{i1}/{zipFiles.Length}", "400");
    297 #if DEBUG
    298                         Debug.WriteLine($"当前进度:{i1},总数:{zipFiles.Length}");
    299 #endif
    300                     }
    301                     await SentMessage(user, "校验完成", "200");
    302                 }
    303                 catch (Exception exception)
    304                 {
    305                     //发送解压缩失败信息
    306                     await SentMessage(user, $"解压缩文件失败:{exception}", "500");
    307 #if DEBUG
    308                     Debug.WriteLine($"解压缩文件失败:{exception}");
    309 #endif
    310                 }
    311             }, TaskCreationOptions.LongRunning);
    312         }
    313 
    314         #endregion
    315 
    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 ClientMessageModel
    322             {
    323                 UserId = user,
    324                 GroupName = "upload",
    325                 Context = content,
    326                 Code = code
    327             });
    328         }
    329 
    330         #endregion
    331     }
    332 }
    View Code

    未能完善的地方:

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

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

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

    参考文章:

    https://www.cnblogs.com/wdw984/p/11725118.html

    http://fex.baidu.com/webuploader/

    如此文章对你有帮助,请点个推荐吧。谢谢!

  • 相关阅读:
    mybatis总结(五)(延迟加载)
    mybatis总结(四)(mybatis的动态sql)
    mybatis总结(三)(resultMap和高级映射-级联)
    mybatis总结(二)(mybatis的基本增删改查实例说明)
    mybatis总结(一)(mybatis的基本定义介绍)
    法门扫地僧简历经验分享
    法门扫地僧面试宝典第五版
    关于https不支持http的解决方案
    浏览器渲染原理
    前端面试宝典第三版
  • 原文地址:https://www.cnblogs.com/wdw984/p/14702514.html
Copyright © 2011-2022 走看看