[toc]html
在以前整理完一套簡單的後臺基礎工程後,由於業務須要鼓搗了文件上傳跟下載,整理完後就火燒眉毛的想分享出來,但願有用到文件相關操做的朋友能夠獲得些幫助。前端
咱們依然用咱們的基礎工程,以前也提到事後續若是有測試功能之類的東西,會一直不斷的更新這套代碼(若是搞炸了以後那就…),代碼下載地址在net core Webapi 總目錄,首先咱們須要理一下文件分片上傳的思路:redis
ps:這裏的鑰匙就是個文件名,固然你能夠來個token啊什麼的根據本身業務須要。數據庫
這裏仍是想分享下敲代碼的經驗,在咱們動手以前,最好把能考慮到的東西全都想好,思路理清也就是打好提綱後,敲代碼的效率會高而且錯誤率也會低,行雲流水不是天馬行空,而是你的大腦中已經有了山水鳥獸。json
OK,流程清楚以後,咱們開始動手敲代碼吧。後端
首先,咱們新建一個控制器FileController,固然名字能夠隨意取,根據咱們上述後端的思路,新建三個接口RequestUploadFile,FileSave,FileMerge。api
[Route("api/[controller]")] [ApiController] public class FileController : ControllerBase { /// <summary> /// 請求上傳文件 /// </summary> /// <param name="requestFile">請求上傳參數實體</param> /// <returns></returns> [HttpPost, Route("RequestUpload")] public MessageEntity RequestUploadFile([FromBody]RequestFileUploadEntity requestFile) { } /// <summary> /// 文件上傳 /// </summary> /// <returns></returns> [HttpPost, Route("Upload")] public async Task<MessageEntity> FileSave() { } /// <summary> /// 文件合併 /// </summary> /// <param name="fileInfo">文件參數信息[name]</param> /// <returns></returns> [HttpPost, Route("Merge")] public async Task<MessageEntity> FileMerge([FromBody]Dictionary<string, object> fileInfo) { } }
若是直接複製的朋友,這裏確定是滿眼紅彤彤,這裏主要用了兩個類,一個請求實體RequestFileUploadEntity,一個回調實體MessageEntity,這兩個咱們到Util工程建立(固然也能夠放到Entity工程,這裏爲何放到Util呢,由於我以爲放到這裏公用比較好,畢竟仍是有複用的價值的)。跨域
/// <summary> /// 文件請求上傳實體 /// </summary> public class RequestFileUploadEntity { private long _size = 0; private int _count = 0; private string _filedata = string.Empty; private string _fileext = string.Empty; private string _filename = string.Empty; /// <summary> /// 文件大小 /// </summary> public long size { get => _size; set => _size = value; } /// <summary> /// 片斷數量 /// </summary> public int count { get => _count; set => _count = value; } /// <summary> /// 文件md5 /// </summary> public string filedata { get => _filedata; set => _filedata = value; } /// <summary> /// 文件類型 /// </summary> public string fileext { get => _fileext; set => _fileext = value; } /// <summary> /// 文件名 /// </summary> public string filename { get => _filename; set => _filename = value; } }
/// <summary> /// 返回實體 /// </summary> public class MessageEntity { private int _Code = 0; private string _Msg = string.Empty; private object _Data = new object(); /// <summary> /// 狀態標識 /// </summary> public int Code { get => _Code; set => _Code = value; } /// <summary> /// 返回消息 /// </summary> public string Msg { get => _Msg; set => _Msg = value; } /// <summary> /// 返回數據 /// </summary> public object Data { get => _Data; set => _Data = value; } }
建立完成寫好以後咱們在紅的地方Alt+Enter,哪裏爆紅點哪裏(so easy),好了,不扯犢子了,每一個接口的方法以下。服務器
RequestUploadFileapp
public MessageEntity RequestUploadFile([FromBody]RequestFileUploadEntity requestFile) { LogUtil.Debug($"RequestUploadFile 接收參數:{JsonConvert.SerializeObject(requestFile)}"); MessageEntity message = new MessageEntity(); if (requestFile.size <= 0 || requestFile.count <= 0 || string.IsNullOrEmpty(requestFile.filedata)) { message.Code = -1; message.Msg = "參數有誤"; } else { //這裏須要記錄文件相關信息,並返回文件guid名,後續請求帶上此參數 string guidName = Guid.NewGuid().ToString("N"); //前期單臺服務器能夠記錄Cache,多臺後需考慮redis或數據庫 CacheUtil.Set(guidName, requestFile, new TimeSpan(0, 10, 0), true); message.Code = 0; message.Msg = ""; message.Data = new { filename = guidName }; } return message; }
FileSave
public async Task<MessageEntity> FileSave() { var files = Request.Form.Files; long size = files.Sum(f => f.Length); string fileName = Request.Form["filename"]; int fileIndex = 0; int.TryParse(Request.Form["fileindex"], out fileIndex); LogUtil.Debug($"FileSave開始執行獲取數據:{fileIndex}_{size}"); MessageEntity message = new MessageEntity(); if (size <= 0 || string.IsNullOrEmpty(fileName)) { message.Code = -1; message.Msg = "文件上傳失敗"; return message; } if (!CacheUtil.Exists(fileName)) { message.Code = -1; message.Msg = "請從新請求上傳文件"; return message; } long fileSize = 0; string filePath = $".{AprilConfig.FilePath}{DateTime.Now.ToString("yyyy-MM-dd")}/{fileName}"; string saveFileName = $"{fileName}_{fileIndex}"; string dirPath = Path.Combine(filePath, saveFileName); if (!Directory.Exists(filePath)) { Directory.CreateDirectory(filePath); } foreach (var file in files) { //若是有文件 if (file.Length > 0) { fileSize = 0; fileSize = file.Length; using (var stream = new FileStream(dirPath, FileMode.OpenOrCreate)) { await file.CopyToAsync(stream); } } } message.Code = 0; message.Msg = ""; return message; }
FileMerge
public async Task<MessageEntity> FileMerge([FromBody]Dictionary<string, object> fileInfo) { MessageEntity message = new MessageEntity(); string fileName = string.Empty; if (fileInfo.ContainsKey("name")) { fileName = fileInfo["name"].ToString(); } if (string.IsNullOrEmpty(fileName)) { message.Code = -1; message.Msg = "文件名不能爲空"; return message; } //最終上傳完成後,請求合併返回合併消息 try { RequestFileUploadEntity requestFile = CacheUtil.Get<RequestFileUploadEntity>(fileName); if (requestFile == null) { message.Code = -1; message.Msg = "合併失敗"; return message; } string filePath = $".{AprilConfig.FilePath}{DateTime.Now.ToString("yyyy-MM-dd")}/{fileName}"; string fileExt = requestFile.fileext; string fileMd5 = requestFile.filedata; int fileCount = requestFile.count; long fileSize = requestFile.size; LogUtil.Debug($"獲取文件路徑:{filePath}"); LogUtil.Debug($"獲取文件類型:{fileExt}"); string savePath = filePath.Replace(fileName, ""); string saveFileName = $"{fileName}{fileExt}"; var files = Directory.GetFiles(filePath); string fileFinalName = Path.Combine(savePath, saveFileName); LogUtil.Debug($"獲取文件最終路徑:{fileFinalName}"); FileStream fs = new FileStream(fileFinalName, FileMode.Create); LogUtil.Debug($"目錄文件下文件總數:{files.Length}"); LogUtil.Debug($"目錄文件排序前:{string.Join(",", files.ToArray())}"); LogUtil.Debug($"目錄文件排序後:{string.Join(",", files.OrderBy(x => x.Length).ThenBy(x => x))}"); byte[] finalBytes = new byte[fileSize]; foreach (var part in files.OrderBy(x => x.Length).ThenBy(x => x)) { var bytes = System.IO.File.ReadAllBytes(part); await fs.WriteAsync(bytes, 0, bytes.Length); bytes = null; System.IO.File.Delete(part);//刪除分塊 } fs.Close(); //這個地方會引起文件被佔用異常 fs = new FileStream(fileFinalName, FileMode.Open); string strMd5 = GetCryptoString(fs); LogUtil.Debug($"文件數據MD5:{strMd5}"); LogUtil.Debug($"文件上傳數據:{JsonConvert.SerializeObject(requestFile)}"); fs.Close(); Directory.Delete(filePath); //若是MD5與原MD5不匹配,提示從新上傳 if (strMd5 != requestFile.filedata) { LogUtil.Debug($"上傳文件md5:{requestFile.filedata},服務器保存文件md5:{strMd5}"); message.Code = -1; message.Msg = "MD5值不匹配"; return message; } CacheUtil.Remove(fileInfo["name"].ToString()); message.Code = 0; message.Msg = ""; } catch (Exception ex) { LogUtil.Error($"合併文件失敗,文件名稱:{fileName},錯誤信息:{ex.Message}"); message.Code = -1; message.Msg = "合併文件失敗,請從新上傳"; } return message; }
這裏說明下,在Merge的時候,主要校驗md5值,用到了一個方法,我這裏沒有放到Util(實際上是由於懶),代碼以下:
/// <summary> /// 文件流加密 /// </summary> /// <param name="fileStream"></param> /// <returns></returns> private string GetCryptoString(Stream fileStream) { MD5 md5 = new MD5CryptoServiceProvider(); byte[] cryptBytes = md5.ComputeHash(fileStream); return GetCryptoString(cryptBytes); } private string GetCryptoString(byte[] cryptBytes) { //加密的二進制轉爲string類型返回 StringBuilder sb = new StringBuilder(); for (int i = 0; i < cryptBytes.Length; i++) { sb.Append(cryptBytes[i].ToString("x2")); } return sb.ToString(); }
方法寫好了以後,咱們需不須要測試呢,那不是廢話麼,本身的代碼不過一遍等着讓測試人員搞你呢。
再說個編碼習慣,就是本身的代碼本身最起碼常規的過一遍,也不說跟大廠同樣什麼KPI啊啥的影響,本身的東西最起碼拿出手讓人一看知道用心了就行,不說什麼測試全覆蓋,就是1+1=2這種基本的正常就OK。
程序運行以後,我這裏寫了個簡單的測試界面,運行以後發現提示OPTIONS,果斷跨域錯誤,還記得咱們以前提到的跨域問題,這裏給出解決方法。
跨域,就是我在這個區域,想跟另外一個區域聯繫的時候,咱們會碰到牆,這堵牆的目的就是,禁止不一樣區域的人私下交流溝通,可是如今咱們就是不要這堵牆或者說要開幾個門的話怎麼作呢,net core有專門設置的地方,咱們回到Startup這裏。
咱們來看新增的代碼:
public IServiceProvider ConfigureServices(IServiceCollection services) { //…以前的代碼忽略 services.AddCors(options => { options.AddPolicy("AllowAll", p => { p.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); }); }); services.AddAspectCoreContainer(); return services.BuildAspectInjectorProvider(); }
AddCors來添加一個跨域處理方式,AddPolicy就是加個巡邏官,看看符合規則的放進來,不符合的直接趕出去。
方法 | 介紹 |
---|---|
AllowAnyOrigin | 容許全部的域名請求 |
AllowAnyMethod | 容許全部的請求方式GET/POST/PUT/DELETE |
AllowAnyHeader | 容許全部的頭部參數 |
AllowCredentials | 容許攜帶Cookie |
這裏我使用的是容許全部,能夠根據自身業務須要來調整,好比只容許部分域名訪問,部分請求方式,部分Header:
//只是示例,具體根據自身須要 services.AddCors(options => { options.AddPolicy("AllowSome", p => { p.WithOrigins("https://www.baidu.com") .WithMethods("GET", "POST") .WithHeaders(HeaderNames.ContentType, "x-custom-header"); }); });
寫好以後咱們在Configure中聲明註冊使用哪一個巡邏官。
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { //…以前的 app.UseCors("AllowAll"); app.UseHttpsRedirection(); app.UseMvc(); }
好了,設置好跨域以後咱們再來執行下上傳操做。
咱們看到這個提示以後,是否是能想起來什麼,咱們以前作過中間層不知道還記得不,忘了的朋友能夠再看下net core Webapi基礎工程搭建(七)——小試AOP及常規測試_Part 1。 在appsettings.json添加上接口白名單。
"AllowUrl": "/api/Values,/api/File/RequestUpload,/api/File/Upload,/api/File/Merge"
設置好以後,咱們繼續上傳,此次總算是能夠了(文件後綴這個忽略,測試使用,js就是作了個簡單的substring)。
咱們來查看上傳文件記錄的日誌信息。 再來咱們看下文件存儲的位置,這個位置咱們在appsettings裏面已經設置過,能夠根據本身業務須要調整。
打開文件看下是否有損壞,壓縮包很容易看出來是否正常,只要能打開基本上(固然可能會有問題)沒問題。
解壓出來若是正常那確定就是沒問題了吧(壓縮這個玩意兒真是牛逼,節省了多少的存儲空間,雖然說硬盤白菜價)。
在整理文件上傳這篇恰好捎帶着把跨域也簡單了過了一遍,下來須要再折騰的東西就是大文件的分片下載,大體的思路與文件上傳一致,畢竟都是一個大蛋糕,切成好幾塊,你一塊,剩下的都是個人。