如何从表单将 2GB+ 的大文件上传到 .NET Core API 控制器?

Posted

技术标签:

【中文标题】如何从表单将 2GB+ 的大文件上传到 .NET Core API 控制器?【英文标题】:Howto upload big files 2GB+ to .NET Core API controller from a form? 【发布时间】:2020-05-10 06:36:46 【问题描述】:

通过 Postman 上传大文件时(从前端使用 php 编写的表单,我遇到了同样的问题)我从 Azure Web 应用程序收到 502 bad gateway 错误消息:

502 - Web 服务器在充当 网关或代理服务器。你所在的页面有问题 正在寻找,无法显示。当 Web 服务器(而 作为网关或代理)联系上游内容服务器, 它收到了来自内容服务器的无效响应。

我在 Azure 应用程序洞察中看到的错误:

Microsoft.AspNetCore.Connections.ConnectionResetException:客户端 已断开连接

尝试上传 2GB 测试文件时会发生这种情况。对于 1GB 的文件,它可以正常工作,但需要达到 ~5GB。

我已经优化了将文件流写入天蓝色 blob 存储的部分,方法是使用块写入方法(归功于:https://www.red-gate.com/simple-talk/cloud/platform-as-a-service/azure-blob-storage-part-4-uploading-large-blobs/),但对我来说,似乎连接正在关闭客户端(邮递员在这种情况下),因为这似乎是单个 HTTP POST 请求,并且底层 Azure 网络堆栈(例如负载均衡器)正在关闭连接,因为我的 API 为 HTTP POST 请求提供返回 HTTP 200 OK 需要很长时间。

我的假设正确吗?如果是,如何实现从我的前端(或邮递员)上传以块(例如 15MB)的形式发生,然后 API 可以以比整个 2GB 更快的方式确认?即使创建一个用于上传到 azure blob 并将 URL 返回到浏览器的 SAS URL 也可以,但我不确定如何轻松集成它 - 还有最大块大小 afaik,所以对于 2GB,我可能需要创建多个块。如果这是建议,那么在这里获得一个好的样品会很棒,但也欢迎其他想法!

这是我在 C# .Net Core 2.2 中的 API 控制器端点中的相关部分:

        [AllowAnonymous]
            [HttpPost("DoPost")]
            public async Task<IActionResult> InsertFile([FromForm]List<IFormFile> files, [FromForm]string msgTxt)
            
                 ...

                        // use generated container name
                        CloudBlobContainer container = blobClient.GetContainerReference(SqlInsertId);

                        // create container within blob
                        if (await container.CreateIfNotExistsAsync())
                        
                            await container.SetPermissionsAsync(
                                new BlobContainerPermissions
                                
                                    // PublicAccess = BlobContainerPublicAccessType.Blob
                                    PublicAccess = BlobContainerPublicAccessType.Off
                                
                                );
                        

                        // loop through all files for upload
                        foreach (var asset in files)
                        
                            if (asset.Length > 0)
                            

                                // replace invalid chars in filename
                                CleanFileName = String.Empty;
                                CleanFileName = Utils.ReplaceInvalidChars(asset.FileName);

                                // get name and upload file
                                CloudBlockBlob blockBlob = container.GetBlockBlobReference(CleanFileName);


                                // START of block write approach

                                //int blockSize = 256 * 1024; //256 kb
                                //int blockSize = 4096 * 1024; //4MB
                                int blockSize = 15360 * 1024; //15MB

                                using (Stream inputStream = asset.OpenReadStream())
                                
                                    long fileSize = inputStream.Length;

                                    //block count is the number of blocks + 1 for the last one
                                    int blockCount = (int)((float)fileSize / (float)blockSize) + 1;

                                    //List of block ids; the blocks will be committed in the order of this list 
                                    List<string> blockIDs = new List<string>();

                                    //starting block number - 1
                                    int blockNumber = 0;

                                    try
                                    
                                        int bytesRead = 0; //number of bytes read so far
                                        long bytesLeft = fileSize; //number of bytes left to read and upload

                                        //do until all of the bytes are uploaded
                                        while (bytesLeft > 0)
                                        
                                            blockNumber++;
                                            int bytesToRead;
                                            if (bytesLeft >= blockSize)
                                            
                                                //more than one block left, so put up another whole block
                                                bytesToRead = blockSize;
                                            
                                            else
                                            
                                                //less than one block left, read the rest of it
                                                bytesToRead = (int)bytesLeft;
                                            

                                            //create a blockID from the block number, add it to the block ID list
                                            //the block ID is a base64 string
                                            string blockId =
                                              Convert.ToBase64String(ASCIIEncoding.ASCII.GetBytes(string.Format("BlockId0",
                                                blockNumber.ToString("0000000"))));
                                            blockIDs.Add(blockId);
                                            //set up new buffer with the right size, and read that many bytes into it 
                                            byte[] bytes = new byte[bytesToRead];
                                            inputStream.Read(bytes, 0, bytesToRead);

                                            //calculate the MD5 hash of the byte array
                                            string blockHash = Utils.GetMD5HashFromStream(bytes);

                                            //upload the block, provide the hash so Azure can verify it
                                            blockBlob.PutBlock(blockId, new MemoryStream(bytes), blockHash);

                                            //increment/decrement counters
                                            bytesRead += bytesToRead;
                                            bytesLeft -= bytesToRead;
                                        

                                        //commit the blocks
                                        blockBlob.PutBlockList(blockIDs);

                                    
                                    catch (Exception ex)
                                    
                                        System.Diagnostics.Debug.Print("Exception thrown = 0", ex);
                                        // return BadRequest(ex.StackTrace);
                                    
                                

                                // END of block write approach
...

这是一个通过 Postman 的 HTTP POST 示例:

我已经在 web.config 中设置了 maxAllowedContentLength 和 requestTimeout 以进行测试:

requestLimits maxAllowedContentLength="4294967295"

aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" requestTimeout="00:59:59" hostingModel="InProcess"

【问题讨论】:

除了延长 timeout 和 maxContentLength 或使用 HttpPostedFileBase 作为模型,您可以在客户端使用 javascript 将文件拆分为 chunks。然后发送大量 100 MB 的小块。 上传大文件,建议你可以试试Azure Storage Data Movement Library。 我认为我认为将 IFormFile 用于大文件是一个坏主意,因为它会将其加载到内存中。 @Charles 谢谢。 HttpPostedFileBase 会改变什么吗?有没有在客户端使用 JavaScript 分块发送的示例? gist.github.com/shiawuen/1534477 看看这个,很简单。在服务器端,您只需保存所有部分并将它们合并在一起,它只是 byte[] 数组。 【参考方案1】:

如果您想将大型 blob 文件上传到 Azure 存储,请从后端获取 SAS 令牌并直接从客户端上传此文件,我认为这将是一个更好的选择,因为它不会增加您的后端工作负载。您可以使用下面的代码为您的客户获取具有 2 小时写入权限的 SAS 令牌:

    var containerName = "<container name>";
    var accountName = "<storage account name>";
    var key = "<storage account key>";
    var cred = new StorageCredentials(accountName, key);
    var account = new CloudStorageAccount(cred,true);
    var container = account.CreateCloudBlobClient().GetContainerReference(containerName);

    var writeOnlyPolicy = new SharedAccessBlobPolicy()  
        SharedAccessStartTime = DateTime.Now,
        SharedAccessExpiryTime = DateTime.Now.AddHours(2),
        Permissions = SharedAccessBlobPermissions.Write
    ;

    var sas = container.GetSharedAccessSignature(writeOnlyPolicy);

获得此 sas 令牌后,您可以使用它在客户端通过storage JS SDK 上传文件。这是一个 html 示例:

<!DOCTYPE html> 
<html> 
<head> 
    <title> 
        upload demo
    </title> 

    <script src= 
"https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"> 
    </script> 


    <script src= "./azure-storage-blob.min.js"> </script> 
</head> 

<body> 
    <div align="center"> 
        <form method="post" action="" enctype="multipart/form-data"
                id="myform"> 

            <div > 
                <input type="file" id="file" name="file" /> 
                <input type="button" class="button" value="Upload"
                        id="but_upload"> 
            </div> 
        </form> 
        <div id="status"></div>


    </div>   

    <script type="text/javascript"> 
        $(document).ready(function()  


            var sasToken = '?sv=2018-11-09&sr=c&sig=XXXXXXXXXXXXXXXXXXXXXXXXXOuqHSrH0Fo%3D&st=2020-01-27T03%3A58%3A20Z&se=2020-01-28T03%3A58%3A20Z&sp=w'
            var containerURL = 'https://stanstroage.blob.core.windows.net/container1/'


            $("#but_upload").click(function()  

                var file = $('#file')[0].files[0]; 
                const container = new azblob.ContainerURL(containerURL + sasToken, azblob.StorageURL.newPipeline(new azblob.AnonymousCredential));
                try 
                    $("#status").wrapInner("uploading .... pls wait");


                    const blockBlobURL = azblob.BlockBlobURL.fromContainerURL(container, file.name);
                    var result  = azblob.uploadBrowserDataToBlockBlob(
                            azblob.Aborter.none, file, blockBlobURL);

                    result.then(function(result) 
                        document.getElementById("status").innerHTML = "Done"
                        , function(err) 
                            document.getElementById("status").innerHTML = "Error"
                            console.log(err); 
                        );


                 catch (error) 
                    console.log(error);
                


            );
        ); 
    </script> 
</body> 

</html> 

我上传了一个 3.6GB 的 .zip 文件 20 分钟,它非常适合我,sdk 将打开多个线程并逐部分上传您的大文件:

注意:在这种情况下,请确保您已为您的存储帐户启用 CORS,以便 statc html 可以将请求发布到 Azure 存储服务。

希望对您有所帮助。

【讨论】:

谢谢,这是一个很好的例子。在这种情况下,您正在为容器创建 SAS 令牌。它也会这样工作吗?我以前知道文件名。所以我可以创建一个容器,但我只会为文件 blob 创建的 SAS 令牌并将列表返回给客户端。基于此列表和 html/js 示例,客户端可以遍历此列表并将文件上传到相应的 blob? 嗨@Romeosan,是的,我认为这是可以实现的

以上是关于如何从表单将 2GB+ 的大文件上传到 .NET Core API 控制器?的主要内容,如果未能解决你的问题,请参考以下文章

使用 jQuery File Upload 上传超过 1GB 到 2GB 的大文件 - blueimp(基于 Ajax)php / yii 它在 Firefox 浏览器中显示错误

如何将大文件上传到 Azure Blob 存储 (.NET Core)

在客户端将大文件(> 2GB)压缩成 ZIP

我无法将大 (> 2GB) 文件上传到 Google Cloud Storage 网络用户界面

为啥 PostgreSQL 9.2.1 可以存储大于 2GB 的大对象?

如何在Python框架Flask中将图像文件从表单上传到数据库