.NET Core 如何上傳文件及處理大文件上傳


當你使用IFormFile接口來上傳文件的時候,一定要注意,IFormFile會將一個Http請求中的所有文件都讀取到服務器內存后,才會觸發ASP.NET Core MVC的Controller中的Action方法。這種情況下,如果上傳一些小文件是沒問題的,但是如果上傳大文件,勢必會造成服務器內存大量被占用甚至溢出,所以IFormFile接口只適合小文件上傳。

一個文件上傳頁面的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字節的數據到服務器內存中,然后寫入下面targetFileStream的文件流中,可以根據服務器的內存大小調整這個值。這樣就避免了一次加載所有上傳文件的數據到服務器內存中,導致服務器崩潰。

                        using (var targetFileStream = System.IO.File.Create(targetDirectory + "\\" + fileName))
                        {
                            //section.Body是System.IO.Stream類型,表示的是Http請求中一個section的數據流,從該數據流中可以讀出每一個section的全部數據,所以我們下面也可以不用section.Body.CopyToAsync方法,而是在一個循環中用section.Body.Read方法自己讀出數據,再將數據寫入到targetFileStream
                            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

在創建的項目里面是沒有 “web.config” 文件的。

上傳大文件時需要配置下文件的大小,需要在 “config” 文件里配置。創建一個或復制一個 “web.config”,代碼:

<?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();
    }

 

 
       


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM