如何使用 ASP.NET Core 上传大文件

        之前有一篇文章介绍如何通过 ASP.NET core 的 API 上传文件,您可以参考链接:https://blog.youkuaiyun.com/hefeng_aspnet/article/details/144507488。文章将讨论如何处理上传大文件 [体积超过 2GB]。

        相比小数据,上传大数据量数据需要一些技巧。首先,.NET MVC 框架对 IIS/Kestrel 做了一些限制。默认情况下,每个请求的体积仅为 28.6MB。如果你尝试超出限制,则会返回 http 状态代码:413 [Payload 太大]。对于 IIS,我们可以修改web.config来释放限制(maxAllowedContentLength)。对于 Kestrel,我们需要同时配置MaxReqeustBodySizeMultipartBodyLengthLimi t来释放更多体积。

        其次,我们可能有两种方法来处理文件上传。我们在之前的文章中演示了使用缓冲(通过IFormFile)。但对于较大的文件,我们可能无法利用缓冲,并且可能很容易耗尽我们的资源(内存/磁盘)。因此,使用流式传输(通过MultipartReader)方式来处理这种情况是合理的。

        在本篇文章中,我们修改了上传 API,使用流式传输方法来处理大容量文件。我们从客户端开始,客户端将向 API 提交请求:

RequestUpload.cs

// files: suppose we have a list of file paths
    // subDirectory: the sub-directory structure under root for storing
    public async Task<HttpResponseMessage> UploadFilesAsync(List<string> files, string subDirectory)
    {
        string boundary = $"--{DateTime.Now.Ticks:x}";
                using var content = new MultipartFormDataContent(boundary);
                content.Headers.Add("ContentType", $"multipart/form-data, boundary={boundary}");
        foreach (var filePath in files)
        {
            content.Add(new StringContent(subDirectory, Encoding.UTF8), "subDirectory");

            using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            var fileNameHash = Path.GetFileNameWithoutExtension(filePath) + "_" + Guid.NewGuid().ToString("N") + Path.GetExtension(filePath);
            content.Add(new StreamContent(stream), "files", fileNameHash);
        
        }
        
        // IHttpClientFactory, consider using factory would be more effeicecy 
        var httpClient = new HttpClient();
        using HttpRequestMessage request = new(HttpMethod.Post, new Uri("{your file API uri}")) { Content = content };
        // fire the post with the MultipartFormDataContent to File API
        var result = await httpClient.SendAsync(request);
        
        return result;
        
    }

以下是使用 multipart/form-data 携带文件内容的 http post 请求的真实示例:

POST https://localhost:5001/devwfapi/api/form/uploadattachmentsonfly
200
45.84 s

POST /devwfapi/api/form/uploadattachmentsonfly HTTP/1.1
System: xxxxxxxx
AccessToken: ALwzpK59onuqtvZVTOEsMYbUSPAB-xVGc22RqCh8z0Y8HHkKV_w3MLmKnJE_ywj_Aw==
User-Agent: PostmanRuntime/7.29.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 9edc5eb6-0397-4520-a03a-13212d1b6996
Host: localhost:5001
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=--------------------------156313382635509050530525
Content-Length: 444312365

----------------------------156313382635509050530525
Content-Disposition: form-data; name="subDirectory"
\path1\path2
----------------------------156313382635509050530525
Content-Disposition: form-data; name="files"; filename="OnScreenControl_7.58.zip"
<OnScreenControl_7.58.zip>
----------------------------156313382635509050530525
Content-Disposition: form-data; name="files"; filename="InstallDivvy.exe"
<InstallDivvy.exe>
----------------------------156313382635509050530525
Content-Disposition: form-data; name="files"; filename="PowerToysSetup-0.64.0-x64.exe"
<PowerToysSetup-0.64.0-x64.exe>
----------------------------156313382635509050530525
Content-Disposition: form-data; name="files"; filename="2022 Scrum Sharing PPT.pdf"
<2022 Scrum Sharing PPT.pdf>
----------------------------156313382635509050530525
Content-Disposition: form-data; name="files"; filename="VSCodeUserSetup-x64-1.72.2.exe"
<VSCodeUserSetup-x64-1.72.2.exe>
----------------------------156313382635509050530525-- 

        您应该注意“ Content-Length ”部分下的结构。因为我们将不再依赖IFormFile (缓冲模型绑定)。对于MultipartReader (流式传输),我们需要自己解析内容部分。

接下来,我们将演示如何通过 API 端使用 MultiparReader。但首先我们需要完成以下帮助类: 


MultipartRequestHelper.cs

