本次,我们来实现一个单个大文件上传,并且把后台对上传文件的处理进度通过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;
当点击(开始)上传的时候,触发事件的流程为:
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 MicrosoftWarning 6 Microsoft.Hosting.Lifetime" 7 } 8 },1)"> 9 FileUpload10 TempPathtemp",//临时文件保存目录 11 FileDirupload上传完成后的保存目录 12 FileExtzip,rar允许上传的文件类型 13 14 AllowedHosts*15 }
在项目中新建一个Model目录,用来实现上传文件的相关配置,建立相应的多个类文件
FileUploadConfig.cs 服务器用来接受和保存文件的配置
using System; 2 3 namespace signalr.Model 4 5 /// <summary> 6 /// 上传文件配置类 7 </summary> [Serializable] 9 public class FileUploadConfig 10 { 11 12 临时文件夹目录名 13 14 string TempPath { get; set; } 15 16 上传文件保存目录名 17 18 string FileDir { 19 20 允许上传的文件扩展名 21 22 string FileExt { 23 24 }
UploadFileWholeModel.cs 前台开始传输前会对文件进行一次MD5算法,这里可以通过文件MD5值传递给后台来通过比对已上传的文件MD5值列表来实现秒传功能
2 4 文件秒传检测前台传递参数 6 UploadFileWholeModel 8 9 请求类型,这里固定为:whole 10 11 string CheckType { 文件的MD5 14 15 string FileMd5 { 前台文件的唯一标识 18 19 string FileGuid { 前台上传文件名 22 23 string FileName { 24 25 文件大小 26 27 int? FileSize { 28 29 }
UploadFileChunkModel.cs 前台文件分块传输的时候会对分块传输内容进行MD5计算,并且分块传输的时候会传递当前分块的一些信息,这里对应的后台接收实体类。
我们可以通过分块传输的MD5值来实现文件续传功能(如文件的某块MD5已存在则返回给前台跳过当前块)
文件分块(续传)传递参数 UploadFileChunkModel 文件分块传输检测类型,这里固定为chunk 文件的总大小 long? FileSize { 当前块所属文件编号 string FileId { 当前块基于文件的开始偏移量 long? ChunkStart { 当前块基于文件的结束偏移量 long? ChunkEnd { 28 29 当前块的大小 30 31 long? ChunkSize { 32 33 当前块编号 34 35 string ChunkIndex { 36 37 当前文件分块总数 38 39 string ChunkCount { 40 41 当前块的编号 42 43 string ChunkId { 44 45 当前块的md5 46 47 string Md5 { 48 49 }
FormData.cs 这是分块传输时传递的当前块的信息配置
上传文件时的附加信息 FormData 当前请求类型 分片传输是:chunk string Checktype { 文件总字节数 int? Filesize { 文件唯一编号 string Fileid { 23 分片数据大小 26 int? Chunksize { 27 当前分片编号 30 int? Chunkindex { 31 分片起始编译量 34 int? Chunkstart { 35 分片结束编译量 38 int? Chunkend { 39 分片总数量 42 int? Chunkcount { 43 当前分片唯一编号 46 string Chunkid { 47 48 当前块MD5值 49 50 51 52 }
UploadFileModel.cs 每次上传文件的时候,前台都会传递这些参数给服务器,服务器可以根据参数做相应的处理
Microsoft.AspNetCore.Mvc; 3 5 WebUploader上传文件实体类 8 9 10 UploadFileModel 11 前台WebUploader的ID string Id { 当前文件(块)的前端计算的md5 当前文件块号 string Chunk { 原始文件名 string Name { 文件类型(如:image/png) 31 [FromForm(Name = type)] 32 string FileType { 当前文件(块)的大小 36 long? Size { 前台给此文件分配的唯一编号 40 string Guid { 附件信息 44 public FormData FromData { Post过来的数据容器 48 byte[] FileData { 49 50 }
UploadFileMergeModel.cs 当所有块传输完成后,传递给后台一个合并文件的请求,后台通过参数中的信息把分块保存的文件合并成一个完整的文件
文件合并请求参数类 UploadFileMergeModel 请求类型 前台检测到的文件大小 前台返回文件总块数 int? ChunkNumber { 前台返回文件的md5值 前台返回上传文件唯一标识 文件扩展名,不包含. 32 33 }
为了实现【秒传】和分块传输时的【断点续传】功能,我们在Class目录中定义一个UploadFileList.cs类,用来模拟持久化保存服务器所接收到的文件MD5校验列表和已接收的分块MD5值信息,这里我们使用了并发线程安全的ConcurrentDictionary和ConcurrentBag
System.Collections.Concurrent; signalr.Class UploadFileList 8 private static readonly Lazy<ConcurrentDictionary<string,1)">string>> _serverUploadFileList = new Lazy<ConcurrentDictionary<string>>(); 9 string>>> _uploadChunkFileList = 10 string>>>public UploadFileList() 12 { 13 ServerUploadFileList = _serverUploadFileList; 14 UploadChunkFileList = _uploadChunkFileList; 15 } 16 服务器上已经存在的文件,key为文件的Md5,value为文件路径 20 ServerUploadFileList; 客户端分配上传文件时的记录信息,key为上传文件的唯一id,value为文件分片后的当前段的md5 24 UploadChunkFileList; 25 26 }
扩展一下HubInterface/IChatClient.cs 用来推送给前台展示后台处理的信息
interface IChatClient { <summary> 客户端接收数据触发函数名 </summary> <param name="clientMessageModel">消息实体类</param> <returns></returns> Task ReceiveMessage(ClientMessageModel clientMessageModel); Echart接收数据触发函数名 <param name="data">JSON格式的可以被Echarts识别的data数据 Task EchartsMessage(Array data); 客户端获取自己登录后的UID Task GetMyId(ClientMessageModel clientMessageModel); 上传成功后服务器处理数据时通知前台的信息内容 Task UploadInfoMessage(ClientMessageModel clientMessageModel); }
扩展一下Class/ClientMessageModel.cs
<summary> 服务端发送给客户端的信息 [Serializable] ClientMessageModel { 接收用户编号 </summary> string UserId { ; } 组编号 string GroupName { 发送的内容 string Context { 自定义的响应编码 string Code { ; } }
我们在Startup.cs中注入上传文件的配置,同时把前文的XSRF防护去掉,我们在前台请求的时候带上防护认证信息。
void ConfigureServices(IServiceCollection services) { services.AddSignalR(); services.AddRazorPages() services.AddSingleton<UploadFileList>();服务器上传的文件信息保存在内存中 services.AddOptions() .Configure<FileUploadConfig>(Configuration.GetSection("));服务器上传文件配置 }
在项目的wwwroot/js下新建一个uploader.js
"use strict"; var connection = new signalR.HubConnectionBuilder() .withUrl("/chatHub") .withAutomaticReconnect() .configureLogging(signalR.LogLevel.Debug) .build(); var user = ""; connection.on("GetMyId",1)">function (data) { user = data.userId; }); connection.on("ReceiveMessage",1)"> (data) { console.log(data.userId + data.context); }); connection.on("UploadInfoMessage",1)"> (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"));//展示后台返回信息 case "400": if ($("#process").length == 0) {//展示后台推送的文件处理进度 $('.modal-body').append($("<p id='process'>" + data.context + "</p>")); } $('#process').text(data.context); ; } }); connection.start().then( () { console.log("服务器已连接"); }).catch( (err) { return console.error(err.toString()); });
在项目的Pages/Shared中新建一个Razor布局页_LayoutUpload.cshtml
<!DOCTYPE html> <html> head> Meta charset="utf-8"name="viewport" content="width=device-width" /> link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" ="~/lib/webuploader/dist/webuploader.css" script type="text/javascript" src="~/lib/jquery/dist/jquery.min.js"></script="~/lib/webuploader/dist/webuploader.js"="~/lib/bootstrap/dist/js/bootstrap.min.js"title>@ViewBag.Title</> @await RenderSectionAsync("Scripts",required: false) body @RenderBody() >
在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 src="~/js/signalr/dist/browser/signalr.js" 10 ="~/js/uploader.js" 11 12 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,1)"> 21 $list = $('#thelist'),1)"> 22 $btn = $('#ctlBtn'),1)"> 23 state = 'pending',1)"> 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",1)"> 43 "before-send": "beforeSend",1)"> 44 "after-send-file": "afterSendFile" 45 },1)"> 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,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",1)"> 65 "filesize": file.size,1)"> 66 "filemd5": md5 67 68 69 }; 70 $.ajax({ 71 url: '/upload/FileWhole',//通过md5校验实现文件秒传 72 type: 'POST',1)"> 73 headers: {//请求的时候传递进去防CSRF攻击的认证信息 74 RequestVerificationToken: 75 $('input:hidden[name="__RequestVerificationToken"]').val() 76 },1)"> 77 data: params,1)"> 78 contentType: 'application/x-www-form-urlencoded',1)"> 79 async: true,// 开启异步请求 80 dataType: 'JSON',1)"> 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 92 owner.skipFile(file,window.WebUploader.File.Status.COMPLETE); 93 deferred.resolve(); 94 $('#' + file.id).find('p.state').text('上传成功【秒传】'); 95 96 } else { 97 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 },1)">109 //上传事件第二步:分块上传时,每个分块触发上传前执行 110 beforeSend: function(block) { 111 112 113 owner.md5File(block.file,block.start,block.end) 114 115 console.log("当前分块内容的MD5计算进度:",1)">116 117 118 119 120 121 //计算当前块的MD5值并写入数组 122 md5s[block.blob.uid] = md5; 123 deferred.resolve(); 124 125 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",1)">140 "filesize": file.size,1)">141 "chunknumber": chunkNumber,1)">142 "filemd5": file.md5,1)">143 "filename": file.guid,1)">144 "fileext": file.ext//扩展名 145 }; 146 $.ajax({ 147 type: "POST",1)">148 url: "/upload/FileMerge",1)">149 headers: { 150 RequestVerificationToken: 151 $('input:hidden[name="__RequestVerificationToken"]').val(),1)">152 userid:user //传递SignalR分配的编号 153 },1)">154 data: params,1)">155 async: true,1)">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 165 166 error: function() { 167 dataState = undefined; 168 169 } 170 }); 171 172 } 173 }); 174 uploader = window.WebUploader.create({ 175 resize: false,1)">176 fileNumLimit: 1,1)">177 swf: '/lib/webuploader/dist/Uploader.swf',1)">178 server: '/upload/FileSave',1)">179 pick: { id: '#picker',multiple: false },1)">180 chunked: true,1)">181 chunkSize: chunkSize,1)">182 chunkRetry: 3,1)">183 fileSizeLimit: fileTotalSize,1)">184 fileSingleSizeLimit: fileSingleSize,1)">185 formData: { 186 } 187 }); 188 uploader.on('beforeFileQueued',1)">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',1)">202 function(block,data,headers) { 203 var params = { 204 "checktype": "chunk",1)">205 "filesize": block.file.size,1)">206 "fileid": block.blob.ruid,1)">207 "chunksize": block.blob.size,1)">208 "chunkindex": block.chunk,1)">209 "chunkstart": block.start,1)">210 "chunkend": block.end,1)">211 "chunkcount": block.chunks,1)">212 "chunkid": block.blob.uid,1)">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',1)">223 224 $list.append('<div id="' + 225 file.id + 226 '" class="item"' + 227 'h4 ="info"228 file.name + 229 'h4230 'input ="hidden" id="h_' + 231 232 value233 file.guid + 234 '" />235 'p ="state">等待上传...p236 'div'); 237 238 239 // 文件上传过程中创建进度条实时显示。 240 uploader.on('uploadProgress',1)">241 function(file,percentage) { 242 var $li = $('#' + file.id),1)">243 $percent = $li.find('.progress .progress-bar'); 244 // 避免重复创建 245 if (!$percent.length) { 246 $percent = $('div ="progress progress-striped active"247 '="progress-bar" role="progressbar" style="width: 0%"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',1)">257 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"="btn btn-primary fright retry pbtn">重新上传button262 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 272 273 274 } else { 275 $('#' + file.id).find('p.state').text('服务器未能成功接收,状态:' + dataState.success); 276 277 278 279 280 uploader.on('uploadError',1)">281 282 $('#' + file.id).find('p.state').text('上传出错'); 283 284 //分块传输后,可以在这个事件中获取到服务器返回的信息,同时这里可以实现文件续传(块文件的MD5存在时,后台可以跳过保存步骤) 285 uploader.on('uploadAccept',1)">286 287 if (response.code !== 200) { 288 alert("上传出错:" + response.msg); 289 return false; 290 291 return true; 292 293 uploader.on('uploadComplete',1)">294 295 $('#' + file.id).find('.progress').fadeOut(); 296 297 298 uploader.on('all',1)">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 312 $btn.text('开始上传'); 313 314 315 $btn.on('click',1)">316 function() { 317 if (state === 'uploading') { 318 uploader.stop(); 319 } else if (state == 'done') { 320 window.location.reload(); 321 322 uploader.upload(); 323 324 325 }); 326 327 328 ="container"329 ="row"330 ="uploader"="wu-example"331 span style="color: red">请上传压缩包span332 ="form-group"="thelist"333 334 ="form-group"335 form method="post"336 ="picker"="webuploader-container"337 ="webuploader-pick">选择文件338 ="position: absolute; top: 0; left: 0; width: 88px; height: 34px; overflow: hidden; bottom: auto; right: auto;"339 ="file" name="webuploader-element-invisible" /> 340 label ="-ms-opacity: 0; opacity: 0; width: 100%; height: 100%; display: block; cursor: pointer; background: rgb(255,255);"label341 342 343 ="ctlBtn"="btn btn-success"="button">开始上传344 form345 346 347 348 349 350 ="modal fade"="myModal" tabindex="-1" aria-labelledby="exampleModalScrollableTitle"="display: none;" data-backdrop="static" aria-hidden="true"351 ="modal-dialog modal-dialog-scrollable"352 ="modal-content"353 ="modal-header"354 h5 ="modal-title"="exampleModalScrollableTitle">正在处理。。。h5355 ="close" data-dismiss="modal" aria-label="Close"356 357 358 359 ="modal-body"360 >服务器正在处理数据,请不要关闭和刷新此页面。361 362 363 364 >
本示例只能解压缩zip文件,并且密码是123456,友情提示,不要用QQ浏览器调试,否则会遇到选择文件后DEBUG停止运行。
本示例只能解压缩zip文件,并且密码是123456,友情提示,不要用QQ浏览器调试,否则会遇到选择文件后DEBUG停止运行。
本示例只能解压缩zip文件,并且密码是123456,友情提示,不要用QQ浏览器调试,否则会遇到选择文件后DEBUG停止运行。
ICSharpCode.SharpZipLib.Zip; Microsoft.AspNetCore.Http; Microsoft.AspNetCore.Mvc.RazorPages; Microsoft.AspNetCore.SignalR; Microsoft.Extensions.Options; signalr.Class; signalr.HubInterface; signalr.Hubs; signalr.Model; 11 System.Diagnostics; System.IO; System.Linq; System.Text.Json; System.Threading.Tasks; 18 signalr.Pages.upload 21 IndexModel : PageModel 23 readonly IOptionsSnapshot<FileUploadConfig> _fileUploadConfig; 24 readonly IOptionsSnapshot<UploadFileList> _fileList; 25 readonly string[] _fileExt; 26 readonly IHubContext<ChatHub,IChatClient> _hubContext; 27 public IndexModel(IOptionsSnapshot<FileUploadConfig> fileUploadConfig,IOptionsSnapshot<UploadFileList> fileList,IHubContext<ChatHub,1)"> hubContext) 29 _fileUploadConfig = fileUploadConfig; 30 _fileList = fileList; 31 _fileExt = _fileUploadConfig.Value.FileExt.Split('').ToArray(); 32 _hubContext = hubContext; 34 IActionResult OnGet() 36 ViewData[Token"] = 666 37 Page(); 39 40 #region 上传文件 41 42 43 上传文件 44 45 46 async Task<JsonResult> OnPostFileSaveAsync(IFormFile file,UploadFileModel model) 48 if (_fileUploadConfig.Value == null) 50 return new JsonResult(new { code = 400,msg = 服务器配置不正确 }); 52 53 if (file == null || file.Length < 1 55 404,1)">没有接收到要保存的文件 Request.EnableBuffering(); 58 var formData = Request.Form[formData]; 59 if (model == null || .IsNullOrWhiteSpace(formData)) 61 401,1)">没有接收到必要的参数 63 64 var request = model; 65 long.TryParse(Request.Form[size"],1)">out var fileSize); 66 request.Size = fileSize; 67 try 69 request.FromData = JsonSerializer.Deserialize<FormData>(formData,1)">new JsonSerializerOptions { PropertyNameCaseInsensitive = true 71 catch (Exception e) Debug.WriteLine(e); 75 76 if (request.FromData == 78 402,1)">参数错误 80 #if DEBUG 82 Debug.WriteLine($原文件名:{request.Name},文件编号:{request.Guid},文件块编号:{request.Chunk},文件Md5:{request.FileMd5},当前块UID:{request.FromData?.Chunkid},当前块MD5:{request.FromData?.Md5}); #endif 84 var fileExt = request.Name.Substring(request.Name.LastIndexOf(.') + ).ToLowerInvariant(); 85 if (!_fileExt.Contains(fileExt)) 87 403,1)">文件类型不在允许范围内 89 if (_fileList.Value.UploadChunkFileList.Value.ContainsKey(request.Guid)) 91 if (!_fileList.Value.UploadChunkFileList.Value[request.Guid].Any(x => .Equals(x,request.FromData.Md5,StringComparison.OrdinalIgnoreCase))) { _fileList.Value.UploadChunkFileList.Value[request.Guid].Add(request.FromData.Md5); 95 96 else 98 Debug.WriteLine($ContainsKey{request.FromData.Chunkindex}存在校验值{request.FromData.Md5} 99 200,1)">成功接收103 105 405,1)">接收失败,因为服务器没有找到此文件的容器,请重新上传106 107 108 var dirPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,_fileUploadConfig.Value.TempPath,request.Guid); 109 Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath); 113 114 var tempFile = string.Concat(dirPath,\\4,1)">0'),fileExt); 115 117 118 await using var fs = System.IO.File.OpenWrite(tempFile); 119 request.FileData = new byte[Convert.ToInt32(request.FromData.Chunksize ?? 0)]; 120 121 var memStream = MemoryStream(); 122 await file.CopyToAsync(memStream); 123 124 request.FileData = memStream.ToArray(); 125 126 await fs.WriteAsync(request.FileData,request.FileData.Length); 127 fs.FlushAsync(); 129 132 Debug.WriteLine($White Error:{e}134 _fileList.Value.UploadChunkFileList.Value.TryRemove(request.Guid,1)">out _); 136 false138 139 #endregion 140 141 #region 合并上传文件 142 143 144 合并分片上传的文件 145 146 <param name="mergeModel">前台传递的请求合并的参数</param> 147 148 OnPostFileMergeAsync(UploadFileMergeModel mergeModel) 150 await Task.Run(async () => 152 if (mergeModel == string.IsNullOrWhiteSpace(mergeModel.FileName) || 153 .IsNullOrWhiteSpace(mergeModel.FileMd5)) 155 300,success = false,count = 0,size = 合并失败,参数不正确。157 _fileExt.Contains(mergeModel.FileExt.ToLowerInvariant())) 159 161 162 var fileSavePath = ""163 _fileList.Value.ServerUploadFileList.Value.ContainsKey(mergeModel.FileMd5)) 165 合并块文件、删除临时文件 166 var chunks = Directory.GetFiles(Path.Combine(AppDomain.CurrentDomain.BaseDirectory,mergeModel.FileName),1)">*.*167 chunks.Any()) { 169 302,1)">未找到文件块信息,请重试。171 172 Directory.CreateDirectory(dirPath); 176 fileSavePath = Path.Combine(_fileUploadConfig.Value.FileDir,1)">177 string.Concat(mergeModel.FileName,mergeModel.FileExt)); 178 var fs = 179 new FileStream(Path.Combine(dirPath,mergeModel.FileExt)),FileMode.Create); 180 foreach (var file in chunks.OrderBy(x => x)) 182 Debug.WriteLine($"File==>{file}"); 183 var bytes = System.IO.File.ReadAllBytesAsync(file); 184 await fs.WriteAsync(bytes.AsMemory(186 Directory.Delete(Path.Combine(AppDomain.CurrentDomain.BaseDirectory,true); 187 188 189 _fileList.Value.ServerUploadFileList.Value.TryAdd(mergeModel.FileMd5,fileSavePath)) 191 301,1)">服务器保存文件失败,请重试。192 193 194 var user = Request.Headers[userid195 调用解压文件 196 if (string.Equals(mergeModel.FileExt.ToLowerInvariant(),1)">zip)) 197 198 DoUnZip(Path.Combine(AppDomain.CurrentDomain.BaseDirectory,fileSavePath),user.ToString()); 199 200 201 202 await SentMessage(user.ToString(),1)">服务器只能解压缩zip格式文件。200203 204 true,1)">上传成功 fileSavePath }); 205 206 207 208 209 210 211 #region 文件秒传检测、文件类型允许范围检测 212 JsonResult OnPostFileWholeAsync(UploadFileWholeModel model) 213 214 .IsNullOrWhiteSpace(model.FileMd5)) 215 216 new { Code = "",Msg = 参数不正确217 218 var fileExt = model.FileName.Substring(model.FileName.LastIndexOf(219 220 221 222 223 (_fileList.Value.ServerUploadFileList.Value.ContainsKey(model.FileMd5)) 224 225 227 检测的时候创建待上传文件的分块MD5容器 228 _fileList.Value.UploadChunkFileList.Value.TryAdd(model.FileGuid,1)">new ConcurrentBag<string>()); 229 230 232 233 234 #region 文件块秒传检测 235 JsonResult OnPostFileChunkAsync(UploadFileChunkModel model) 236 237 string.IsNullOrWhiteSpace(model.Md5) || .IsNullOrWhiteSpace(model.FileId)) 238 239 241 242 _fileList.Value.UploadChunkFileList.Value.ContainsKey(model.FileId)) 244 246 247 _fileList.Value.UploadChunkFileList.Value[model.FileId].Contains(model.Md5)) 248 249 251 252 253 254 255 #region 解压、校验文件 256 257 void DoUnZip(string zipFile,1)"> user) 259 Task.Factory.StartNew(261 System.IO.File.Exists(zipFile)) 263 发送一条文件不存在的消息 264 await SentMessage(user,1)">访问上传的压缩包失败265 267 var fastZip = FastZip 269 Password = 123456270 CreateEmptyDirectories = true 272 273 274 var zipExtDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,1)">ZipEx601018275 删除现有文件夹 276 (Directory.Exists(zipExtDir)) 277 Directory.Delete(zipExtDir,1)">278 发送开始解压缩信息 279 开始解压缩文件。。。281 Debug.WriteLine(283 fastZip.ExtractZip(zipFile,zipExtDir,1)">285 Debug.WriteLine(解压缩文件成功。。。287 解压缩文件成功,开始校验。。。288 发送解压成功并开始校验文件信息 289 var zipFiles = Directory.GetFiles(zipExtDir,1)">*.jpg290 for (var i = 0; i < zipFiles.Length; i++292 var file = zipFiles[i]; 293 var i1 = i + 294 await Task.Delay(100);模拟文件处理需要100毫秒 295 发送进度 i/length 296 校验进度==>{i1}/{zipFiles.Length}400297 298 Debug.WriteLine($当前进度:{i1},总数:{zipFiles.Length}301 校验完成303 (Exception exception) 305 发送解压缩失败信息 306 解压缩文件失败:{exception}500308 Debug.WriteLine($313 314 315 316 #region 消息推送前台 317 318 async Task SentMessage(string user,1)">string content,1)">string code = 300320 321 await _hubContext.Clients.Client(user).UploadInfoMessage( ClientMessageModel 323 UserId = user,1)">324 GroupName = 325 Context = content,1)">326 Code = code 329 331 332 }
未能完善的地方:
1、上传几百兆或更大的文件,webuploader计算md5时间太长;
3、分块传输时文件断点续传没有具体实现(理论上是没问题的)
参考文章:
https://www.cnblogs.com/wdw984/p/11725118.html
http://fex.baidu.com/webuploader/
如此文章对你有帮助,请点个推荐吧。谢谢!
原文链接:https://www.cnblogs.com/wdw984/p/14702514.html