Asp Net Core Api文件流上传

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


一、文件流

那么接下来,我们来看看怎么用文件流来上传大文件,避免一次性将所有上传的文件都加载到服务器内存中。用文件流来上传比较麻烦的地方在于你无法使用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 文件创建权限问题

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值