public static class MultipartRequestManager
    {
    // get the boundary information, for above exmaple would be: --------------------------156313382635509050530525
        public static string GetBoundary(MediaTypeHeaderValue contentType)
        {
            var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;

            if (string.IsNullOrWhiteSpace(boundary))
            {
                throw new InvalidDataException("Missing content-type boundary.");
            }
            return boundary;
        }
    
    // validate if it was multipart form data
        public static bool IsMultipartContentType(string contentType)
        {
            return !string.IsNullOrEmpty(contentType)
                   && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            //  for exmaple, Content-Disposition: form-data; name="subdirectory";
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && string.IsNullOrEmpty(contentDisposition.FileName.Value)
                && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
        }

        public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // for example, Content-Disposition: form-data; name="files"; filename="OnScreenControl_7.58.zip"
      // ...
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
                    || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
        }
    }
  
  // we disable automatic model binding for MVC default behavior than to the access streaming directly 
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
    {
        public void OnResourceExecuting(ResourceExecutingContext context)
        {
            var factories = context.ValueProviderFactories;
            factories.RemoveType<FormValueProviderFactory>();
            factories.RemoveType<JQueryFormValueProviderFactory>();
        }

        public void OnResourceExecuted(ResourceExecutedContext context)
        {
        }
    }

需要提一下的是,如果您的文件API向公众开放,那么您必须更加关心可能发生的安全性和病毒威胁。

最后我们将完成文件上传功能 API: 

FileController.cs

[Route("api/[controller]")]
    public class FileController : ControllerBase
    {
        [HttpPost("uploadlarge")]
        [DisableFormValueModelBinding]
        public async Task<IActionResult> UploadLargeFileAsync()
        {
            try
            {
                if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
                {
                    throw new FormatException("Form without multipart content.");
                }

                var subDirectory = string.Empty;
                var count = 0;
                var totalSize = 0L;
        // find the boundary
                var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(Request.ContentType));
        // use boundary to iterator through the multipart section
                var reader = new MultipartReader(boundary, HttpContext.Request.Body);

                var section = await reader.ReadNextSectionAsync();
                do
                {
                    ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition);
                    if (!MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
                    {
                        if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition) && contentDisposition.Name == nameof(subDirectory))
                        {
                            using var streamReader = new StreamReader(section.Body, Encoding.UTF8);
              // get the subdirectory first
                            subDirectory = streamReader.ReadToEnd();
                        }

                        section = await reader.ReadNextSectionAsync();
                        continue;
                    }
                    totalSize += await SaveFileAsync(section, subDirectory);

                    count++;
                    section = await reader.ReadNextSectionAsync();
                } while (section != null);

                return Ok(new { Count = count, Size = SizeConverter(totalSize) });

            }
            catch (Exception exception)
            {
                return BadRequest($"Error: {exception.Message}");
            }
        }

        private async Task<long> SaveFileAsync(MultipartSection section, string subDirectory)
        {
            subDirectory ??= string.Empty;
            var target = Path.Combine("{root}", subDirectory);
            Directory.CreateDirectory(target);

            var fileSection = section.AsFileSection();

            var filePath = Path.Combine(target, fileSection.FileName);
            using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 1024);
            await fileSection.FileStream.CopyToAsync(stream);

            return fileSection.FileStream.Length;
        }

        private string SizeConverter(long bytes)
        {
            var fileSize = new decimal(bytes);
            var kilobyte = new decimal(1024);
            var megabyte = new decimal(1024 * 1024);
            var gigabyte = new decimal(1024 * 1024 * 1024);

            switch (fileSize)
            {
                case var _ when fileSize < kilobyte:
                    return $"Less then 1KB";
                case var _ when fileSize < megabyte:
                    return $"{Math.Round(fileSize / kilobyte, 0, MidpointRounding.AwayFromZero):##,###.##}KB";
                case var _ when fileSize < gigabyte:
                    return $"{Math.Round(fileSize / megabyte, 2, MidpointRounding.AwayFromZero):##,###.##}MB";
                case var _ when fileSize >= gigabyte:
                    return $"{Math.Round(fileSize / gigabyte, 2, MidpointRounding.AwayFromZero):##,###.##}GB";
                default:
                    return "n/a";
            }
        }
    }

我们在将子目录路径传递给 API 时做了一些小技巧。您会发现我们在内容顶部(MultipartFormDataContent 内)添加了“子目录”字符串。然后我们可以在 API 端检索第一部分。

其余部分,我们只需向我们的文件 API 发送上传请求,并在本地测试环境下比较缓冲与流式上传相同文件量。流式方法在处理较大文件方面具有优势,这一点并不令我们感到惊讶。 

缓冲(IFromFile)

流式传输(MultipartReader)

参考

如果您喜欢此文章,请收藏、点赞、评论,谢谢,祝您快乐每一天。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

csdn_aspnet

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值