大文件分片上传,后端拼接保存(前端:antd;后端:.Net 5 WebAPI)

前言

对于普通业务场景而言,直接用 FormData() 将文件以入参的一个参数传给后端即可,但此方法有一个弊端就是,有个 30M 的上限。

对于动辄几百 M、几个 G 的文件上传需求,FormData() 显然已经黯然了,但仍想以此方法上传,那么就可以把大文件,按照同一大小进行分片,然后分片上传至后端,上传完后在通知后端进行组装保存即可。

效果先贴出:

  

  

 废话不多说,代码见!

前端部分

  js 业务处理代码:(一些细节就不全部贴了,需要自己调试修改喽)

  1   state = {
  2     fileList: [],
  3     filenameview: "等待上传...",
  4     zhuti: "",
  5     wenjianfjdz: "",//附件共享盘地址
  6     fileuploading: false,
  7     disablebtndelete: "none",
  8     chunkList: [],//文件切片 List
  9   };
 10   //创建文件切片//默认大小为 20M 一个片段
 11   createFileChunk = (file, size = 20 * 1024 * 1024) => {
 12     const fileChunkList = [];
 13     let cur = 0;
 14     while (cur < file.size) {
 15       fileChunkList.push({ file: file.slice(cur, cur + size), name: file.uid });//uid:文件唯一标识
 16       cur += size;
 17     }
 18     return fileChunkList;
 19   };
 20   //上传文件附件(大文件)
 21   uploadloadFileAttachment = ({ file, fileList }) => {
 22     this.setState({ fileuploading: true, tipContent: "正在校验文件,请稍后..." });
 23     if (this.state.zhuti == null || this.state.zhuti.length == 0) {
 24       message.warning("上传文件前,请先填写‘主题’!");this.setState({ fileuploading: false }); return;
 25     }
 26     else {
 27       if (file.status === "done") {
 28         this.setState({ tipContent: "文件已完成校验,上传中,请耐心等待..." });
 29         let file = fileList[fileList.length - 1].originFileObj;
 30         if (file.size / (1024 * 1024) > 3096) {//设置最大不超过 3G
 31           message.warning("附件大小不允许超过 3GB !");
 32           this.setState({ fileuploading: false, tipContent: "加载中..." });
 33           return;
 34         }
 35         let filename = file.name;
 36         let chunklistcurr = this.createFileChunk(file).map(({ file, name }, index) => {
 37           return {
 38             chunk: file,
 39             size: file.size,
 40             percent: 0,
 41             name: name,
 42             index,
 43           };
 44         });
 45         let filecount = chunklistcurr.length;
 46         let ii = 0;
 47         chunklistcurr.forEach(element => {//分片传输
 48           const formData = new FormData()
 49           formData.append('file', element.chunk);
 50           formData.append('index', element.index);//片段顺序
 51           formData.append('name', element.name);//文件唯一标识
 52           formData.append('size', element.size);
 53           formData.append('filecount', filecount);//片段总数
 54           axios({
 55             method: 'post',
 56             url: '/api/system/System/UploadFileAttachmentChunk?zhuti=' + this.state.zhuti,
 57             data: formData,
 58             headers: { "Content-Type": "multipart/form-data" }
 59           }).then(({ data }) => {
 60             if (data.code == 200) {
 61               ii++;//记录已经上传成功的片段数量
 62               if (ii == filecount) {//分块全部上传完成
 63                 let indata = { "name": chunklistcurr[0].name, "filecount": filecount, "filename": filename, "zhuti": this.state.zhuti }
 64                 axios({//传输完成,通知拼接
 65                   method: 'post',
 66                   url: '/api/system/System/CombineChunkToFile',
 67                   data: indata,
 68                   headers: { "Content-Type": "application/json" }
 69                 }).then(({ data }) => {
 70                   if (data.code == 200) {
 71                     this.setState({ wenjianfjdz: data.desc.split("|")[0], filenameview: data.desc.split("|")[1], disablebtndelete: "" });//记录返回值
 72                     message.success(`上传成功!`);
 73                   }
 74                   else if (result.code == 202) {
 75                     window.location.href = "/wellcome";
 76                   }
 77                   else {
 78                     message.error("上传失败,请稍后重试!详情:" + data.desc);
 79                   }
 80                 }).catch((err) => {
 81                   console.log(err);
 82                   message.error("上传失败,请稍后重试!");
 83                 }).finally(() => {
 84                   this.setState({ fileuploading: false, tipContent: "加载中..." });
 85                 })
 86               }
 87             }
 88             else if (result.code == 202) {
 89               window.location.href = "/wellcome";
 90             }
 91             else {
 92               message.error("上传失败,请稍后重试!详情:" + data.desc);
 93               return;
 94             }
 95           }).catch((err) => {
 96             console.log(err);
 97             message.error("上传失败,请稍后重试!");
 98             return;
 99           }).finally(() => {
100           })
101         })
102       }
103       else if (file.status === "error") {
104         message.error(`上传失败,请稍后重试!${file.name}`);
105         this.setState({ fileuploading: false, tipContent: "" });
106       }
107     }
108     this.setState({ fileList: [...fileList] });
109   }
110   beforeUploadMethod = () => this.setState({ fileuploading: true, tipContent: "正在校验文件,请稍后..." })

  html 处理代码:(一些细节就不全部贴了,需要自己调试修改喽)

 1   <Upload
 2     fileList={fileList}
 3     showUploadList={false}
 4     onChange={this.uploadloadFileAttachment}
 5     style={{ float: "left", lineHeight: "55px" }}
 6   >
 7     <Button icon={<UploadOutlined />} style={{ margin: "20px 20px 20px 0", display: permissionedit }}>上传附件</Button>
 8   </Upload>
 9   <Button icon={<DeleteOutlined />} danger style={{ margin: "20px 20px 20px 0", display: disablebtndelete }} onClick={this.btnDeleteFileClick}>删除附件</Button>
