前言

在之前整理完一套简单的后台基础工程后,因为业务需要鼓捣了文件上传跟下载,整理完后就迫不及待的想分享出来,希望有用到文件相关操作的朋友可以得到些帮助。

开始

我们依然用我们的基础工程,之前也提到过后续如果有测试功能之类的东西,会一直不断的更新这套代码(如果搞炸了之后那就…),代码下载地址在net core Webapi 总目录,首先我们需要理一下文件分片上传的思路:

  • 后端
  1. 接收前端文件上传请求并处理回调
  2. 根据前端传递的钥匙判断,允许后开始接收文件流并保存到临时文件夹
  3. 前端最终上传完成后给予后端合并请求(也称作上传完成确认),后端合并文件后判断最终文件是否正确给予回调。
  • 前端
  1. 读取文件相关信息(名称,扩展类型,大小等基本信息)
  2. 根据需要做片段划分以及文件的md5值(md5主要用于最终确认文件是否缺损)
  3. 请求后端获取钥匙
  4. 拿到钥匙后,我们根据划分的片段去循环上传文件,并根据每次回调判断是否上传成功,如失败则重新上传
  5. 最终循环完成后,给予后端合并请求(上传完成确认)

ps:这里的钥匙就是个文件名,当然你可以来个token啊什么的根据自己业务需要。

这里还是想分享下敲代码的经验,在我们动手之前,最好把能考虑到的东西全都想好,思路理清也就是打好提纲后,敲代码的效率会高并且错误率也会低,行云流水不是天马行空,而是你的大脑中已经有了山水鸟兽。

OK,流程清楚之后,我们开始动手敲代码吧。

首先,我们新建一个控制器FileController,当然名字可以随意取,根据我们上述后端的思路,新建三个接口RequestUploadFileFileSaveFileMerge

    [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),好了,不扯犊子了,每个接口的方法如下。

RequestUploadFile

        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里面已经设置过,可以根据自己业务需要调整。

打开文件看下是否有损坏,压缩包很容易看出来是否正常,只要能打开基本上(当然可能会有问题)没问题。

解压出来如果正常那肯定就是没问题了吧(压缩这个玩意儿真是牛逼,节省了多少的存储空间,虽说硬盘白菜价)。

小结

在整理文件上传这篇刚好捎带着把跨域也简单了过了一遍,下来需要再折腾的东西就是大文件的分片下载,大致的思路与文件上传一致,毕竟都是一个大蛋糕,切成好几块,你一块,剩下的都是我的。

net core WebApi——文件分片上传与跨域请求处理的更多相关文章

  1. .NET Core Web 文件分片上传,带进度条实用插件

    话不多说,上源码连接: 链接:https://pan.baidu.com/s/1_u15zqAjhH0aVpeoyVMfUA 提取码:z209

  2. net core WebApi——文件分片下载

    目录 前言 开始 测试 小结 @ 前言 上一篇net core WebApi--文件分片上传与跨域请求处理介绍完文件的上传操作,本来是打算紧接着写文件下载,中间让形形色色的事给耽误的,今天还是抽个空整 ...

  3. .NET Core Web APi大文件分片上传研究

    前言 前两天发表利用FormData进行文件上传,然后有人问要是大文件几个G上传怎么搞,常见的不就是分片再搞下断点续传,动动手差不多也能搞出来,只不过要深入的话,考虑的东西还是很多.由于断点续传之前写 ...

  4. Webuploader 大文件分片上传

    百度Webuploader 大文件分片上传(.net接收)   前阵子要做个大文件上传的功能,找来找去发现Webuploader还不错,关于她的介绍我就不再赘述. 动手前,在园子里找到了一篇不错的分片 ...

  5. Vue2.0结合webuploader实现文件分片上传

    Vue项目中遇到了大文件分片上传的问题,之前用过webuploader,索性就把Vue2.0与webuploader结合起来使用,封装了一个vue的上传组件,使用起来也比较舒爽. 上传就上传吧,为什么 ...

  6. asp.net 文件分片上传

    最近在研究文件上传,里面的门道还是挺多的,网上大多数文章比较杂乱,代码都是片段,对于新手小白来说难度较高,所以在此详细写一下今天看到的一个demo,关于文件分片上传的. <!DOCTYPE ht ...

  7. java springboot 大文件分片上传处理

    参考自:https://blog.csdn.net/u014150463/article/details/74044467 这里只写后端的代码,基本的思想就是,前端将文件分片,然后每次访问上传接口的时 ...

  8. vue+大文件分片上传

    最近公司在使用vue做工程项目,实现大文件分片上传. 网上找了一天,发现网上很多代码都存在很多问题,最后终于找到了一个符合要求的项目. 工程如下: 对项目的大文件上传功能做出分析,怎么实现大文件分片上 ...

  9. plupload 大文件分片上传与PHP分片合并探索

    最近老大分给我了做一个电影cms系统,其中涉及到一个功能,使用七牛云的文件上传功能.七牛javascript skd,使用起来很方便,屏蔽了许多的技术细节.如果只满足与调用sdk,那么可能工作中也就没 ...

