• ASP.NET CORE使用WebUploader对大文件分片上传,实时通知前端上传进度


    阅读参考此文章前,请先看一下
    ASP.NET Core 使用SignalR后台实时推送数据给Echarts展示图表

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

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

    在前端上传文件的组件选择上,采用了WebUploader(Web Uploader)这个优秀的前端组件,下面是来自它的官网介绍:

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

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

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

    在正式使用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. namespace signalr.Model
    3. {
    4. ///
    5. /// 上传文件配置类
    6. ///
    7. [Serializable]
    8. public class FileUploadConfig
    9. {
    10. ///
    11. /// 临时文件夹目录名
    12. ///
    13. public string TempPath { get; set; }
    14. ///
    15. /// 上传文件保存目录名
    16. ///
    17. public string FileDir { get; set; }
    18. ///
    19. /// 允许上传的文件扩展名
    20. ///
    21. public string FileExt { get; set; }
    22. }
    23. }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    1. public interface IChatClient
    2. {
    3. ///
    4. /// 客户端接收数据触发函数名
    5. ///
    6. /// 消息实体类
    7. ///
    8. Task ReceiveMessage(ClientMessageModel clientMessageModel);
    9. ///
    10. /// Echart接收数据触发函数名
    11. ///
    12. /// JSON格式的可以被Echarts识别的data数据
    13. ///
    14. Task EchartsMessage(Array data);
    15. ///
    16. /// 客户端获取自己登录后的UID
    17. ///
    18. /// 消息实体类
    19. ///
    20. Task GetMyId(ClientMessageModel clientMessageModel);
    21. ///
    22. /// 上传成功后服务器处理数据时通知前台的信息内容
    23. ///
    24. /// 消息实体类
    25. ///
    26. Task UploadInfoMessage(ClientMessageModel clientMessageModel);
    27. }

    扩展一下Class/ClientMessageModel.cs

    1. ///
    2. /// 服务端发送给客户端的信息
    3. ///
    4. [Serializable]
    5. public class ClientMessageModel
    6. {
    7. ///
    8. /// 接收用户编号
    9. ///
    10. public string UserId { get; set; }
    11. ///
    12. /// 组编号
    13. ///
    14. public string GroupName { get; set; }
    15. ///
    16. /// 发送的内容
    17. ///
    18. public string Context { get; set; }
    19. ///
    20. /// 自定义的响应编码
    21. ///
    22. public string Code { get; set; }
    23. }

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

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

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

    1. "use strict";
    2. var connection = new signalR.HubConnectionBuilder()
    3. .withUrl("/chatHub")
    4. .withAutomaticReconnect()
    5. .configureLogging(signalR.LogLevel.Debug)
    6. .build();
    7. var user = "";
    8. connection.on("GetMyId", function (data) {
    9. user = data.userId;
    10. });
    11. connection.on("ReceiveMessage", function (data) {
    12. console.log(data.userId + data.context);
    13. });
    14. connection.on("UploadInfoMessage", function (data) {
    15. switch (data.code) {
    16. case "200":
    17. $('.modal-body').append($("

      " + data.context + "

      "
      ));//当后台返回处理完成或出错时,前台显示内容,同时显示关闭按钮
    18. $(".modal-content").append($("
      "
      ));
    19. break;
    20. case "300":
    21. case "500":
    22. $('.modal-body').append($("

      " + data.context + "

      "
      ));//展示后台返回信息
    23. break;
    24. case "400":
    25. if ($("#process").length == 0) {//展示后台推送的文件处理进度
    26. $('.modal-body').append($("

      " + data.context + "

      "
      ));
    27. }
    28. $('#process').text(data.context);
    29. break;
    30. }
    31. });
    32. connection.start().then(function () {
    33. console.log("服务器已连接");
    34. }).catch(function (err) {
    35. return console.error(err.toString());
    36. });

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

    1. html>
    2. <html>
    3. <head>
    4. <meta charset="utf-8">
    5. <meta name="viewport" content="width=device-width" />
    6. <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    7. <link rel="stylesheet" href="~/lib/webuploader/dist/webuploader.css" />
    8. <script type="text/javascript" src="~/lib/jquery/dist/jquery.min.js">script>
    9. <script type="text/javascript" src="~/lib/webuploader/dist/webuploader.js">script>
    10. <script type="text/javascript" src="~/lib/bootstrap/dist/js/bootstrap.min.js">script>
    11. <title>@ViewBag.Titletitle>
    12. @await RenderSectionAsync("Scripts", required: false)
    13. head>
    14. <body>
    15. @RenderBody()
    16. body>
    17. html>

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

    1. 1 @page "{handler?}"
    2. 2 @model MediatRStudy.Pages.upload.IndexModel
    3. 3 @{
    4. 4 ViewBag.Title = "WebUploader";
    5. 5 Layout = "_LayoutUpload";
    6. 6 }
    7. 7 @section Scripts
    8. 8 {
    9. 9 <script src="~/js/signalr/dist/browser/signalr.js">script>
    10. 10 <script src="~/js/uploader.js">script>
    11. 11
    12. 12 <script>
    13. 13 // 每次分片文件大小限制为5M
    14. 14 var chunkSize = 5 * 1024 * 1024;
    15. 15 // 全部文件限制10G大小
    16. 16 var fileTotalSize = 10 * 1024 * 1024 * 1024;
    17. 17 // 单文件限制5G大小
    18. 18 var fileSingleSize = 5 * 1024 * 1024 * 1024;
    19. 19 jQuery(function() {
    20. 20 var $ = jQuery,
    21. 21 $list = $('#thelist'),
    22. 22 $btn = $('#ctlBtn'),
    23. 23 state = 'pending',
    24. 24 md5s = {},//分块传输时的各个块的md5值
    25. 25 dataState,//当前状态
    26. 26 Token,//可以做用户验证
    27. 27 uploader;//webUploader的实例
    28. 28 var fileExt = ["zip", "rar"];//允许上传的类型
    29. 29 Token = '@ViewData["Token"]';
    30. 30 if (Token == '' || Token == 'undefined') {
    31. 31 $("#uploader").hide();
    32. 32 alert("登录超时,请重新登录。");
    33. 33 }
    34. 34 35 36 37 38 //注册Webuploader要监听的上传文件时的三个事件
    35. 39 //before-send-file 在执行文件上传前先执行这个;before-send在开始往服务器发送文件前执行;after-send-file所有文件上传完毕后执行
    36. 40
    37. 41 window.WebUploader.Uploader.register({
    38. 42 "before-send-file": "beforeSendFile",
    39. 43 "before-send": "beforeSend",
    40. 44 "after-send-file": "afterSendFile"
    41. 45 },
    42. 46 {
    43. 47 //第一步,开始上传前校验文件,并传递给服务器当前文件的MD5,服务器可根据MD5来实现类似秒传效果
    44. 48 beforeSendFile: function(file) {
    45. 49 var owner = this.owner;
    46. 50 md5s.length = 0;
    47. 51 var deferred = window.WebUploader.Deferred();
    48. 52 owner.md5File(file, 0, file.size)
    49. 53 .progress(function(percentage) {
    50. 54 console.log("文件MD5计算进度:", percentage);
    51. 55 })
    52. 56 .fail(function() {
    53. 57 deferred.reject();
    54. 58 console.log("文件MD5获取失败");
    55. 59 })
    56. 60 .then(function(md5) {
    57. 61 console.log("文件MD5:", md5);
    58. 62 file.md5 = md5;
    59. 63 var params = {
    60. 64 "checktype": "whole",
    61. 65 "filesize": file.size,
    62. 66 "filemd5": md5
    63. 67 ,"filename":file.name
    64. 68 ,"fileguid":file.guid
    65. 69 };
    66. 70 $.ajax({
    67. 71 url: '/upload/FileWhole', //通过md5校验实现文件秒传
    68. 72 type: 'POST',
    69. 73 headers: {//请求的时候传递进去防CSRF攻击的认证信息
    70. 74 RequestVerificationToken:
    71. 75 $('input:hidden[name="__RequestVerificationToken"]').val()
    72. 76 },
    73. 77 data: params,
    74. 78 contentType: 'application/x-www-form-urlencoded',
    75. 79 async: true, // 开启异步请求
    76. 80 dataType: 'JSON',
    77. 81 success: function(data) {
    78. 82 data = (typeof data) == 'string' ? JSON.parse(data) : data;
    79. 83 if (data.code != '200') {
    80. 84 dataState = data;
    81. 85 //服务器返回错误信息
    82. 86 alert('错误:' + data.msg);
    83. 87 deferred.reject();//取消后续上传
    84. 88 }
    85. 89 if (data.isExist) {
    86. 90 // 跳过当前文件并标记文件状态为上传完成
    87. 91 dataState = data;
    88. 92 owner.skipFile(file, window.WebUploader.File.Status.COMPLETE);
    89. 93 deferred.resolve();
    90. 94 $('#' + file.id).find('p.state').text('上传成功【秒传】');
    91. 95
    92. 96 } else {
    93. 97 deferred.resolve();
    94. 98 }
    95. 99 },
    96. 100 error: function(xhr, status) {
    97. 101 $('#' + file.id).find('p.state').text('上传失败:'+status);
    98. 102 console.log("上传失败:", status);
    99. 103 }
    100. 104 });
    101. 105 });
    102. 106
    103. 107 return deferred.promise();
    104. 108 },
    105. 109 //上传事件第二步:分块上传时,每个分块触发上传前执行
    106. 110 beforeSend: function(block) {
    107. 111 var deferred = window.WebUploader.Deferred();
    108. 112 var owner = this.owner;
    109. 113 owner.md5File(block.file, block.start, block.end)
    110. 114 .progress(function(percentage) {
    111. 115 console.log("当前分块内容的MD5计算进度:", percentage);
    112. 116 })
    113. 117 .fail(function() {
    114. 118 deferred.reject();
    115. 119 })
    116. 120 .then(function(md5) {
    117. 121 //计算当前块的MD5值并写入数组
    118. 122 md5s[block.blob.uid] = md5;
    119. 123 deferred.resolve();
    120. 124 });
    121. 125 return deferred.promise();
    122. 126 },
    123. 127 //时间点3:所有分块上传成功后调用此函数
    124. 128 afterSendFile: function(file) {
    125. 129 var deferred = $.Deferred();
    126. 130 $('#' + file.id).find('p.state').text('执行最后一步');
    127. 131 console.log(file);
    128. 132 if (file.skipped) {
    129. 133 deferred.resolve();
    130. 134 console.log("执行服务器合并分块文件操作");
    131. 135 return deferred.promise();
    132. 136 }
    133. 137 var chunkNumber = Math.ceil(file.size / chunkSize);//总块数
    134. 138 var params = {
    135. 139 "checktype": "merge",
    136. 140 "filesize": file.size,
    137. 141 "chunknumber": chunkNumber,
    138. 142 "filemd5": file.md5,
    139. 143 "filename": file.guid,
    140. 144 "fileext": file.ext//扩展名
    141. 145 };
    142. 146 $.ajax({
    143. 147 type: "POST",
    144. 148 url: "/upload/FileMerge",
    145. 149 headers: {
    146. 150 RequestVerificationToken:
    147. 151 $('input:hidden[name="__RequestVerificationToken"]').val(),
    148. 152 userid:user //传递SignalR分配的编号
    149. 153 },
    150. 154 data: params,
    151. 155 async: true,
    152. 156 success: function(response) {
    153. 157 if (response.code == 200) {
    154. 158 //服务器合并完成分块传输的文件后执行
    155. 159 dataState = response;
    156. 160 $("#myModal").modal('show');
    157. 161 } else {
    158. 162 alert(response.msg);
    159. 163 }
    160. 164 deferred.resolve();
    161. 165 },
    162. 166 error: function() {
    163. 167 dataState = undefined;
    164. 168 deferred.reject();
    165. 169 }
    166. 170 });
    167. 171 return deferred.promise();
    168. 172 }
    169. 173 });
    170. 174 uploader = window.WebUploader.create({
    171. 175 resize: false,
    172. 176 fileNumLimit: 1,
    173. 177 swf: '/lib/webuploader/dist/Uploader.swf',
    174. 178 server: '/upload/FileSave',
    175. 179 pick: { id: '#picker', multiple: false },
    176. 180 chunked: true,
    177. 181 chunkSize: chunkSize,
    178. 182 chunkRetry: 3,
    179. 183 fileSizeLimit: fileTotalSize,
    180. 184 fileSingleSizeLimit: fileSingleSize,
    181. 185 formData: {
    182. 186 }
    183. 187 });
    184. 188 uploader.on('beforeFileQueued',
    185. 189 function(file) {
    186. 190 var isAdd = false;
    187. 191 for (var i = 0; i < fileExt.length; i++) {
    188. 192 if (file.ext == fileExt[i]) {
    189. 193 file.guid = window.WebUploader.Base.guid();
    190. 194 isAdd = true;
    191. 195 break;
    192. 196 }
    193. 197 }
    194. 198 return isAdd;
    195. 199 });
    196. 200 //每次上传前,如果分块传输,则带上分块信息参数
    197. 201 uploader.on('uploadBeforeSend',
    198. 202 function(block, data, headers) {
    199. 203 var params = {
    200. 204 "checktype": "chunk",
    201. 205 "filesize": block.file.size,
    202. 206 "fileid": block.blob.ruid,
    203. 207 "chunksize": block.blob.size,
    204. 208 "chunkindex": block.chunk,
    205. 209 "chunkstart": block.start,
    206. 210 "chunkend": block.end,
    207. 211 "chunkcount": block.chunks,
    208. 212 "chunkid": block.blob.uid,
    209. 213 "md5": md5s[block.blob.uid]
    210. 214 };
    211. 215 data.formData = JSON.stringify(params);
    212. 216
    213. 217 headers.Authorization = Token;
    214. 218 headers.RequestVerificationToken = $('input:hidden[name="__RequestVerificationToken"]').val();
    215. 219 data.guid = block.file.guid;
    216. 220 });
    217. 221 // 当有文件添加进来的时候
    218. 222 uploader.on('fileQueued',
    219. 223 function(file) {
    220. 224 $list.append('
      225 file.id +
    221. 226 '" class="item">' +
    222. 227 '

      ' +

    223. 228 file.name +
    224. 229 '' +
    225. 230 '
      231 file.id +
    226. 232 '" value="' +
    227. 233 file.guid +
    228. 234 '" />' +
    229. 235 '

      等待上传...

      '
      +
    230. 236 '
      ');
  • 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 = $('
    ' +
  • 247 '
    ' +
  • 248 '
    ' +
  • 249 '
    ').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('');
  • 262 file.setStatus('error');
  • 263 return;
  • 264 }
  • 265 if (dataState.success == true) {
  • 266 if (dataState.miaochuan == true) {
  • 267 $('#' + file.id).find('p.state').text('上传成功[秒传]');
  • 268 } else {
  • 269 $('#' + file.id).find('p.state').text('上传成功');
  • 270 }
  • 271 $('#' + file.id).find('button').remove();
  • 272 return;
  • 273
  • 274 } else {
  • 275 $('#' + file.id).find('p.state').text('服务器未能成功接收,状态:' + dataState.success);
  • 276 return;
  • 277 }
  • 278 });
  • 279
  • 280 uploader.on('uploadError',
  • 281 function(file) {
  • 282 $('#' + file.id).find('p.state').text('上传出错');
  • 283 });
  • 284 //分块传输后,可以在这个事件中获取到服务器返回的信息,同时这里可以实现文件续传(块文件的MD5存在时,后台可以跳过保存步骤)
  • 285 uploader.on('uploadAccept',
  • 286 function(file, response, reject) {
  • 287 if (response.code !== 200) {
  • 288 alert("上传出错:" + response.msg);
  • 289 return false;
  • 290 }
  • 291 return true;
  • 292 });
  • 293 uploader.on('uploadComplete',
  • 294 function(file) {
  • 295 $('#' + file.id).find('.progress').fadeOut();
  • 296 });
  • 297
  • 298 uploader.on('all',
  • 299 function(type) {
  • 300 if (type === 'startUpload') {
  • 301 state = 'uploading';
  • 302 } else if (type === 'stopUpload') {
  • 303 state = 'paused';
  • 304 } else if (type === 'uploadFinished') {
  • 305 state = 'done';
  • 306 }
  • 307 if (state === 'done') {
  • 308 $btn.text('继续上传');
  • 309 } else if (state === 'uploading') {
  • 310 $btn.text('暂停上传');
  • 311 } else {
  • 312 $btn.text('开始上传');
  • 313 }
  • 314 });
  • 315 $btn.on('click',
  • 316 function() {
  • 317 if (state === 'uploading') {
  • 318 uploader.stop();
  • 319 } else if (state == 'done') {
  • 320 window.location.reload();
  • 321 } else {
  • 322 uploader.upload();
  • 323 }
  • 324 });
  • 325 });
  • 326 script>
  • 327 }
  • 328 <div class="container">
  • 329 <div class="row">
  • 330 <div id="uploader" class="wu-example">
  • 331 <span style="color: red">请上传压缩包span>
  • 332 <div class="form-group" id="thelist">
  • 333 div>
  • 334 <div class="form-group">
  • 335 <form method="post">
  • 336 <div id="picker" class="webuploader-container">
  • 337 <div class="webuploader-pick">选择文件div>
  • 338 <div style="position: absolute; top: 0; left: 0; width: 88px; height: 34px; overflow: hidden; bottom: auto; right: auto;">
  • 339 <input type="file" name="file" class="webuploader-element-invisible" />
  • 340 <label style="-ms-opacity: 0; opacity: 0; width: 100%; height: 100%; display: block; cursor: pointer; background: rgb(255, 255, 255);">label>
  • 341 div>
  • 342 div>
  • 343 <button id="ctlBtn" class="btn btn-success" type="button">开始上传button>
  • 344 form>
  • 345 div>
  • 346 div>
  • 347 div>
  • 348 div>
  • 349
  • 350 <div class="modal fade" id="myModal" tabindex="-1" aria-labelledby="exampleModalScrollableTitle" style="display: none;" data-backdrop="static" aria-hidden="true">
  • 351 <div class="modal-dialog modal-dialog-scrollable">
  • 352 <div class="modal-content">
  • 353 <div class="modal-header">
  • 354 <h5 class="modal-title" id="exampleModalScrollableTitle">正在处理。。。h5>
  • 355 <button type="button" class="close" data-dismiss="modal" aria-label="Close">
  • 356
  • 357 button>
  • 358 div>
  • 359 <div class="modal-body">
  • 360 <p>服务器正在处理数据,请不要关闭和刷新此页面。p>
  • 361 div>
  • 362 div>
  • 363 div>
  • 364 div>
  • index.cshtml的代码文件如下

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

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

  • 相关阅读:
    JVM 第一部分 JVM两种解释器 类加载过程和类加载器
    vim快捷指令
    Leetcode112. 路径总和 Path Sum - Python 递归法
    ARM学习
    如何给Nginx配置访问IP白名单
    【填坑】ESP32 bootloader初探(下)
    (论文阅读)TiDB:一款基于Raft的HTAP数据库
    (十九)STM32——输入捕获
    C++17静态数据成员声明为inline
    【Javascript】等于与全等于
  • 原文地址:https://blog.csdn.net/5653325/article/details/125996197