阅读参考此文章前,请先看一下 Core 使用SignalR后台实时推送数据给Echarts展示图表
此文章是上一篇的功能扩展,一些基本的程序模块逻辑都已经在上一篇文章中做了介绍,这里就不再重复。
本次,我们来实现一个单个大文件上传,并且把后台对上传文件的处理进度通过 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中增加一个配置用来保存上传文件。
{"Logging": {"LogLevel": {"Default": "Information","Microsoft": "Warning","Microsoft.Hosting.Lifetime": "Information"}},"FileUpload": {"TempPath": "temp",//临时文件保存目录"FileDir": "upload",//上传完成后的保存目录"FileExt": "zip,rar"//允许上传的文件类型},"AllowedHosts": "*"}
在项目中新建一个Model目录,用来实现上传文件的相关配置,建立相应的多个类文件
FileUploadConfig.cs服务器用来接受和保存文件的配置
using System;namespace signalr.Model{/// <summary>/// 上传文件配置类/// </summary>[Serializable]public class FileUploadConfig{/// <summary>/// 临时文件夹目录名/// </summary>public string TempPath { get; set; }/// <summary>/// 上传文件保存目录名/// </summary>public string FileDir { get; set; }/// <summary>/// 允许上传的文件扩展名/// </summary>public string FileExt { get; set; }}}
UploadFileWholeModel.cs前台开始传输前会对文件进行一次MD5算法,这里可以通过文件MD5值传递给后台来通过比对已上传的文件MD5值列表来实现秒传功能
namespace signalr.Model{/// <summary>/// 文件秒传检测前台传递参数/// </summary>public class UploadFileWholeModel{/// <summary>/// 请求类型,这里固定为:whole/// </summary>public string CheckType { get; set; }/// <summary>/// 文件的MD5/// </summary>public string FileMd5 { get; set; }/// <summary>/// 前台文件的唯一标识/// </summary>public string FileGuid { get; set; }/// <summary>/// 前台上传文件名/// </summary>public string FileName { get; set; }/// <summary>/// 文件大小/// </summary>public int? FileSize { get; set; }}}
UploadFileChunkModel.cs前台文件分块传输的时候会对分块传输内容进行MD5计算,并且分块传输的时候会传递当前分块的一些信息,这里对应的后台接收实体类。
我们可以通过分块传输的MD5值来实现文件续传功能(如文件的某块MD5已存在则返回给前台跳过当前块)
namespace signalr.Model{/// <summary>/// 文件分块(续传)传递参数/// </summary>public class UploadFileChunkModel{/// <summary>/// 文件分块传输检测类型,这里固定为chunk/// </summary>public string CheckType { get; set; }/// <summary>/// 文件的总大小/// </summary>public long? FileSize { get; set; }/// <summary>/// 当前块所属文件编号/// </summary>public string FileId { get; set; }/// <summary>/// 当前块基于文件的开始偏移量/// </summary>public long? ChunkStart { get; set; }/// <summary>/// 当前块基于文件的结束偏移量/// </summary>public long? ChunkEnd { get; set; }/// <summary>/// 当前块的大小/// </summary>public long? ChunkSize { get; set; }/// <summary>/// 当前块编号/// </summary>public string ChunkIndex { get; set; }/// <summary>/// 当前文件分块总数/// </summary>public string ChunkCount { get; set; }/// <summary>/// 当前块的编号/// </summary>public string ChunkId { get; set; }/// <summary>/// 当前块的md5/// </summary>public string Md5 { get; set; }}}
FormData.cs这是分块传输时传递的当前块的信息配置
using System;namespace signalr.Model{/// <summary>/// 上传文件时的附加信息/// </summary>[Serializable]public class FormData{/// <summary>/// 当前请求类型 分片传输是:chunk/// </summary>public string Checktype { get; set; }/// <summary>/// 文件总字节数/// </summary>public int? Filesize { get; set; }/// <summary>/// 文件唯一编号/// </summary>public string Fileid { get; set; }/// <summary>/// 分片数据大小/// </summary>public int? Chunksize { get; set; }/// <summary>/// 当前分片编号/// </summary>public int? Chunkindex { get; set; }/// <summary>/// 分片起始编译量/// </summary>public int? Chunkstart { get; set; }/// <summary>/// 分片结束编译量/// </summary>public int? Chunkend { get; set; }/// <summary>/// 分片总数量/// </summary>public int? Chunkcount { get; set; }/// <summary>/// 当前分片唯一编号/// </summary>public string Chunkid { get; set; }/// <summary>/// 当前块MD5值/// </summary>public string Md5 { get; set; }}}
UploadFileModel.cs每次上传文件的时候,前台都会传递这些参数给服务器,服务器可以根据参数做相应的处理
using System;using Microsoft.AspNetCore.Mvc;namespace signalr.Model{/// <summary>/// WebUploader上传文件实体类/// </summary>[Serializable]public class UploadFileModel{/// <summary>/// 前台WebUploader的ID/// </summary>public string Id { get; set; }/// <summary>/// 当前文件(块)的前端计算的md5/// </summary>public string FileMd5 { get; set; }/// <summary>/// 当前文件块号/// </summary>public string Chunk { get; set; }/// <summary>/// 原始文件名/// </summary>public string Name { get; set; }/// <summary>/// 文件类型(如:image/png)/// </summary>[FromForm(Name = "type")]public string FileType { get; set; }/// <summary>/// 当前文件(块)的大小/// </summary>public long? Size { get; set; }/// <summary>/// 前台给此文件分配的唯一编号/// </summary>public string Guid { get; set; }/// <summary>/// 附件信息/// </summary>public FormData FromData { get; set; }/// <summary>/// Post过来的数据容器/// </summary>public byte[] FileData { get; set; }}}
UploadFileMergeModel.cs当所有块传输完成后,传递给后台一个合并文件的请求,后台通过参数中的信息把分块保存的文件合并成一个完整的文件
namespace signalr.Model{/// <summary>/// 文件合并请求参数类/// </summary>public class UploadFileMergeModel{/// <summary>/// 请求类型/// </summary>public string CheckType { get; set; }/// <summary>/// 前台检测到的文件大小/// </summary>public long? FileSize { get; set; }/// <summary>/// 前台返回文件总块数/// </summary>public int? ChunkNumber { get; set; }/// <summary>/// 前台返回文件的md5值/// </summary>public string FileMd5 { get; set; }/// <summary>/// 前台返回上传文件唯一标识/// </summary>public string FileName { get; set; }/// <summary>/// 文件扩展名,不包含./// </summary>public string FileExt { get; set; }}}
为了实现【秒传】和分块传输时的【断点续传】功能,我们在Class目录中定义一个UploadFileList.cs类,用来模拟持久化保存服务器所接收到的文件MD5校验列表和已接收的分块MD5值信息,这里我们使用了并发线程安全的ConcurrentDictionary和ConcurrentBag
using System;using System.Collections.Concurrent;namespace signalr.Class{public class UploadFileList{private static readonly Lazy<ConcurrentDictionary<string, string>> _serverUploadFileList = new Lazy<ConcurrentDictionary<string, string>>();private static readonly Lazy<ConcurrentDictionary<string, ConcurrentBag<string>>> _uploadChunkFileList =new Lazy<ConcurrentDictionary<string, ConcurrentBag<string>>>();public UploadFileList(){ServerUploadFileList = _serverUploadFileList;UploadChunkFileList = _uploadChunkFileList;}/// <summary>/// 服务器上已经存在的文件,key为文件的Md5,value为文件路径/// </summary>public readonly Lazy<ConcurrentDictionary<string, string>> ServerUploadFileList;/// <summary>/// 客户端分配上传文件时的记录信息,key为上传文件的唯一id,value为文件分片后的当前段的md5/// </summary>public readonly Lazy<ConcurrentDictionary<string, ConcurrentBag<string>>> UploadChunkFileList;}}
扩展一下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.IndexModel3 @{4ViewBag.Title = "WebUploader";5Layout = "_LayoutUpload";6 }7 @section Scripts8 {9 <script src="~/js/signalr/dist/browser/signalr.js"></script>10 <script src="~/js/uploader.js"></script>11 12 <script>13// 每次分片文件大小限制为5M14var chunkSize = 5 * 1024 * 1024;15// 全部文件限制10G大小16var fileTotalSize = 10 * 1024 * 1024 * 1024;17// 单文件限制5G大小18var fileSingleSize = 5 * 1024 * 1024 * 1024;19jQuery(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": md567 ,"filename":file.name68 ,"fileguid":file.guid69 };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.PLETE);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) {133deferred.resolve();134console.log("执行服务器合并分块文件操作");135return 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({147type: "POST",148url: "/upload/FileMerge",149headers: {150 RequestVerificationToken:151 $('input:hidden[name="__RequestVerificationToken"]').val(),152 userid:user //传递SignalR分配的编号153},154data: params,155async: true,156success: 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},166error: 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]) {193file.guid = window.WebUploader.Base.guid();194isAdd = true;195break;196 }197 }198 return isAdd;199 });200 //每次上传前,如果分块传输,则带上分块信息参数201 uploader.on('uploadBeforeSend',202 function(block, data, headers) {203 var params = {204 "checktype": "chunk",205 "filesize": block.file.size,206 "fileid": block.blob.ruid,207 "chunksize": block.blob.size,208 "chunkindex": block.chunk,209 "chunkstart": block.start,210 "chunkend": block.end,211 "chunkcount": block.chunks,212 "chunkid": block.blob.uid,213 "md5": md5s[block.blob.uid]214 };215 data.formData = JSON.stringify(params);216 217 headers.Authorization = Token;218 headers.RequestVerificationToken = $('input:hidden[name="__RequestVerificationToken"]').val();219 data.guid = block.file.guid;220 });221 // 当有文件添加进来的时候222 uploader.on('fileQueued',223 function(file) {224 $list.append('<div id="' +225 file.id +226 '" class="item">' +227 '<h4 class="info">' +228 file.name +229 '</h4>' +230 '<input type="hidden" id="h_' +231 file.id +232 '" value="' +233 file.guid +234 '" />' +235 '<p class="state">等待上传...</p>' +236 '</div>');237 });238 239 // 文件上传过程中创建进度条实时显示。240 uploader.on('uploadProgress',241 function(file, percentage) {242 var $li = $('#' + file.id),243 $percent = $li.find('.progress .progress-bar');244 // 避免重复创建245 if (!$percent.length) {246 $percent = $('<div class="progress progress-striped active">' +247'<div class="progress-bar" role="progressbar" style="width: 0%">' +248'</div>' +249'</div>').appendTo($li).find('.progress-bar');250 }251 $li.find('p.state').text('上传中');252 253 $percent.css('width', percentage * 100 + '%');254 });255 256 uploader.on('uploadSuccess',257 function(file) {258 if (dataState == undefined) {259 $('#' + file.id).find('p.state').text('上传失败');260 $('#' + file.id).find('button').remove();261 $('#' + file.id).find('p.state').before('<button id="retry" type="button" class="btn btn-primary fright retry pbtn">重新上传</button>');262 file.setStatus('error');263 return;264 }265 if (dataState.success == true) {266 if (dataState.miaochuan == true) {267$('#' + file.id).find('p.state').text('上传成功[秒传]');268 } else {269$('#' + file.id).find('p.state').text('上传成功');270 }271 $('#' + file.id).find('button').remove();272 return;273 274 } else {275 $('#' + file.id).find('p.state').text('服务器未能成功接收,状态:' + dataState.success);276 return;277 }278 });279 280 uploader.on('uploadError',281 function(file) {282 $('#' + file.id).find('p.state').text('上传出错');283 });284 //分块传输后,可以在这个事件中获取到服务器返回的信息,同时这里可以实现文件续传(块文件的MD5存在时,后台可以跳过保存步骤)285 uploader.on('uploadAccept',286 function(file, response, reject) {287 if (response.code !== 200) {288 alert("上传出错:" + response.msg);289 return false;290 }291 return true;292 });293 uploader.on('uploadComplete',294 function(file) {295 $('#' + file.id).find('.progress').fadeOut();296 });297 298 uploader.on('all',299 function(type) {300 if (type === 'startUpload') {301 state = 'uploading';302 } else if (type === 'stopUpload') {303 state = 'paused';304 } else if (type === 'uploadFinished') {305 state = 'done';306 }307 if (state === 'done') {308 $btn.text('继续上传');309 } else if (state === 'uploading') {310 $btn.text('暂停上传');311 } else {312 $btn.text('开始上传');313 }314 });315 $btn.on('click',316 function() {317 if (state === 'uploading') {318 uploader.stop();319 } else if (state == 'done') {320 window.location.reload();321 } else {322 uploader.upload();323 }324 });325});326 </script>327 }328 <div class="container">329<div class="row">330 <div id="uploader" class="wu-example">331 <span style="color: red">请上传压缩包</span>332 <div class="form-group" id="thelist">333 </div>334 <div class="form-group">335 <form method="post">336 <div id="picker" class="webuploader-container">337<div class="webuploader-pick">选择文件</div>338<div style="position: absolute; top: 0; left: 0; width: 88px; height: 34px; overflow: hidden; bottom: auto; right: auto;">339 <input type="file" name="file" class="webuploader-element-invisible" />340 <label style="-ms-opacity: 0; opacity: 0; width: 100%; height: 100%; display: block; cursor: pointer; background: rgb(255, 255, 255);"></label>341</div>342 </div>343 <button id="ctlBtn" class="btn btn-success" type="button">开始上传</button>344 </form>345 </div>346 </div>347</div>348 </div>349 350 <div class="modal fade" id="myModal" tabindex="-1" aria-labelledby="exampleModalScrollableTitle" style="display: none;" data-backdrop="static" aria-hidden="true">351<div class="modal-dialog modal-dialog-scrollable">352 <div class="modal-content">353 <div class="modal-header">354 <h5 class="modal-title" id="exampleModalScrollableTitle">正在处理。。。</h5>355 <button type="button" class="close" data-dismiss="modal" aria-label="Close">356 357 </button>358 </div>359 <div class="modal-body">360 <p>服务器正在处理数据,请不要关闭和刷新此页面。</p>361 </div>362 </div>363</div>364 </div>
index.cshtml的代码文件如下
本示例只能解压缩zip文件,并且密码是123456,友情提示,不要用QQ浏览器调试,否则会遇到选择文件后DEBUG停止运行。
using ICSharpCode.SharpZipLib.Zip;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;using Microsoft.AspNetCore.Mvc.RazorPages;using Microsoft.AspNetCore.SignalR;using Microsoft.Extensions.Options;using signalr.Class;using signalr.HubInterface;using signalr.Hubs;using signalr.Model;using System;using System.Collections.Concurrent;using System.Diagnostics;using System.IO;using System.Linq;using System.Text.Json;using System.Threading.Tasks;namespace signalr.Pages.upload{public class IndexModel : PageModel{private readonly IOptionsSnapshot<FileUploadConfig> _fileUploadConfig;private readonly IOptionsSnapshot<UploadFileList> _fileList;private readonly string[] _fileExt;private readonly IHubContext<ChatHub, IChatClient> _hubContext;public IndexModel(IOptionsSnapshot<FileUploadConfig> fileUploadConfig, IOptionsSnapshot<UploadFileList> fileList, IHubContext<ChatHub, IChatClient> hubContext){_fileUploadConfig = fileUploadConfig;_fileList = fileList;_fileExt = _fileUploadConfig.Value.FileExt.Split(',').ToArray();_hubContext = hubContext;}public IActionResult OnGet(){ViewData["Token"] = "666";return Page();}#region 上传文件/// <summary>/// 上传文件/// </summary>/// <returns></returns>public async Task<JsonResult> OnPostFileSaveAsync(IFormFile file, UploadFileModel model){if (_fileUploadConfig.Value == null){return new JsonResult(new { code = 400, msg = "服务器配置不正确" });}if (file == null || file.Length < 1){return new JsonResult(new { code = 404, msg = "没有接收到要保存的文件" });}Request.EnableBuffering();var formData = Request.Form["formData"];if (model == null || string.IsNullOrWhiteSpace(formData)){return new JsonResult(new { code = 401, msg = "没有接收到必要的参数" });}var request = model;long.TryParse(Request.Form["size"], out var fileSize);request.Size = fileSize;try{request.FromData = JsonSerializer.Deserialize<FormData>(formData, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });}catch (Exception e){Debug.WriteLine(e);}if (request.FromData == null){return new JsonResult(new { code = 402, msg = "参数错误" });}#if DEBUGDebug.WriteLine($"原文件名:{request.Name},文件编号:{request.Guid},文件块编号:{request.Chunk},文件Md5:{request.FileMd5},当前块UID:{request.FromData?.Chunkid},当前块MD5:{request.FromData?.Md5}");#endifvar fileExt = request.Name.Substring(request.Name.LastIndexOf('.') + 1).ToLowerInvariant();if (!_fileExt.Contains(fileExt)){return new JsonResult(new { code = 403, msg = "文件类型不在允许范围内" });}if (_fileList.Value.UploadChunkFileList.Value.ContainsKey(request.Guid)){if (!_fileList.Value.UploadChunkFileList.Value[request.Guid].Any(x => string.Equals(x, request.FromData.Md5, StringComparison.OrdinalIgnoreCase))){_fileList.Value.UploadChunkFileList.Value[request.Guid].Add(request.FromData.Md5);}#if DEBUGelse{Debug.WriteLine($"ContainsKey{request.FromData.Chunkindex}存在校验值{request.FromData.Md5}");return new JsonResult(new { code = 200, msg = "成功接收", miaochuan = true });}#endif}else{return new JsonResult(new { code = 405, msg = "接收失败,因为服务器没有找到此文件的容器,请重新上传" });}var dirPath = bine(AppDomain.CurrentDomain.BaseDirectory, _fileUploadConfig.Value.TempPath, request.Guid);if (!Directory.Exists(dirPath)){Directory.CreateDirectory(dirPath);}var tempFile = string.Concat(dirPath, "\\", request.FromData.Chunkindex.ToString().PadLeft(4, '0'), ".", fileExt);try{await using var fs = System.IO.File.OpenWrite(tempFile);request.FileData = new byte[Convert.ToInt32(request.FromData.Chunksize ?? 0)];await using var memStream = new MemoryStream();await file.CopyToAsync(memStream);request.FileData = memStream.ToArray();await fs.WriteAsync(request.FileData, 0, request.FileData.Length);await fs.FlushAsync();}catch (Exception e){#if DEBUGDebug.WriteLine($"White Error:{e}");#endif_fileList.Value.UploadChunkFileList.Value.TryRemove(request.Guid, out _);}return new JsonResult(new { code = 200, msg = "成功接收", miaochuan = false });}#endregion#region 合并上传文件/// <summary>/// 合并分片上传的文件/// </summary>/// <param name="mergeModel">前台传递的请求合并的参数</param>/// <returns></returns>public async Task<JsonResult> OnPostFileMergeAsync(UploadFileMergeModel mergeModel){return await Task.Run(async () =>{if (mergeModel == null || string.IsNullOrWhiteSpace(mergeModel.FileName) ||string.IsNullOrWhiteSpace(mergeModel.FileMd5)){return new JsonResult(new { code = 300, success = false, count = 0, size = 0, msg = "合并失败,参数不正确。" });}if (!_fileExt.Contains(mergeModel.FileExt.ToLowerInvariant())){return new JsonResult(new { code = 403, success = false, msg = "文件类型不在允许范围内" });}var fileSavePath = "";if (!_fileList.Value.ServerUploadFileList.Value.ContainsKey(mergeModel.FileMd5)){//合并块文件、删除临时文件var chunks = Directory.GetFiles(bine(AppDomain.CurrentDomain.BaseDirectory, _fileUploadConfig.Value.TempPath, mergeModel.FileName), "*.*");if (!chunks.Any()){return new JsonResult(new { code = 302, success = false, count = 0, size = 0, msg = "未找到文件块信息,请重试。" });}var dirPath = bine(AppDomain.CurrentDomain.BaseDirectory, _fileUploadConfig.Value.FileDir);if (!Directory.Exists(dirPath)){Directory.CreateDirectory(dirPath);}fileSavePath = bine(_fileUploadConfig.Value.FileDir,string.Concat(mergeModel.FileName, ".", mergeModel.FileExt));await using var fs =new FileStream(bine(dirPath, string.Concat(mergeModel.FileName, ".", mergeModel.FileExt)), FileMode.Create);foreach (var file in chunks.OrderBy(x => x)){//Debug.WriteLine($"File==>{file}");var bytes = await System.IO.File.ReadAllBytesAsync(file);await fs.WriteAsync(bytes.AsMemory(0, bytes.Length));}//Directory.Delete(bine(AppDomain.CurrentDomain.BaseDirectory, _fileUploadConfig.Value.TempPath, mergeModel.FileName), true);if (!_fileList.Value.ServerUploadFileList.Value.TryAdd(mergeModel.FileMd5, fileSavePath)){return new JsonResult(new { code = 301, success = false, count = 0, size = 0, msg = "服务器保存文件失败,请重试。" });}}var user = Request.Headers["userid"];//调用解压文件if (string.Equals(mergeModel.FileExt.ToLowerInvariant(), "zip")){DoUnZip(bine(AppDomain.CurrentDomain.BaseDirectory, fileSavePath), user.ToString());}else{await SentMessage(user.ToString(), "服务器只能解压缩zip格式文件。", "200");}return new JsonResult(new { code = 200, success = true, count = 0, size = 0, msg = "上传成功", url = fileSavePath });});}#endregion#region 文件秒传检测、文件类型允许范围检测public JsonResult OnPostFileWholeAsync(UploadFileWholeModel model){if (model == null || string.IsNullOrWhiteSpace(model.FileMd5)){return new JsonResult(new { Code = 300, IsExist = false, success = false, FileUrl = "", Msg = "参数不正确" });}var fileExt = model.FileName.Substring(model.FileName.LastIndexOf('.') + 1).ToLowerInvariant();if (!_fileExt.Contains(fileExt)){return new JsonResult(new { code = 403, success = false, msg = "文件类型不在允许范围内" });}if (_fileList.Value.ServerUploadFileList.Value.ContainsKey(model.FileMd5)){return new JsonResult(new { Code = 200, IsExist = true, success = true, FileUrl = _fileList.Value.ServerUploadFileList.Value[model.FileMd5], miaochuan = true });}//检测的时候创建待上传文件的分块MD5容器_fileList.Value.UploadChunkFileList.Value.TryAdd(model.FileGuid, new ConcurrentBag<string>());return new JsonResult(new { Code = 200, IsExist = false, FileUrl = "" });}#endregion#region 文件块秒传检测public JsonResult OnPostFileChunkAsync(UploadFileChunkModel model){if (model == null || string.IsNullOrWhiteSpace(model.Md5) || string.IsNullOrWhiteSpace(model.FileId)){return new JsonResult(new { Code = 300, IsExist = false, success = false, FileUrl = "", Msg = "参数不正确" });}if (!_fileList.Value.UploadChunkFileList.Value.ContainsKey(model.FileId)){return new JsonResult(new { Code = 200, IsExist = false, FileUrl = "" });}if (!_fileList.Value.UploadChunkFileList.Value[model.FileId].Contains(model.Md5)){return new JsonResult(new { Code = 200, IsExist = false, FileUrl = "" });}return new JsonResult(new { Code = 200, IsExist = true, success = true, miaochuan = true });}#endregion#region 解压、校验文件private void DoUnZip(string zipFile, string user){Task.Factory.StartNew(async () =>{if (!System.IO.File.Exists(zipFile)){//发送一条文件不存在的消息await SentMessage(user, "访问上传的压缩包失败");return;}var fastZip = new FastZip{Password = "123456",CreateEmptyDirectories = true};try{var zipExtDir = bine(AppDomain.CurrentDomain.BaseDirectory, "ZipEx", "601018");//删除现有文件夹if (Directory.Exists(zipExtDir))Directory.Delete(zipExtDir, true);//发送开始解压缩信息await SentMessage(user, "开始解压缩文件。。。");#if DEBUGDebug.WriteLine("开始解压缩文件。。。");#endiffastZip.ExtractZip(zipFile, zipExtDir, "");#if DEBUGDebug.WriteLine("解压缩文件成功。。。");#endifawait SentMessage(user, "解压缩文件成功,开始校验。。。");//发送解压成功并开始校验文件信息var zipFiles = Directory.GetFiles(zipExtDir, "*.jpg", SearchOption.AllDirectories);for (var i = 0; i < zipFiles.Length; i++){var file = zipFiles[i];var i1 = i + 1;await Task.Delay(100);//模拟文件处理需要100毫秒//发送进度 i/lengthawait SentMessage(user, $"校验进度==>{i1}/{zipFiles.Length}", "400");#if DEBUGDebug.WriteLine($"当前进度:{i1},总数:{zipFiles.Length}");#endif}await SentMessage(user, "校验完成", "200");}catch (Exception exception){//发送解压缩失败信息await SentMessage(user, $"解压缩文件失败:{exception}", "500");#if DEBUGDebug.WriteLine($"解压缩文件失败:{exception}");#endif}}, TaskCreationOptions.LongRunning);}#endregion#region 消息推送前台private async Task SentMessage(string user, string content, string code = "300"){await _hubContext.Clients.Client(user).UploadInfoMessage(new ClientMessageModel{UserId = user,GroupName = "upload",Context = content,Code = code});}#endregion}}