.NETCore如何上传⽂件及处理⼤⽂件上传
当你使⽤IFormFile接⼝来上传⽂件的时候,⼀定要注意,IFormFile会将⼀个Http请求中的所有⽂件都读取到服务器内存后,才会触发ASP.NET Core MVC的Controller中的
Action⽅法。这种情况下,如果上传⼀些⼩⽂件是没问题的,但是如果上传⼤⽂件,势必会造成服务器内存⼤量被占⽤甚⾄溢出,所以IFormFile接⼝只适合⼩⽂件上传。
inputtypefile不上传文件⼀个⽂件上传页⾯的Html代码⼀般如下所⽰:
<form method="post" enctype="multipart/form-data" action="/Upload">
<div>
<p>Upload one or more files using this form:</p>
<input type="file" name="files"/>
</div>
<div>
<input type="submit" value="Upload"/>
</div>
</form>
为了⽀持⽂件上传,form标签上⼀定要记得声明属性enctype="multipart/form-data",否者你会发现ASP.NET Core MVC的Controller中死活都读不到任何⽂件。Input
type="file"标签在html 5中⽀持上传多个⽂件,加上属性multiple即可。
使⽤IFormFile接⼝上传⽂件⾮常简单,将其声明为Contoller中Action的集合参数即可:
[HttpPost]
public async Task<IActionResult> Post(List<IFormFile> files)
{
long size = files.Sum(f => f.Length);
foreach (var formFile in files)
{
var filePath = @"D:\UploadingFiles\" + formFile.FileName;
if (formFile.Length > 0)
{
using (var stream = new FileStream(filePath, FileMode.Create))
{
await formFile.CopyToAsync(stream);
}
}
}
return Ok(new { count = files.Count, size });
}
注意上⾯Action⽅法Post的参数名files,必须要和上传页⾯中的Input type="file"标签的name属性值⼀样。
⽤⽂件流 (⼤⽂件上传)
在介绍这个⽅法之前我们先来看看⼀个包含上传⽂件的Http请求是什么样⼦的:
Content-Type=multipart/form-data; boundary=---------------------------99614912995
-----------------------------99614912995
Content-Disposition: form-data; name="SOMENAME"
Formulaire de Quota
-----------------------------99614912995
Content-Disposition: form-data; name="OTHERNAME"
SOMEDATA
-----------------------------99614912995
Content-Disposition: form-data; name="files"; filename="Misc 001.jpg"
SDFESDSDSDJXCK+DSDSDSSDSFDFDF423232DASDSDSDFDSFJHSIHFSDUIASUI+/==
-----------------------------99614912995
Content-Disposition: form-data; name="files"; filename="Misc 002.jpg"
ASAADSDSDJXCKDSDSDSHAUSAUASAASSDSDFDSFJHSIHFSDUIASUI+/==
-----------------------------99614912995
Content-Disposition: form-data; name="files"; filename="Misc 003.jpg"
TGUHGSDSDJXCK+DSDSDSSDSFDFDSAOJDIOASSAADDASDASDASSADASDSDSDSDFDSFJHSIHFSDUIASUI+/==
-----------------------------99614912995--
这就是⼀个multipart/form-data格式的Http请求,我们可以看到第⼀⾏信息是Http header,这⾥我们只列出了Content-Type这⼀⾏Http header信息,这和我们在html页⾯中form标
签上的enctype属性值⼀致,第⼀⾏中接着有⼀个boundary=---------------------------99614912995,boundary=后⾯的值是随机⽣成的,这个其实是在声明Http请求中表单数据的分隔
符是什么,其代表的是在Http请求中每读到⼀⾏ ---------------------------99614912995,表⽰⼀个section数据,⼀个section有可能是⼀个表单的键值数据,也有可能是⼀个上传⽂件
的⽂件数据。每个section的第⼀⾏是section header,其中Content-Disposition属性都为form-data,表⽰这个section来⾃form标签提交的表单数据,如果section header拥有
filename或filenamestar属性,那么表⽰这个section是⼀个上传⽂件的⽂件数据,否者这个section是⼀个表单的键值数据,section header之后的⾏就是这个section真正的数据
⾏。例如我们上⾯的例⼦中,前两个section就是表单键值对,后⾯三个section是三个上传的图⽚⽂件。
那么接下来,我们来看看怎么⽤⽂件流来上传⼤⽂件,避免⼀次性将所有上传的⽂件都加载到服务器内存中。⽤⽂件流来上传⽐较⿇烦的地⽅在于你⽆法使⽤ASP.NET Core
MVC的模型绑定器来将上传⽂件反序列化为C#对象(如同前⾯介绍的IFormFile接⼝那样)。⾸先我们需要定义类MultipartRequestHelper,⽤于识别Http请求中的各个section类
型(是表单键值对section,还是上传⽂件section)
using System;
using System.IO;
using Microsoft.Net.Http.Headers;
namespace AspNetCore.MultipartRequest
{
public static class MultipartRequestHelper
{
// Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
// The spec says 70 characters is a reasonable limit.
public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
/
/var boundary = Microsoft.Net.Http.Headers.HeaderUtilities.RemoveQuotes(contentType.Boundary);// .NET Core <2.0
var boundary = Microsoft.Net.Http.Headers.HeaderUtilities.RemoveQuotes(contentType.Boundary).Value; //.NET Core 2.0
if (string.IsNullOrWhiteSpace(boundary))
{
throw new InvalidDataException("Missing content-type boundary.");
}
//注意这⾥的boundary.Length指的是boundary=---------------------------99614912995中等号后⾯---------------------------99614912995字符串的长度,也就是section分隔符的长度,上⾯也说了这个长度⼀般不会超过70个字符是⽐较合理的if (boundary.Length > lengthLimit)
{
throw new InvalidDataException(
$"Multipart boundary length limit {lengthLimit} exceeded.");
}
return boundary;
}
public static bool IsMultipartContentType(string contentType)
{
return !string.IsNullOrEmpty(contentType)
&& contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
}
//如果section是表单键值对section,那么本⽅法返回true
public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="key";
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& string.IsNullOrEmpty(contentDisposition.FileName.Value) // For .NET Core <2.0 remove ".Value"
&& string.IsNullOrEmpty(contentDisposition.FileNameStar.Value); // For .NET Core <2.0 remove ".Value"
}
//如果section是上传⽂件section,那么本⽅法返回true
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) // For .NET Core <2.0 remove ".Value"
|| !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value)); // For .NET Core <2.0 remove ".Value"
}
// 如果⼀个section的Header是: Content-Disposition: form-data; name="files"; filename="Misc 002.jpg"
// 那么本⽅法返回: files
public static string GetFileContentInputName(ContentDispositionHeaderValue contentDisposition)
{
return contentDisposition.Name.Value;
}
// 如果⼀个section的Header是: Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
// 那么本⽅法返回: Misc 002.jpg
public static string GetFileName(ContentDispositionHeaderValue contentDisposition)
{
return contentDisposition.FileName.Value;
}
}
}
然后我们需要定义⼀个扩展类叫FileStreamingHelper,其中的StreamFiles扩展⽅法⽤于读取上传⽂件的⽂件流数据,并且将数据写⼊到服务器的硬盘上,其接受⼀个参数
targetDirectory,⽤于声明将上传⽂件存储到服务器的哪个⽂件夹下。
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;
using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace AspNetCore.MultipartRequest
{
public static class FileStreamingHelper
{
private static readonly FormOptions _defaultFormOptions = new FormOptions();
public static async Task<FormValueProvider> StreamFiles(this HttpRequest request, string targetDirectory)
{
if (!MultipartRequestHelper.IsMultipartContentType(request.ContentType))
{
throw new Exception($"Expected a multipart request, but got {request.ContentType}");
}
// Used to accumulate all the form url encoded key value pairs in the
// request.
var formAccumulator = new KeyValueAccumulator();
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, request.Body);
var section = await reader.ReadNextSectionAsync();//⽤于读取Http请求中的第⼀个section数据
while (section != null)
{
ContentDispositionHeaderValue contentDisposition;
var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition);
if (hasContentDispositionHeader)
{
/*
⽤于处理上传⽂件类型的的section
-----------------------------99614912995
Content - Disposition: form - data; name = "files"; filename = "Misc 002.jpg"
ASAADSDSDJXCKDSDSDSHAUSAUASAASSDSDFDSFJHSIHFSDUIASUI+/==
-----------------------------99614912995
*/
if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
{
if (!Directory.Exists(targetDirectory))
{
Directory.CreateDirectory(targetDirectory);
}
var fileName = MultipartRequestHelper.GetFileName(contentDisposition);
var loadBufferBytes = 1024;//这个是每⼀次从Http请求的section中读出⽂件数据的⼤⼩,单位是Byte即字节,这⾥设置为1024的意思是,每次从Http请求的section数据流中读取出1024字节的数据到服务器内存中,然后写⼊下⾯targ using (var targetFileStream = System.IO.File.Create(targetDirectory + "\\" + fileName))
{
/
/section.Body是System.IO.Stream类型,表⽰的是Http请求中⼀个section的数据流,从该数据流中可以读出每⼀个section的全部数据,所以我们下⾯也可以不⽤section.Body.CopyToAsync⽅法,⽽是在⼀个循环中⽤section.Bo await section.Body.CopyToAsync(targetFileStream, loadBufferBytes);
}
}
/*
⽤于处理表单键值数据的section
-----------------------------99614912995
Content - Disposition: form - data; name = "SOMENAME"
Formulaire de Quota
-----------------------------99614912995
*/
else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
{
// Content-Disposition: form-data; name="key"
//
// value
// Do not limit the key name length here because the
// multipart headers length limit is already in effect.
var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
var encoding = GetEncoding(section);
using (var streamReader = new StreamReader(
section.Body,
encoding,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024,
leaveOpen: true))
{
// The value length limit is enforced by MultipartBodyLengthLimit
var value = await streamReader.ReadToEndAsync();
if (String.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
{
value = String.Empty;
}
formAccumulator.Append(key.Value, value); // For .NET Core <2.0 remove ".Value" from key
if (formAccumulator.ValueCount > _defaultFormOptions.ValueCountLimit)
{
throw new InvalidDataException($"Form key count limit {_defaultFormOptions.ValueCountLimit} exceeded.");
}
}
}
}
// Drains any remaining section body that has not been consumed and
// reads the headers for the next section.
section = await reader.ReadNextSectionAsync();//⽤于读取Http请求中的下⼀个section数据
}
// Bind form data to a model
var formValueProvider = new FormValueProvider(
BindingSource.Form,
new FormCollection(formAccumulator.GetResults()),
CultureInfo.CurrentCulture);
return formValueProvider;
}
private static Encoding GetEncoding(MultipartSection section)
{
MediaTypeHeaderValue mediaType;
var hasMediaTypeHeader = MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType);
// UTF-7 is insecure and should not be honored. UTF-8 will succeed in
// most cases.
if (!hasMediaTypeHeader || Encoding.UTF7.Equals(mediaType.Encoding))
{
return Encoding.UTF8;
}
return mediaType.Encoding;
}
}
}
现在我们还需要创建⼀个ASP.NET Core MVC的⾃定义DisableFormValueModelBindingAttribute,该实现接⼝IResourceFilter,⽤来禁⽤ASP.NET Core MVC的模型绑定器,这样当⼀个Http请求到达服务器后,ASP.NET Core MVC就不会在将请求的所有上传⽂件数据都加载到服务器内存后,才执⾏Contoller的Action⽅法,⽽是当Http 请求到达服务器时,就⽴刻执⾏Contoller的Action⽅法。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var formValueProviderFactory = context.ValueProviderFactories
.OfType<FormValueProviderFactory>()
.FirstOrDefault();
if (formValueProviderFactory != null)
{
context.ValueProviderFactories.Remove(formValueProviderFactory);
}
var jqueryFormValueProviderFactory = context.ValueProviderFactories
.OfType<JQueryFormValueProviderFactory>()
.FirstOrDefault();
if (jqueryFormValueProviderFactory != null)
{
context.ValueProviderFactories.Remove(jqueryFormValueProviderFactory);
}
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
最后我们在Contoller中定义⼀个叫Index的Action⽅法,并注册我们定义的DisableFormValueModelBindingAttribute,来禁⽤Action的模型绑定。Index⽅法会调⽤我们前⾯定义的FileStreamingHelper类中的StreamFiles⽅法,其参数为⽤来存储上传⽂件的⽂件夹路径。StreamFiles⽅法会返回⼀个FormValueProvider,⽤来存储Http请求中的表单键值数据,之后我们会将其绑定到MVC的视图模型viewModel上,然后将viewModel传回给客户端浏览器,来告述客户端浏览器⽂件上传成功。
[HttpPost]
[DisableFormValueModelBinding]
public async Task<IActionResult> Index()
{
FormValueProvider formModel;
formModel = await Request.StreamFiles(@"D:\UploadingFiles");
var viewModel = new MyViewModel();
var bindingSuccessful = await TryUpdateModelAsync(viewModel, prefix: "",
valueProvider: formModel);
if (!bindingSuccessful)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
}
return Ok(viewModel);
}
视图模型viewModel的定义如下:
public class MyViewModel
{
public string Username { get; set; }
}
最后我们⽤于上传⽂件的html页⾯和前⾯⼏乎⼀样:
<form method="post" enctype="multipart/form-data" action="/Home/Index">
<div>
<p>Upload one or more files using this form:</p>
<input type="file" name="files" multiple />
</div>
<div>
<p>Your Username</p>
<input type="text" name="username"/>
</div>
<div>
<input type="submit" value="Upload"/>
</div>
</form>
到这⾥上传⼤⽂件时提⽰404
在创建的项⽬⾥⾯是没有 “fig” ⽂件的。
上传⼤⽂件时需要配置下⽂件的⼤⼩,需要在 “config” ⽂件⾥配置。创建⼀个或复制⼀个 “fig”,代码:<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.webServer>
<security>
<requestFiltering>
<!--单位:字节。 -->
<requestLimits maxAllowedContentLength="1073741824"/>
<!-- 1 GB -->
</requestFiltering>
</security>
</system.webServer>
</configuration>
然后在 Startup.cs ⽂件中代码如下:
public void ConfigureServices(IServiceCollection services)
{
//设置接收⽂件长度的最⼤值。
services.Configure<FormOptions>(x =>
{
x.ValueLengthLimit = int.MaxValue;
x.MultipartBodyLengthLimit = int.MaxValue;
x.MultipartHeadersLengthLimit = int.MaxValue;
});
services.AddMvc();
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论