随机推荐

  1. Golang 高效实践之defer、panic、recover实践

    前言 我们知道Golang处理异常是用error返回的方式,然后调用方根据error的值走不同的处理逻辑.但是,如果程序触发其他的严重异常,比如说数组越界,程序就要直接崩溃.Golang有没有一种异常 ...

  2. Spring框架之IoC和AOP

    Spring框架简介: 2003年2月,Spring框架正式成为一个开源项目,并发布于SourceForge中.致力于Java EE应用的各种解决方案,而并不是仅仅专注于某一层的方案,是企业应用开发的 ...

  3. Python开发:NumPy学习(一)ndarray数组

    一.数据维度 一个数据表达一个含义,一组数据表达一个或多个含义. 数据维度概念:一组数据的组织形式,其中有一维数据.二维数据.多维数据.高维数据. 1.  一维数据 一维数据由对等关系的有序或无序数据 ...

  4. 校园表白墙、微信表白墙、校园墙 微信小程序 JAVA 开发记录与分享

    目录 最新版表白墙博客地址 1.微信小程序前台展示 2.功能介绍 3.后台管理 4.后端语言采用 JAVA 开发 5.体验此微信小程序 扫描下方二维码 6.如何联系我或需要源码进行联系 最新版表白墙博 ...

  5. echarts在react项目中的使用

    数据可视化在前端开发中经常会遇到,万恶的图表,有时候总是就差一点,可是怎么也搞不定. 别慌,咱们一起来研究. 引入我就不多说了 npm install echarts 对于基础的可视化组件,我一般采用 ...

  6. 《VR入门系列教程》之16---第一个OculusVR应用

    第一个VR应用    之前我们已经将Oculus的开发包导入到空工程中了,现在我们来构建第一个桌面VR的示例.开发包中已经有一个示例场景,只需要几步就可以让这个场景运行起来.我们将要构建的这个Demo ...

  7. linux初学者-pxe装机篇

    linux初学者-pxe装机篇 PXE的网络装机是客户机从自己的网卡启动,向本网络中的DHCP服务器索取ip,并从本网络的TFTP服务器中索取启动文件进行装机.此装机需要kickstart.tftp. ...

  8. Android的简述

    程序截图 先来简单了解下程序运行的效果 程序入口点  类似于win32程序里的WinMain函数,Android自然也有它的程序入口点.它通过在AndroidManifest.xml文件中配置来指明, ...

  9. JAVA-基础-数据类型转换

    一.类型的转换 java中数据具有类型.这些类型是可以相互进行转换的. 1.自动类型转换 六个和数字相关的基本类型,可以自动由小到大进行类型转换.但是反过来就不行. *注意,在整形自动转浮点型时,有可 ...

  10. 小伙子,你真的清楚 JVM GC ?

    序 正文 如何确定垃圾? 前面已经提到 JVM 可以采用 引用计数法 与 可达性分析算法 来确定需要回收的垃圾,我们来具体看一下这两种算法: 引用计数法 该方法实现为:给每个对象添加一个引用计数器,每 ...