[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里面已经设置过,能够根据本身业务须要调整。
打开文件看下是否有损坏,压缩包很容易看出来是否正常,只要能打开基本上(固然可能会有问题)没问题。
解压出来若是正常那确定就是没问题了吧(压缩这个玩意儿真是牛逼,节省了多少的存储空间,虽然说硬盘白菜价)。
在整理文件上传这篇恰好捎带着把跨域也简单了过了一遍,下来须要再折腾的东西就是大文件的分片下载,大体的思路与文件上传一致,毕竟都是一个大蛋糕,切成好几块,你一块,剩下的都是个人。