ASP.NET 核心 Web 应用程序 - 如何上传大文件

ASP.NET Core web application - How to upload large files

问题

我正在尝试创建一个 ASP.NET 核心 (3.1) Web 应用程序,它接受文件上传,然后将其分成块以通过 MS Graph API 发送到 Sharepoint。这里还有一些其他的 posts 可以解决类似的问题,但它们假设我具有一定程度的 .NET 知识,而我还没有。所以我希望有人能帮我拼凑一些东西。

配置 Web 服务器和应用程序以接受大文件

我已完成以下操作以允许 IIS Express 上传最多 2GB 的文件:

a) 使用以下代码创建了一个 web.config 文件:

<?xml version="1.0" encoding="utf-8"?>
<configuration>

    <location path="Home/UploadFile">
        <system.webServer>
            <handlers>
                <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
            </handlers>
            <security>
                <requestFiltering>
                    <!--unit is bytes => 2GB-->
                    <requestLimits maxAllowedContentLength="2147483647" />
                </requestFiltering>
            </security>
        </system.webServer>
    </location>
</configuration>

B) 我的 Startup.cs 配置部分有以下内容:

        //Add support for uploading large files  TODO:  DO I NEED THIS?????
        services.Configure<FormOptions>(x =>
        {

            x.ValueLengthLimit = int.MaxValue; // Limit on individual form values
            x.MultipartBodyLengthLimit = int.MaxValue; // Limit on form body size
            x.MultipartHeadersLengthLimit = int.MaxValue; // Limit on form header size
        });

        services.Configure<IISServerOptions>(options =>
        {
            options.MaxRequestBodySize = int.MaxValue;  //2GB
         });

我的表单如下所示,它允许用户选择文件并提交:

@{
    ViewData["Title"] = "Messages";
}
<h1>@ViewData["Title"]</h1>

<p></p>
<form id="uploadForm" action="UploadFile" method="post" enctype="multipart/form-data">
    <dl>
        <dt>
            <label for="file">File</label>
        </dt>
        <dd>
            <input id="file" type="file" name="file" />
        </dd>
    </dl>

    <input class="btn" type="submit" value="Upload" />

    <div style="margin-top:15px">
        <output form="uploadForm" name="result"></output>
    </div>
</form>

控制器的外观如下:

    [HttpPost]
    [RequestSizeLimit(2147483647)]       //unit is bytes => 2GB
    [RequestFormLimits(MultipartBodyLengthLimit = 2147483647)]
    public async void UploadFile()
    {
        User currentUser = null;
        currentUser = await _graphServiceClient.Me.Request().GetAsync();
        //nothing have to do with the file has been written yet. 

    }

当用户单击文件按钮并选择os一个大文件时,我不再收到 IIS 413 错误消息。伟大的。逻辑在我的控制器中命中了正确的方法。

但是我对这部分代码有如下疑问:

对于新手的问题,我们深表歉意。在 pos 到这里之前,我已经尝试做我的研究。但是有些地方还是有点模糊。

编辑 1

根据其中一个 posted 答案中的建议,我已经下载了 sample code that demonstrates how to bypass saving to a local file on the web server. It's based on this article

我再次创建了一个 web.config 文件 - 以避免 IIS 的 413 错误。我还编辑了允许的文件扩展名列表以支持 .pdf、.docx 和 .mp4。

当我尝试 运行 示例项目时,我在“物理存储上传示例”下选择了ose“使用 AJAX 将文件流式传输到控制器端点” " 部分,它死在这里:

                // This check assumes that there's a file
                // present without form data. If form data
                // is present, this method immediately fails
                // and returns the model error.
                if (!MultipartRequestHelper
                    .HasFileContentDisposition(contentDisposition))
                if (!MultipartRequestHelper
                    .HasFileContentDisposition(contentDisposition))
                {
                    ModelState.AddModelError("File", 
                        $"The request couldn't be processed (Error 2).");
                    // Log error

                    return BadRequest(ModelState);
                }

正如代码上方的评论中所提到的,它正在检查表单数据,然后当它找到它时......它就死了。所以我一直在玩 HTML 页面,它看起来像这样:

<form id="uploadForm" action="Streaming/UploadPhysical" method="post" 
    enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;">
    <dl>
        <dt>
            <label for="file">File</label>
        </dt>
        <dd>
            <input id="file" type="file" name="file" />asdfasdf
        </dd>
    </dl>

    <input class="btn" type="submit" value="Upload" />

    <div style="margin-top:15px">
        <output form="uploadForm" name="result"></output>
    </div>
</form>

我试过像这样删除表格:

