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

3

问题

我正在尝试创建一个ASP.NET Core(3.1)Web应用程序,该应用程序接受文件上传,然后将其分成块通过MS Graph API发送到Sharepoint。这里有一些其他帖子涉及类似的问题,但它们假设我已经掌握了一定程度的.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. 

    }

当用户点击文件按钮并选择一个大文件时,我不再收到IIS 413错误消息。太好了。逻辑会命中我的控制器中的正确方法。
但是对于代码的这一部分,我有以下问题:
  • 当用户选择文件时...实际上在幕后发生了什么?文件是否已经被插入到我的表单中,并且可以从我的控制器中访问?

  • 它是一个流吗?

  • 我该如何访问这个文件?

  • 如果最终需要使用 this type 的方法将此文件发送到Sharepoint(分块的最后一个示例),似乎最好的方法是将文件保存在服务器上的某个位置...然后复制示例代码并尝试对其进行分块处理?示例代码似乎在引用文件路径和文件大小,我假设我需要先将其持久化到我的Web服务器上,然后再从那里开始。

  • 如果我确实需要保存它,你能指点我正确的方向吗?也许有一些示例代码可以向我展示如何获取表单中POST的数据并将其保存?

  • 最终,这将需要重构以便没有GUI...但它只是一个API,接受大文件上传到某个地方。但我想我会先学习如何以这种方式完成...然后重构以将我的代码更改为仅API。

抱歉提出这些初学者问题。在发帖之前,我已经尽力做了研究。但有些事情仍然有点模糊。

编辑1

根据发布的答案建议,我下载了演示代码,演示如何绕过保存到Web服务器本地文件的方法。它基于这篇文章

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

当我尝试运行示例项目,并选择“物理存储上传示例”部分下的“使用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文件夹中,当示例应用程序打开时,它确实列出了该文件-证明它可以读取该文件夹。我添加了读/写权限,所以希望Web应用程序在我完成后能够写入它。

请参阅 https://learn.microsoft.com/zh-cn/aspnet/core/mvc/models/file-uploads?view=aspnetcore-6.0 以获取缓冲 / 非缓冲示例。 - Jeremy Lakeman
你有没有得到“直接上传到SharePoint而不先上传到文件服务器,然后流式传输到SharePoint”的答案?我处于类似的情况。 - Shezi
@Shezi 不,我从来没有这样做过。 - dot
2个回答

3
我已经实现了一个类似的大文件控制器,但是使用了mongoDB GridFS。
无论如何,流式传输是处理大文件的最佳方式,因为它快速且轻量级。而且,最好的选择是在发送文件之前将其保存在服务器存储中。建议添加一些验证以允许特定扩展名并限制执行权限。
回到你的问题:
整个文件被读入IFormFile中,这是用于处理或保存文件的C#表示形式。
文件上传使用的资源(磁盘、内存)取决于并发文件上传的数量和大小。如果应用程序尝试缓冲太多的上传,当它耗尽内存或磁盘空间时,网站会崩溃。如果文件上传的大小或频率耗尽了应用程序的资源,请使用流式传输。 source 1 CopyToAsync方法使您能够执行资源密集型的I/O操作,而不会阻塞主线程。

来源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"});
}

1
我正在点赞你的答案,因为它“只是有效地工作了”。但是...我实际上希望避免写入磁盘...所以我正在尝试看看是否可以获得流式解决方案,因为它有助于我的最终目标,即将数据“块”发送到Sharepoint。 - dot

1
你走在正确的道路上,但是正如其他人所指出的,Microsoft已经发布了一篇关于文件上传的精心撰写的文档,在你的情况下必须阅读 - https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-6.0#upload-large-files-with-streaming
至于你的问题:
1. 你需要 `services.Configure(x =>` 吗? 不需要!你也不需要 `services.Configure(options =>`,它从你在 web.config 中配置的 `maxAllowedContentLength` 读取。
2. 当用户选择文件时...底层实际上正在发生什么?文件是否已经被插入到我的表单中,并且可以从我的控制器访问?是一个流吗? 如果禁用表单值模型绑定并使用 MultipartReader,则文件将被流式传输,并且不会缓存在内存或磁盘中,当你消耗流时,更多的数据将从客户端(浏览器)接受。
3. 如何获取文件? 检查上面的文档,有一个可用的示例来访问流。
4. 如果最终我需要使用这种方法将此文件发送到 Sharepoint(分块的最后一个示例),似乎最好的方法是在我的服务器上保存文件...然后复制示例代码并尝试将其分块?示例代码似乎在引用文件路径和文件大小,我假设我需要首先将其持久化到我的 Web 服务器上,然后再进行操作。 不一定,使用流式传输方法,你可以直接复制流数据。

所以我一直在回顾那个例子...它是我最初开始但无法启动的例子。但是我已经从微软下载了演示此代码的sampleApp,但它失败了。请参见Edit 1。 - dot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接