10   <label style={{ float: "left", lineHeight: "0px", color: '#bfbfbf', fontSize: 10 }}>支持扩展名:apk/exe/pdf/xls/doc/ppt等</label>
11   <label style={{ float: "left", lineHeight: "55px" }}>{filenameview}</label>

后端部分

        private static List<FileChunkModel> fileChunkList = new List<FileChunkModel>();
        /// <summary>
        /// 接收文件片段
        /// </summary>
        /// <param name="file"></param>
        /// <returns></returns>
        [HttpPost]
        public BackDataModel UploadFileAttachmentChunk(IFormCollection file)
        {
            try
            {
                var fileexist = fileChunkList.Where(ff => ff.name == file["name"] && ff.index == file["index"]).ToList();
                if (fileexist.Count == 0)
                {
                    using (var stream = file.Files[0].OpenReadStream())
                    {
                        using (BinaryReader reader = new BinaryReader(stream))
                        {
                            fileChunkList.Add(new FileChunkModel
                            {
                                name = file["name"],
                                index = Convert.ToInt32(file["index"]),
                                size = file["size"],
                                formfile = reader.ReadBytes((int)stream.Length)
                            });
                        }
                    }
                }
                return new BackDataModel { Code = 200, Desc = "" };
            }
            catch (Exception ex)
            {
                return new BackDataModel { Code = 201, Desc = "" };
            }
        }

        /// <summary>
        /// 文件片段组装
        /// </summary>
        /// <param name="filemodel"></param>
        /// <returns></returns>
        [HttpPost]
        public BackDataModel CombineChunkToFile([FromBody] FileChunkModel filemodel)
        {
            try
            {if (!connectState())
                    return new BackDataModel() { Code = 201, Desc = $"上传失败,共享文件夹无法访问,请联系管理员确认!" };
                List<FileChunkModel> fileChunkModels = fileChunkList.Where(ff => ff.name == filemodel.name).ToList();
                if (fileChunkModels.Count == Convert.ToInt32(filemodel.filecount))
                {
                    if (!Directory.Exists($@"\\存储\{filemodel.zhuti}"))
                    {
                        Directory.CreateDirectory($@"\\存储\{filemodel.zhuti}");
                    }
                    string path = $@"\\存储\{filemodel.zhuti}\{filemodel.filename}";
                    path = PublicMethod.GetFilePath(path, filemodel);

                    using (FileStream CombineStream = new FileStream(path, FileMode.OpenOrCreate))
                    {
                        using (BinaryWriter CombineWriter = new BinaryWriter(CombineStream))
                        {
                 fileChunkModels = fileChunkModels.OrderBy(ff => ff.index).ToList(); //合并前必须给文件片段按照顺序排列,否则部分文件会被破坏(例如:.apk)
foreach (var filecurr in fileChunkModels) { var filechunk = filecurr.formfile; CombineWriter.Write(filechunk); fileChunkList.Remove(filecurr); } lock (fileChunkList) { if (fileChunkList.Count == 0) { fileChunkList = null; fileChunkList=new List<FileChunkModel>(); } } return new BackDataModel() { Code = 200, Desc = $"{path}|{path.Split('\\')[path.Split('\\').Length-1]}" }; } } } else return new BackDataModel() { Code = 201, Desc = "上传失败" }; } catch (Exception ex) { return new BackDataModel() { Code = 201, Desc = "上传失败" }; } } /// <summary> /// 连接远程共享文件夹 /// </summary> /// <param name="path">远程共享文件夹的路径</param> /// <param name="userName">用户名</param> /// <param name="passWord">密码</param> /// <returns></returns> private static bool connectState() { bool Flag = false; Process proc = new Process(); try { proc.StartInfo.FileName = "cmd.exe"; proc.StartInfo.UseShellExecute = false; proc.StartInfo.RedirectStandardInput = true; proc.StartInfo.RedirectStandardOutput = true; proc.StartInfo.RedirectStandardError = true; proc.StartInfo.CreateNoWindow = true; proc.Start(); string dosLine = @"net use \\存储 password /user:username"; proc.StandardInput.WriteLine(dosLine); proc.StandardInput.WriteLine("exit"); while (!proc.HasExited) { proc.WaitForExit(1000); } string errormsg = proc.StandardError.ReadToEnd(); proc.StandardError.Close(); if (string.IsNullOrEmpty(errormsg)) { Flag = true; } else { throw new Exception(errormsg); } } catch (Exception ex) { throw ex; } finally { proc.Close(); proc.Dispose(); } return Flag; }

部署至 IIS 上传大文件异常问题

问题描述:

  在前后端都配置好支持大文件上传后,本地测试没问题,但部署至 IIS 后,上传 30M 以上的文件一直提示失败。

解决方法:

  问题原因: IIS 默认限制了交互最大内容长度(字节)。默认值为:30 000 000(约 2.93M)。

  所以解决问题的关键就是突破这种限制了。

  其中一种修改步骤如下图:(改成最大3G:3,221,225,472)

 

 

 注:个人整理,已验证可用,有疑问欢迎指正。

posted @ 2022-09-16 11:29  橙子家  阅读(1232)  评论(0编辑  收藏  举报