• .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

  • 相关阅读:
    [转]django自定义表单提交
    [django/mysql] 使用distinct在mysql中查询多条不重复记录值的解决办法
    [Django]下拉表单与模型查询
    [Django]模型提高部分--聚合(group by)和条件表达式+数据库函数
    [Django]模型学习记录篇--基础
    [Django]数据批量导入
    怎么让自己的本地php网站让别人访问到
    HTML Marquee跑马灯
    marquee标签详解
    apache的虚拟域名rewrite配置以及.htaccess的使用。
  • 原文地址:https://www.cnblogs.com/qiyebao/p/13503773.html
Copyright © 2020-2023  润新知