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/

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

  • 相关阅读:
    java基础部分的一些有意思的东西。
    antdvue按需加载插件babelpluginimport报错
    阿超的烦恼 javaScript篇
    .NET E F(Entity Framework)框架 DataBase First 和 Code First 简单用法。
    JQuery获得input ID相同但是type不同的方法
    gridview的删除,修改,数据绑定处理
    jgGrid数据格式
    Cannot read configuration file due to insufficient permissions
    Invoke action which type of result is JsonResult on controller from view using Ajax or geJSon
    Entity model数据库连接
  • 原文地址:https://www.cnblogs.com/wdw984/p/14702514.html
Copyright © 2011-2022 走看看