<dl>
    <dt>
        <label for="file">File</label>
    </dt>
    <dd>
        <input id="file" type="file" name="file" />
    </dd>
</dl>

<input class="btn" type="button" asp-controller="Streaming" asp-action="UploadPhysical" value="Upload" />

<div style="margin-top:15px">
    <output form="uploadForm" name="result"></output>
</div>

但是当我点击它时,按钮现在没有任何反应。

此外,如果您想知道/它有帮助,我将一个文件手动复制到我计算机上的 c:\files 文件夹中,当示例应用程序打开时,它会列出该文件 - 证明它可以读取文件夹。 我添加了读/写权限,希望网络应用程序可以在我到达那一步时写入它。

我实现了一个类似的大文件控制器,但使用的是 mongoDB GridFS。

无论如何,流式传输是处理大文件的最佳方式,因为它速度快且重量轻。 是的,最好的选择是在发送之前将文件保存在服务器存储中。 一个建议是,添加一些验证以允许特定扩展并限制执行权限。

回到你的问题:

The entire file is read into an IFormFile, which is a C# representation of the file used to process or save the file.

The resources (disk, memory) used by file uploads depend on the number and size of concurrent file uploads. If an app attempts to buffer too many uploads, the site crashes when it runs out of memory or disk space. If the size or frequency of file uploads is exhausting app resources, use streaming.

source 1

The CopyToAsync method enables you to perform resource-intensive I/O operations without blocking the main thread.

source 2

这里有例子。

示例 1:

using System.IO;
using Microsoft.AspNetCore.Http;
//...

[HttpPost]
[Authorize]
[DisableRequestSizeLimit]
[RequestFormLimits(ValueLengthLimit = int.MaxValue, MultipartBodyLengthLimit = int.MaxValue)]
[Route("upload")]
public async Task<ActionResult> UploadFileAsync(IFormFile file)
{  
  if (file == null)
    return Ok(new { success = false, message = "You have to attach a file" });

  var fileName = file.FileName;     
  // var extension = Path.GetExtension(fileName);

  // Add validations here...
      
  var localPath = $"{Path.Combine(System.AppContext.BaseDirectory, "myCustomDir")}\{fileName}";
  
  // Create dir if not exists
  Directory.CreateDirectory(Path.Combine(System.AppContext.BaseDirectory, "myCustomDir"));
  
  using (var stream = new FileStream(localPath, FileMode.Create)){
    await file.CopyToAsync(stream);
  }

  // db.SomeContext.Add(someData);
  // await db.SaveChangesAsync();

  return Ok(new { success = true, message = "All set", fileName});      
}  

使用 GridFS 的示例 2:

[HttpPost]
[Authorize]
[DisableRequestSizeLimit]
[RequestFormLimits(ValueLengthLimit = int.MaxValue, MultipartBodyLengthLimit = int.MaxValue)]
[Route("upload")]
public async Task<ActionResult> UploadFileAsync(IFormFile file)
{
  if (file == null)
    return Ok(new { success = false, message = "You have to attach a file" });

  var options = new GridFSUploadOptions
  {
    Metadata = new BsonDocument("contentType", file.ContentType)
  };

  using (var reader = new StreamReader(file.OpenReadStream()))
  {
    var stream = reader.BaseStream;
    await mongo.GridFs.UploadFromStreamAsync(file.FileName, stream, options);    
  }

  return Ok(new { success = true, message = "All set"});
}

你走在正确的道路上,但正如其他人所指出的那样,微软已经提供了一份关于文件上传的书面文档,在你的情况下这是必须阅读的 - https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-6.0#upload-large-files-with-streaming

关于你的问题

  • 需要吗 services.Configure<FormOptions>(x =>

    不,你不知道!而且您也不需要 services.Configure<IISServerOptions>(options =>,它是从您在 web.config

    中配置的 maxAllowedContentLength 中读取的
  • 当用户选择文件时……幕后究竟发生了什么?该文件是否已实际填充到我的表单中并且可以从我的控制器访问?它是流吗?

    如果您禁用表单值模型绑定并使用 MultipartReader,文件将被流式传输并且不会缓存到内存或磁盘中,当您排出流时,将从客户端(浏览器)接受更多数据

  • 如何获取文件?

    检查上面的文档,有一个访问流的工作示例。

  • 如果最终,我需要使用这种方法将此文件发送到 Sharepoint(关于分块的最后一个示例),似乎最好的方法是保存文件在我的服务器某处......然后复制示例代码并尝试将其分块?示例代码似乎指的是文件路径和文件大小,我假设我需要先将它保存到我的网络服务器的某个地方,然后再从那里获取它。

    不一定,使用流式的方式可以直接复制流数据