net core WebApi——文件分片上传与跨域请求处理

  • 时间:
  • 浏览:0
  • 来源:极速快3_快3棋牌_极速快3棋牌

@

前言

在时候分发完一套简单的后台基础工程后,原因分析分析分析业务能够否鼓捣了文件上传跟下载,分发时候就迫不及待的想分享出来,希望有用到文件相关操作的亲戚大伙能够否得到些帮助。

刚开始英文英文英文

亲戚亲戚大伙依然用亲戚亲戚大伙的基础工程,时候也提到时候续原因分析分析分析有测试功能累似 的东西,会另两个 劲不断的更新这套代码(原因分析分析分析搞炸了时候那就…),代码下载地址在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里面原因分析分析分析设置过,能够否根据你是什么 人业务能够否调整。

打开文件看下是不是 有损坏,压缩包很容易看出来是不是 正常,倘若能打开基本上(当然原因分析分析分析会有什么的问题)没什么的问题。

解压出来原因分析分析分析正常那肯定倘若没什么的问题了吧(压缩你你是什么 玩意儿果然牛逼,节省了几条的存储空间,虽说硬盘白菜价)。

小结

在分发文件上传这篇刚好捎带着把跨域也简单了过了一遍,下来能够否再折腾的东西倘若大文件的分片下载,大致的思路与文件上传一致,毕竟是不是 另另两个 多大蛋糕,切成好几条,你一块,剩下的是不是 我的。