大文件分片上传,后端拼接保存(前端: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)
注:个人整理,已验证可用,有疑问欢迎指正。
本文来自博客园,作者:橙子家,欢迎微信扫码关注博主【橙子家czzj】,有任何疑问欢迎沟通,共同成长!