ASP.NET CORE使用WebUploader對大文件分片上傳,並通過ASP.NET CORE SignalR實時反饋後台處理進度給前端展示
- 2021 年 4 月 26 日
- 筆記
- .netcore, SignalR, WebUploader實現大文件分片上傳
本次,我們來實現一個單個大文件上傳,並且把後台對上傳文件的處理進度通過ASP.NET CORE SignalR反饋給前端展示,比如上傳一個大的zip壓縮包文件,後台進行解壓縮,並且對壓縮包中的文件進行md5校驗,同時要求前台可以實時(實際情況看網絡情況)展示後台對壓縮包的處理進度(解壓、校驗文件)。
在前端上傳文件的組件選擇上,採用了WebUploader(//fex.baidu.com/webuploader/)這個優秀的前端組件,下面是來自它的官網介紹:
WebUploader是由Baidu WebFE(FEX)團隊開發的一個簡單的以HTML5為主,FLASH為輔的現代文件上傳組件。在現代的瀏覽器裏面能充分發揮HTML5的優勢,同時又不摒棄主流IE瀏覽器,沿用原來的FLASH運行時,兼容IE6+,iOS 6+, android 4+。兩套運行時,同樣的調用方式,可供用戶任意選用。
採用大文件分片並發上傳,極大的提高了文件上傳效率。
WebUploader的功能很多,本次只使用它的上傳前文件MD5校驗、並發分片上傳、分片MD5校驗三個主要功能,分別來實現類似網盤中的文件【秒傳】,瀏覽器多線程上傳文件和文件的斷點續傳。
閱讀參考此文章前,請先看一下//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="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停止運行。
本示例只能解壓縮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、分塊傳輸時文件斷點續傳沒有具體實現(理論上是沒問題的)
參考文章:
//www.cnblogs.com/wdw984/p/11725118.html
//fex.baidu.com/webuploader/
如此文章對你有幫助,請點個推薦吧。謝謝!