6 ViewBag.Title = "Home Page"; 8 < div class ="form-horizontal" style ="margin-top:80px;" > 9 < div class ="form-group" > 10 < div class ="col-md-10" > 11 < input name ="file" id ="file" type ="file" /> 12 </ div > 13 </ div > 14 < div class ="form-group" > 15 < div class ="col-md-offset-2 col-md-10" > 16 < input type ="submit" id ="submit" value ="上传" class ="btn btn-success" /> 17 </ div > 18 </ div > 19 </ div > 21 < script type ="text/javascript" src ="~/js/jquery-3.4.1.min.js" ></ script > 22 < script type ="text/javascript" > 24 $( function () { 25 $( ' #submit ' ).click( function () { 26 UploadFile($( ' #file ' )[ 0 ].files); 27 }); 28 }); 30 function UploadFile(targetFile) { 31 // 创建上传文件分片缓冲区 32 var fileChunks = []; 33 // 目标文件 34 var file = targetFile[ 0 ]; 35 // 设置分片缓冲区大小 36 var maxFileSizeMB = 8 ; 37 var bufferChunkSize = maxFileSizeMB * ( 1024 * 1024 ); 38 // 读取文件流其实位置 39 var fileStreamPos = 0 ; 40 // 设置下一次读取缓冲区初始大小 41 var endPos = bufferChunkSize; 42 // 文件大小 43 var size = file.size; 44 // 将文件进行循环分片处理塞入分片数组 45 while (fileStreamPos < size) { 46 var fileChunkInfo = { 47 file: file.slice(fileStreamPos, endPos), 48 start: fileStreamPos, 49 end: endPos 50 } 51 fileChunks.push(fileChunkInfo); 52 fileStreamPos = endPos; 53 endPos = fileStreamPos + bufferChunkSize; 54 } 55 // 获取上传文件分片总数量 56 var totalParts = fileChunks.length; 57 var partCount = 0 ; 58 // 循环调用上传每一片 59 while (chunk = fileChunks.shift()) { 60 partCount ++ ; 61 // 上传文件命名约定 62 var filePartName = file.name + " .partNumber- " + partCount; 63 chunk.filePartName = filePartName; 64 // url参数 65 var url = ' partNumber= ' + partCount + ' &chunks= ' + totalParts + ' &size= ' + bufferChunkSize + ' &start= ' + chunk.start + ' &end= ' + chunk.end + ' &total= ' + size; 66 chunk.urlParameter = url; 67 // 上传文件 68 UploadFileChunk(chunk); 69 } 70 } 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