之前有一篇文章介绍如何通过 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,我们需要同时配置MaxReqeustBodySize和MultipartBodyLengthLimi 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)
参考
- https://stackoverflow.com/questions/68150360/uploading-large-files-over-4gb-asp-net-core-5
- Upload Large Files in C# | Scattered Code
- File Upload in ASP.NET Core 6 - Detailed Guide | Pro Code Guide
- Upload Large Files In Web API With A Simple Method
- Upload files in ASP.NET Core | Microsoft Learn
如果您喜欢此文章,请收藏、点赞、评论,谢谢,祝您快乐每一天。