}
72
function
UploadFileChunk(chunk) {
73
var
data
=
new
FormData();
74
data.append(
"
file
"
, chunk.file, chunk.filePartName);
75
$.ajax({
76
url:
'
/api/upload/upload?
'
+
chunk.urlParameter,
77
type:
"
post
"
,
78
cache:
false
,
79
contentType:
false
,
80
processData:
false
,
81
data: data,
82
});
83
}
84
</
script
>
Index.html
UploadController.cs
1 [Route("api/[controller]/[action]")]
2 [ApiController]
3 public class UploadController : ControllerBase
5 private const string DEFAULT_FOLDER = "Upload";
6 private readonly IWebHostEnvironment _environment;
8 public UploadController(IWebHostEnvironment environment)
10 this._environment = environment;
11 }
13 /// <summary>
14 /// 文件分片上传
15 /// </summary>
16 /// <param name="chunk"></param>
17 /// <returns></returns>
18 [HttpPost]
19 [DisableFormValueModelBinding]
20 public async Task<IActionResult> Upload([FromQuery] FileChunk chunk)
21 {
22 if (!this.IsMultipartContentType(this.Request.ContentType))
23 {
24 return this.BadRequest();
25 }
27 var boundary = this.GetBoundary();
28 if (string.IsNullOrEmpty(boundary))
29 {
30 return this.BadRequest();
31 }
33
var reader = new MultipartReader(boundary, this.Request.Body);
35 var section = await reader.ReadNextSectionAsync();
37 while (section != null)
38 {
39 var buffer = new byte[chunk.Size];
40 var fileName = this.GetUploadFileSerialName(section.ContentDisposition);
41 chunk.FileName = fileName;
42 var path = Path.Combine(this._environment.WebRootPath, DEFAULT_FOLDER, fileName);
43 using (var stream = new FileStream(path, FileMode.Append))
44 {
45 int bytesRead;
46 do
47 {
48 bytesRead = await section.Body.ReadAsync(buffer, 0, buffer.Length);
49 stream.Write(buffer, 0, bytesRead);
51 } while (bytesRead > 0);
52 }
54 section = await reader.ReadNextSectionAsync();
55 }
57 //TODO: 计算上传文件大小实时反馈进度
59 //合并文件(可能涉及转码等)
60 if (chunk.PartNumber == chunk.Chunks)
61 {
62 await this.MergeChunkFile(chunk);
63 }
65 return this.Ok();
66 }
68 /// <summary>
69 /// 判断是否含有上传文件
70 /// </summary>
71 /// <param name="contentType"></param>
72 /// <returns></returns>
73 private bool IsMultipartContentType(string contentType)
74 {
75 return !string.IsNullOrEmpty(contentType)
76 && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
77 }
79 /// <summary>
80 /// 得到上传文件的边界
81 /// </summary>
82 /// <returns></returns>
83 private string GetBoundary()
84 {
85 var mediaTypeHeaderContentType = MediaTypeHeaderValue.Parse(this.Request.ContentType);
86 return HeaderUtilities.RemoveQuotes(mediaTypeHeaderContentType.Boundary).Value;
87 }
89 /// <summary>
90 /// 得到带有序列号的上传文件名
91 /// </summary>
92 /// <param name="contentDisposition"></param>
93 /// <returns></returns>
94 private string GetUploadFileSerialName(string contentDisposition)
95 {
96 return contentDisposition
97 .Split(';')
98 .SingleOrDefault(part => part.Contains("filename"))
99 .Split('=')
100 .Last()
101 .Trim('"');
102 }
104 /// <summary>
105 /// 合并文件
106 /// </summary>
107 /// <param name="chunk"></param>
108 /// <returns></returns>
109 public async Task MergeChunkFile(FileChunk chunk)
110 {
111 var uploadDirectoryName = Path.Combine(this._environment.WebRootPath, DEFAULT_FOLDER);
113 var baseFileName = chunk.FileName.Substring(0, chunk.FileName.IndexOf(FileSort.PART_NUMBER));
115 var searchpattern = $"{Path.GetFileName(baseFileName)}{FileSort.PART_NUMBER}*";
117 var fileNameList = Directory.GetFiles(uploadDirectoryName, searchpattern).ToArray();
118 if (fileNameList.Length == 0)
119 {
120 return;
121 }
123 List<FileSort> mergeFileSortList = new List<FileSort>(fileNameList.Length);
125 string fileNameNumber;
126 foreach (string fileName in fileNameList)
127 {
128 fileNameNumber = fileName.Substring(fileName.IndexOf(FileSort.PART_NUMBER) + FileSort.PART_NUMBER.Length);
130 int.TryParse(fileNameNumber, out var number);
131 if (number <= 0)
132 {
133 continue;
134 }
136 mergeFileSortList.Add(new FileSort
137 {
138 FileName = fileName,
139 PartNumber = number
140 });
141 }
143 // 按照分片排序
144 FileSort[] mergeFileSorts = mergeFileSortList.OrderBy(s => s.PartNumber).ToArray();
146 mergeFileSortList.Clear();
147 mergeFileSortList = null;
149 // 合并文件
150 string fileFullPath = Path.Combine(this._environment.WebRootPath, DEFAULT_FOLDER, baseFileName);
151 if (System.IO.File.Exists(fileFullPath))
152 {
153 System.IO.File.Delete(fileFullPath);
154 }
155 bool error = false;
156 using var fileStream = new FileStream(fileFullPath, FileMode.Create);
157 foreach (FileSort fileSort in mergeFileSorts)
158 {
159 error = false;
160 do
161 {
162 try
163 {
164 using FileStream fileChunk = new FileStream(fileSort.FileName, FileMode.Open, FileAccess.Read, FileShare.Read);
165 await fileChunk.CopyToAsync(fileStream);
166 error = false;
167 }
168 catch (Exception)
169 {
170 error = true;
171 Thread.Sleep(0);
172 }
173 }
174 while (error);
175 }
177 //删除分片文件
178 foreach (FileSort fileSort in mergeFileSorts)
179 {
180 System.IO.File.Delete(fileSort.FileName);
181 }
182 Array.Clear(mergeFileSorts, 0, mergeFileSorts.Length);
183 mergeFileSorts = null;
184 }
185 }
UploadController.cs
await Policy.Handle<IOException>()
.RetryForeverAsync()
.ExecuteAsync(async () =>
foreach (FileSort fileSort in mergeFileSorts)
using FileStream fileChunk =
new FileStream(fileSort.FileName, FileMode.Open,
FileAccess.Read, FileShare.Read);
await fileChunk.CopyToAsync(fileStream);
//删除分片文件
Parallel.ForEach(mergeFiles, f =>
System.IO.File.Delete(f.FileName);
FileChunk.cs
1 /// <summary>
2 /// 文件批量上传的URL参数模型
3 /// </summary>
4 public class FileChunk
6 //文件名
7 public string FileName { get; set; }
8 /// <summary>
9 /// 当前分片
10 /// </summary>
11 public int PartNumber { get; set; }
12 /// <summary>
13 /// 缓冲区大小
14 /// </summary>
15 public int Size { get; set; }
16 /// <summary>
17 /// 分片总数
18 /// </summary>
19 public int Chunks { get; set; }
20 /// <summary>
21 /// 文件读取起始位置
22 /// </summary>
23 public int Start { get; set; }
24 /// <summary>
25 /// 文件读取结束位置
26 /// </summary>
27 public int End { get; set; }
28 /// <summary>
29 /// 文件大小
30 /// </summary>
31 public int Total { get; set; }
View Code
FileSort.cs
1 public class FileSort
3 public const string PART_NUMBER = ".partNumber-";
4 /// <summary>
5 /// 带有序列号的文件名
6 /// </summary>
7 public string FileName { get; set; }
8 /// <summary>
9 /// 文件分片号
10 /// </summary>
11 public int PartNumber { get; set; }
FileSort.cs