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

2022-12-21,,,,

前言

对于普通业务场景而言,直接用 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。

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

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

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

大文件分片上传,后端拼接保存(前端:antd;后端:.Net 5 WebAPI)的相关教程结束。

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

下载本文的Word格式文档,以方便收藏与打印。