<ul id="qxxfc"><fieldset id="qxxfc"><tr id="qxxfc"></tr></fieldset></ul>


      目錄

      * 前言 <https://www.cnblogs.com/AprilBlank/p/11399509.html#前言>
      * 開始 <https://www.cnblogs.com/AprilBlank/p/11399509.html#開始>
      * 測試 <https://www.cnblogs.com/AprilBlank/p/11399509.html#測試>
      * 跨域 <https://www.cnblogs.com/AprilBlank/p/11399509.html#跨域>
      * 小結(jié) <https://www.cnblogs.com/AprilBlank/p/11399509.html#小結(jié)>
      @

      前言

      在之前整理完一套簡單的后臺基礎(chǔ)工程后,因為業(yè)務(wù)需要鼓搗了文件上傳跟下載,整理完后就迫不及待的想分享出來,希望有用到文件相關(guān)操作的朋友可以得到些幫助。

      開始

      我們依然用我們的基礎(chǔ)工程,之前也提到過后續(xù)如果有測試功能之類的東西,會一直不斷的更新這套代碼(如果搞炸了之后那就…),首先我們需要理一下文件分片上傳的思路:

      * 后端
      * 接收前端文件上傳請求并處理回調(diào)
      * 根據(jù)前端傳遞的鑰匙判斷,允許后開始接收文件流并保存到臨時文件夾
      * 前端最終上傳完成后給予后端合并請求(也稱作上傳完成確認),后端合并文件后判斷最終文件是否正確給予回調(diào)。
      * 前端
      * 讀取文件相關(guān)信息(名稱,擴展類型,大小等基本信息)
      * 根據(jù)需要做片段劃分以及文件的md5值(md5主要用于最終確認文件是否缺損)
      * 請求后端獲取鑰匙
      * 拿到鑰匙后,我們根據(jù)劃分的片段去循環(huán)上傳文件,并根據(jù)每次回調(diào)判斷是否上傳成功,如失敗則重新上傳
      * 最終循環(huán)完成后,給予后端合并請求(上傳完成確認)
      ps:這里的鑰匙就是個文件名,當然你可以來個token啊什么的根據(jù)自己業(yè)務(wù)需要。


      這里還是想分享下敲代碼的經(jīng)驗,在我們動手之前,最好把能考慮到的東西全都想好,思路理清也就是打好提綱后,敲代碼的效率會高并且錯誤率也會低,行云流水不是天馬行空,而是你的大腦中已經(jīng)有了山水鳥獸。

      OK,流程清楚之后,我們開始動手敲代碼吧。

      首先,我們新建一個控制器FileController,當然名字可以隨意取,根據(jù)我們上述后端的思路,新建三個接口RequestUploadFile,
      FileSave,F(xiàn)ileMerge。
      [Route("api/[controller]")] [ApiController] public class FileController :
      ControllerBase { /// <summary> /// 請求上傳文件 /// </summary> /// <param
      name="requestFile">請求上傳參數(shù)實體</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">文件參數(shù)信息[name]</param> ///
      <returns></returns> [HttpPost, Route("Merge")] public async Task<MessageEntity>
      FileMerge([FromBody]Dictionary<string, object> fileInfo) { } }
      如果直接復(fù)制的朋友,這里肯定是滿眼紅彤彤,這里主要用了兩個類,一個請求實體RequestFileUploadEntity,一個回調(diào)實體
      MessageEntity
      ,這兩個我們到Util工程創(chuàng)建(當然也可以放到Entity工程,這里為什么放到Util呢,因為我覺得放到這里公用比較好,畢竟還是有復(fù)用的價值的)。
      /// <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> /// 片段數(shù)量 /// </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> /// 狀態(tài)標識 ///
      </summary> public int Code { get => _Code; set => _Code = value; } ///
      <summary> /// 返回消息 /// </summary> public string Msg { get => _Msg; set => _Msg
      = value; } /// <summary> /// 返回數(shù)據(jù) /// </summary> public object Data { get =>
      _Data; set => _Data = value; } }
      創(chuàng)建完成寫好之后我們在紅的地方Alt+Enter,哪里爆紅點哪里(so easy),好了,不扯犢子了,每個接口的方法如下。

      RequestUploadFile
      public MessageEntity RequestUploadFile([FromBody]RequestFileUploadEntity
      requestFile) { LogUtil.Debug($"RequestUploadFile
      接收參數(shù):{JsonConvert.SerializeObject(requestFile)}"); MessageEntity message = new
      MessageEntity(); if (requestFile.size <= 0 || requestFile.count <= 0 ||
      string.IsNullOrEmpty(requestFile.filedata)) { message.Code = -1; message.Msg =
      "參數(shù)有誤"; } else { //這里需要記錄文件相關(guān)信息,并返回文件guid名,后續(xù)請求帶上此參數(shù) string guidName =
      Guid.NewGuid().ToString("N"); //前期單臺服務(wù)器可以記錄Cache,多臺后需考慮redis或數(shù)據(jù)庫
      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開始執(zhí)行獲取數(shù)據(jù):{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($"目錄文件下文件總數(shù):{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(); //這個地方會引發(fā)文件被占用異常
      fs = new FileStream(fileFinalName, FileMode.Open); string strMd5 =
      GetCryptoString(fs); LogUtil.Debug($"文件數(shù)據(jù)MD5:{strMd5}");
      LogUtil.Debug($"文件上傳數(shù)據(jù):{JsonConvert.SerializeObject(requestFile)}");
      fs.Close(); Directory.Delete(filePath); //如果MD5與原MD5不匹配,提示重新上傳 if (strMd5 !=
      requestFile.filedata) {
      LogUtil.Debug($"上傳文件md5:{requestFile.filedata},服務(wù)器保存文件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) { //加密的二進制轉(zhuǎn)為string類型返回 StringBuilder
      sb = new StringBuilder(); for (int i = 0; i < cryptBytes.Length; i++) {
      sb.Append(cryptBytes[i].ToString("x2")); } return sb.ToString(); }
      測試

      方法寫好了之后,我們需不需要測試呢,那不是廢話么,自己的代碼不過一遍等著讓測試人員搞你呢。


      再說個編碼習慣,就是自己的代碼自己最起碼常規(guī)的過一遍,也不說跟大廠一樣什么KPI啊啥的影響,自己的東西最起碼拿出手讓人一看知道用心了就行,不說什么測試全覆蓋,就是1+1=2這種基本的正常就OK。

      程序運行之后,我這里寫了個簡單的測試界面,運行之后發(fā)現(xiàn)提示OPTIONS,果斷跨域錯誤,還記得我們之前提到的跨域問題,這里給出解決方法。


      跨域


      跨域,就是我在這個區(qū)域,想跟另一個區(qū)域聯(lián)系的時候,我們會碰到墻,這堵墻的目的就是,禁止不同區(qū)域的人私下交流溝通,但是現(xiàn)在我們就是不要這堵墻或者說要開幾個門的話怎么做呢,net
      core有專門設(shè)置的地方,我們回到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就是加個巡邏官,看看符合規(guī)則的放進來,不符合的直接趕出去。

      方法 介紹
      AllowAnyOrigin 允許所有的域名請求
      AllowAnyMethod 允許所有的請求方式GET/POST/PUT/DELETE
      AllowAnyHeader 允許所有的頭部參數(shù)
      AllowCredentials 允許攜帶Cookie
      這里我使用的是允許所有,可以根據(jù)自身業(yè)務(wù)需要來調(diào)整,比如只允許部分域名訪問,部分請求方式,部分Header:
      //只是示例,具體根據(jù)自身需要 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(); }
      好了,設(shè)置好跨域之后我們再來執(zhí)行下上傳操作。


      我們看到這個提示之后,是不是能想起來什么,我們之前做過中間層不知道還記得不,忘了的朋友可以再看下net core
      Webapi基礎(chǔ)工程搭建(七)——小試AOP及常規(guī)測試_Part 1
      <https://www.cnblogs.com/AprilBlank/p/11282409.html>。
      在appsettings.json添加上接口白名單。
      "AllowUrl":
      "/api/Values,/api/File/RequestUpload,/api/File/Upload,/api/File/Merge"
      設(shè)置好之后,我們繼續(xù)上傳,這次總算是可以了(文件后綴這個忽略,測試使用,js就是做了個簡單的substring)。


      我們來查看上傳文件記錄的日志信息。

      再來我們看下文件存儲的位置,這個位置我們在appsettings里面已經(jīng)設(shè)置過,可以根據(jù)自己業(yè)務(wù)需要調(diào)整。


      打開文件看下是否有損壞,壓縮包很容易看出來是否正常,只要能打開基本上(當然可能會有問題)沒問題。


      解壓出來如果正常那肯定就是沒問題了吧(壓縮這個玩意兒真是牛逼,節(jié)省了多少的存儲空間,雖說硬盤白菜價)。


      小結(jié)

      在整理文件上傳這篇剛好捎帶著把跨域也簡單了過了一遍,下來需要再折騰的東西就是大文件的分片下載
      ,大致的思路與文件上傳一致,畢竟都是一個大蛋糕,切成好幾塊,你一塊,剩下的都是我的。

      友情鏈接
      ioDraw流程圖
      API參考文檔
      OK工具箱
      云服務(wù)器優(yōu)惠
      阿里云優(yōu)惠券
      騰訊云優(yōu)惠券
      京東云優(yōu)惠券
      站點信息
      問題反饋
      郵箱:[email protected]
      QQ群:637538335
      關(guān)注微信

        <ul id="qxxfc"><fieldset id="qxxfc"><tr id="qxxfc"></tr></fieldset></ul>
          人人干人人色 | 韩国成人三级 | av色在线| 91在线成人免费视频 | 含紧一点h边做边上课教室 | 亚洲无码三级电影 | 在线观看免费人成a片 | 国产精品国产三级国语电影有那些 | 婷婷综合六月天 | 啊灬啊别停灬用力啊岳小说 |