缓冲方式上传文件有大小限制,文件太大则无法上传。微软提供了流方式上传文件,流式传输无法显著提高性能,但可降低上传文件时对内存或磁盘空间的需求。
微软提供的流式上传文件示例是在客户端使用AJAX将文件上传到控制器的指定函数中,本文创建MVC项目,并将微软示例中的流式上传文件到物理文件夹部分的代码剥离到新建项目中,实现最简单的上传单个文件操作。
直接在项目默认创建的HomeController类中增加文件上传函数UploadPhysical,微软示例中在函数中增加了多种特性,同时调用MultipartRequestHelper和FileHelpers类辅助进行文件检查等操作,本文中仅保留必要的函数,并将这些必要函数都添加到HomeController类中,主要的代码如下所示(微软示例中将文件保存到文件夹中时处理了文件名和后缀名,本项目中也一并去掉了,就使用的原始文件名)。
public static bool IsMultipartContentType(string contentType)
{
return !string.IsNullOrEmpty(contentType)
&& contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
}
public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;
if (string.IsNullOrWhiteSpace(boundary))
{
throw new InvalidDataException("Missing content-type boundary.");
}
if (boundary.Length > lengthLimit)
{
throw new InvalidDataException(
$"Multipart boundary length limit {lengthLimit} exceeded.");
}
return boundary;
}
private static readonly FormOptions _defaultFormOptions = new FormOptions();
public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
|| !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
}
public static async Task<byte[]> ProcessStreamedFile(
MultipartSection section, ModelStateDictionary modelState)
{
try
{
using (var memoryStream = new MemoryStream())
{
await section.Body.CopyToAsync(memoryStream);
if (memoryStream.Length == 0)
{
modelState.AddModelError("File", "The file is empty.");
}
else
{
return memoryStream.ToArray();
}
}
}
catch (Exception ex)
{
modelState.AddModelError("File",
"The upload failed. Please contact the Help Desk " +
$" for support. Error: {ex.HResult}");
}
return new byte[0];
}
[HttpPost]
public async Task<IActionResult> UploadPhysical()
{
if (!IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
return BadRequest(ModelState);
}
var boundary = GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
if (!HasFileContentDisposition(contentDisposition))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
return BadRequest(ModelState);
}
else
{
var streamedFileContent = await ProcessStreamedFile(
section, ModelState);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
using (var targetStream = System.IO.File.Create(
Path.Combine(_targetFilePath, contentDisposition.FileName.Value)))
{
await targetStream.WriteAsync(streamedFileContent);
}
}
}
section = await reader.ReadNextSectionAsync();
}
return View("Index");
}
前端代码基本是直接复制过来的,这里仅是放在这里作为参考:
<h1>MVC项目调用AJAX上传文件</h1>
<form id="uploadForm" action="Home/UploadPhysical" method="post"
enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;">
<dl>
<dt>
<label for="file">文件</label>
</dt>
<dd>
<input id="file" type="file" name="file" />
</dd>
</dl>
<input class="btn" type="submit" value="上传文件" />
<div style="margin-top:15px">
<output form="uploadForm" name="result"></output>
</div>
</form>
@section Scripts {
<script>
"use strict";
async function AJAXSubmit (oFormElement) {
const formData = new FormData(oFormElement);
try {
const response = await fetch(oFormElement.action, {
method: 'POST',
headers: {
'RequestVerificationToken': getCookie('RequestVerificationToken')
},
body: formData
});
oFormElement.elements.namedItem("result").value =
'Result: ' + response.status + ' ' + response.statusText;
} catch (error) {
console.error('Error:', error);
}
}
function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}
</script>
}
最后的程序的运行效果:下面第一个图是程序的初始页面,第二个图是选择文件后上传到指定文件夹。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL3ADOyUmM2YWM0kTYkljY4EDN2QTM5U2NyMGO1I2M0YzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
参考文献:
[1]https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/file-uploads?view=aspnetcore-6.0
[2]https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/file-uploads?view=aspnetcore-6.0#upload-large-files-with-streaming