.NET Core Web API 实现大文件分片上传

.NET Core Web APi大文件分片上传

Index.html

 1 @{
 2     ViewData["Title"] = "Home Page";
 3 }
 4 
 5 @{
 6     ViewBag.Title = "Home Page";
 7 }
 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>
20 
21 <script type="text/javascript" src="~/js/jquery-3.4.1.min.js"></script>
22 <script type="text/javascript">
23 
24     $(function () {
25         $('#submit').click(function () {
26             UploadFile($('#file')[0].files);
27         });
28     });
29 
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     }
71 
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
  4     {
  5         private const string DEFAULT_FOLDER = "Upload";
  6         private readonly IWebHostEnvironment _environment;
  7 
  8         public UploadController(IWebHostEnvironment environment)
  9         {
 10             this._environment = environment;
 11         }
 12 
 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             }
 26 
 27             var boundary = this.GetBoundary();
 28             if (string.IsNullOrEmpty(boundary))
 29             {
 30                 return this.BadRequest();
 31             }
 32 
 33             var reader = new MultipartReader(boundary, this.Request.Body);
 34 
 35             var section = await reader.ReadNextSectionAsync();
 36 
 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);
 50 
 51                     } while (bytesRead > 0);
 52                 }
 53 
 54                 section = await reader.ReadNextSectionAsync();
 55             }
 56 
 57             //TODO: 计算上传文件大小实时反馈进度
 58 
 59             //合并文件(可能涉及转码等)
 60             if (chunk.PartNumber == chunk.Chunks)
 61             {
 62                 await this.MergeChunkFile(chunk);
 63             }
 64 
 65             return this.Ok();
 66         }
 67 
 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         }
 78 
 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         }
 88 
 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         }
103 
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);
112 
113             var baseFileName = chunk.FileName.Substring(0, chunk.FileName.IndexOf(FileSort.PART_NUMBER));
114 
115             var searchpattern = $"{Path.GetFileName(baseFileName)}{FileSort.PART_NUMBER}*";
116 
117             var fileNameList = Directory.GetFiles(uploadDirectoryName, searchpattern).ToArray();
118             if (fileNameList.Length == 0)
119             {
120                 return;
121             }
122 
123             List<FileSort> mergeFileSortList = new List<FileSort>(fileNameList.Length);
124 
125             string fileNameNumber;
126             foreach (string fileName in fileNameList)
127             {
128                 fileNameNumber = fileName.Substring(fileName.IndexOf(FileSort.PART_NUMBER) + FileSort.PART_NUMBER.Length);
129 
130                 int.TryParse(fileNameNumber, out var number);
131                 if (number <= 0)
132                 {
133                     continue;
134                 }
135 
136                 mergeFileSortList.Add(new FileSort
137                 {
138                     FileName = fileName,
139                     PartNumber = number
140                 });
141             }
142 
143             // 按照分片排序
144             FileSort[] mergeFileSorts = mergeFileSortList.OrderBy(s => s.PartNumber).ToArray();
145 
146             mergeFileSortList.Clear();
147             mergeFileSortList = null;
148 
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             }
176 
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
 5     {
 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; }
32     }
View Code

FileSort.cs

 1 public class FileSort
 2     {
 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; }
12     }
FileSort.cs

 

 

posted @ 2020-08-14 18:20  —八戒—  阅读(1584)  评论(2编辑  收藏  举报