提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
一、文件流
那么接下来,我们来看看怎么用文件流来上传大文件,避免一次性将所有上传的文件都加载到服务器内存中。用文件流来上传比较麻烦的地方在于你无法使用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="F:\Misc 002.jpg"
// 那么本方法返回: files
public static string GetFileContentInputName(ContentDispositionHeaderValue contentDisposition)
{
return contentDisposition.Name.Value;
}
// 如果一个section的Header是: Content-Disposition: form-data; name="myfile1"; filename="F:\Misc 002.jpg"
// 那么本方法返回: Misc 002.jpg
public static string GetFileName(ContentDispositionHeaderValue contentDisposition)
{
return Path.GetFileName(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<FileReturnType> StreamFiles(this HttpRequest request, string targetDirectory = null)
{
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 fileEntity = new List<FileEntity>();
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))
{
var fileName = MultipartRequestHelper.GetFileName(contentDisposition);
var streamdFileContent = await FileHelpers.ProcessStreamedFile(section, fileName,targetDirectory)
fileEntity.Add( new FileEntity
{
Name = fileName;
AttachmentType = Path.GetExtension(fileName).ToLower(),
SavePath = streamdFileContent.Item2,
FileSize = FileHelper.GetLength(streamdFileContent.Item.Length);
FileFormat= Path.GetExtension(fileName).ToLower(),
});
}
/*
用于处理表单键值数据的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 fileReturnType = new FileReturnType();
fileReturnType.formValueProvider = new FormValueProvider(
BindingSource.Form,
new FormCollection(formAccumulator.GetResults()),
CultureInfo.CurrentCulture);
fileReturnType.fileEntity = fileEntity;
return fileReturnType;
}
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就不会在将请求的所有上传文件数据都加载到服务器内存后,才执行Controller的Action方法,而是当Http请求到达服务器时,就立刻执行Controller的Action方法。
ASP.NET Core 3.X使用下面的代码:
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Linq;
namespace AspNetCore.MultipartRequest
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<FormFileValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
}
最后我们在Controller中定义一个叫Index的Action方法,并注册我们定义的DisableFormValueModelBindingAttribute拦截器,来禁用Action的模型绑定。Index方法会调用我们前面定义的FileStreamingHelper类中的StreamFiles方法,其参数为用来存储上传文件的文件夹路径。StreamFiles方法会返回一个FormValueProvider,用来存储Http请求中的表单键值数据,之后我们会将其绑定到MVC的视图模型viewModel上,然后将viewModel传回给客户端浏览器,来告述客户端浏览器文件上传成功。
[HttpPost]
[DisableFormValueModelBinding]
[DisableRequestSizeLimit]
public async Task<IActionResult> Index()
{
FormValueProvider formModel;
// formModel = await Request.StreamFiles(@"F:\UploadingFiles");
formModel = await Request.StreamFiles();
var viewModel = new MyViewModel();
var bindingSuccessful = await TryUpdateModelAsync(viewModel, prefix: "",
valueProvider: formModel);
if (!bindingSuccessful)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
}
//bindingSuccessful 接受到上传值的属性,自己保存到数据库
return Ok(viewModel);
}
public static class FileHelpers
{
private static string _filePath = _filePath ?? AppContext.BaseDirectory;//保存视频文件路径
private static string _dbFilePath; //保存数据库中的文件夹路径
///<summary>
/// 文件返回类型
///</summary>
///<param name="section">文件流</param>
///<param name="fileName">文件名</param>
///<param name="targetDirectory">存储位置</param>
///<returns></returns>
public static async Task<Tuple<byte[],string>> ProcessStreamedFile(MultipartSection section,string fileName,
string targetDirectory = null)
{
try
{
if(string.IsNullOrEmpty(fileName))
{
throw new Exception("文件名不能为空");
}
using(var memoryStream = new MemoryStream())
{
//这个是每一次从Http请求的section中读出文件数据的大小,单位是Byte即字节,这里设置为1024的意思是,每次从Http请求的section数据流中读取出1024字节的数据到服务器内存中,然后写入下面targetFileStream的文件流中,可以根据服务器的内存大小调整这个值。这样就避免了一次加载所有上传文件的数据到服务器内存中,导致服务器崩溃。
var loadBufferBytes = 1024;
//section.Body是System.IO.Stream类型,表示的是Http请求中一个section的数据流,从该数据流中可以读出每一个section的全部数据,所以我们下面也可以不用section.Body.CopyToAsync方法,而是在一个循环中用section.Body.Read方法自己读出数据(如果section.Body.Read方法返回0,表示数据流已经到末尾,数据已经全部都读取完了),再将数据写入到targetFileStream
await section.Body.CopyToAsync(memoryStream ,loadBufferBytes);
var ext = Path.GetExtension(fileName).ToLower();
if(memoryStream.Length == 0 )
{
throw new Exception("文件是空文件");
}
else if(memoryStream.Length > 2147483648 )
{
throw new Exception("文件大小超过2GB");
}
else if(memoryStream.Length > 2147483648 )
{
throw new Exception("文件大小超过2GB");
}
else if(ext.Contains(".mp4") || ext.Contains(".avi") || ext.Contains(".wmv")
|| ext.Contains(".3gp") || ext.Contains(".dv"))
{
SaveFile(memoryStream, fileName, targetDirectory);
Tuple<byte[],string> tuple = new Tuple<byte[],string>(memoryStream.ToArray(), _dbFilePath);
return tuple;
}
}
}
catch (Exception ex)
{
throw new Exception(ex.InnerException?.Message ?? ex.Message);
}
}
private static void SaveFile(MultipartSection memoryStream, string fileName,
string targetDirectory = null)
{
string currentDate = DateTime.Now.ToString("yyyy");
string subfolder = "Video";
//F:\Practice\DOTNET CORE\StudentManagement\StudentManagement\bin\Debug\net5.0\20210901\Video
var folder = Path.Combine(currentDate, subfolder);
var uploadPath = Path.Combine(_filePath,folder);
//自定义存储路径
if (!string.IsNullOrEmpty(targetDirectory))
{
if (!Directory.Exists(uploadPath))
{
Directory.CreateDirectory(uploadPath);
}
uploadPath = targetDirectory;
}
//规定路径
else if (!string.IsNullOrEmpty(uploadPath))
{
Directory.CreateDirectory(uploadPath);
}
var ext = Path.GetExtension(fileName).ToLower();
var newName = GenerateId.GenerateOrderNumber() + ext;
using (var fs = new FileStream(Path.Combine(uploadPath, newName), FileMode.Create))
{
fs.Write(memoryStream.ToArray(), 0, memoryStream.ToArray().Length);
fs.Close();
_dbFilePath = Path.Combine(folder,newName);
}
}
//GenerateId类
/// <summary>
/// 获取文件大小显示为字符串
/// </summary>
/// <param name="lengthOfDocument">文件大小 单位: Byte 类型:long</param>
/// <returns></returns>
public static string GetLength(long lengthOfDocument)
{
//如果文件大小0-1024B以内 显示以B为单位
//如果文件大小1KB-1024KB以内 显示以KB为单位
//如果文件大小1M-1024M以内 显示以M为单位
//如果文件大小1024 MB以内 显示以GB为单位
if (lengthOfDocument <1024)
return string.Format(lengthOfDocument.ToString() + 'B');
else if(lengthOfDocument >1024 && lengthOfDocument <= Math.Pow(1024, 2))
return string.Format((lengthOfDocument / 1024.0).ToString("F2") + "KB");
else if (lengthOfDocument > Math.Pow(1024, 2) && lengthOfDocument <= Math.Pow(1024, 3))
return string.Format((lengthOfDocument / 1024.0 / 1024.0).ToString("F2") + "M");
else
return string.Format((lengthOfDocument / 1024.0 / 1024.0 / 1024.0).ToString("F2") + "GB");
}
///<summary>
/// 文件返回类型
///</summary>
public class FileReturnType
{
public FileReturnType()
{
fileEntity = new List<>(FileEntity);
}
public FormValueProvider formValueProvider {get;set;}
public List<FileEntity> fileEntity {get;set;}
}
}
二、类
public class FileEntity
{
public string Name {get;set;}
//附件类型
public string AttachmentType{get;set;}
public string SavePath {get;set;}
//文件格式
public string FileFormat{get;set;}
public string FileSize{get;set;}
}
总结
为自己提供方便,有错误请指出(写的有点乱) 谢谢。
参考文献:
https://www.cnblogs.com/OpenCoder/p/9785031.html 文件流上传
https://hub.fastgit.org/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Controllers/StreamingController.cs 官方上传文件
https://blog.csdn.net/qq_22098679/article/details/81327074 文件创建权限问题