Как загрузить большие файлы 2GB + в. NET Core API контроллер из формы? - PullRequest
5 голосов
/ 24 января 2020

При загрузке большого файла через Postman (из внешнего интерфейса с формой, написанной на php У меня та же проблема), я получаю сообщение об ошибке 502 неверный шлюз из Azure Web App:

502 - веб-сервер получил неверный ответ, выступая в качестве шлюза или прокси-сервера. Проблема со страницей, которую вы ищете, и она не может быть отображена. Когда веб-сервер (действуя в качестве шлюза или прокси-сервера) связался с вышестоящим контент-сервером, он получил неверный ответ от контент-сервера.

Ошибка, которую я вижу в Azure комментариях к приложению:

Microsoft.AspNetCore.Connections.ConnectionResetException: клиент отключился <--- Была предпринята попытка выполнить операцию на несуществующем сетевом соединении. (Исключение из HRESULT: 0x800704CD) </p>

Это происходит при попытке загрузить тестовый файл объемом 2 ГБ. С файлом 1 ГБ он работает нормально, но он должен работать до ~ 5 ГБ.

Я оптимизировал часть, которая записывает потоки файлов в azure хранилище больших двоичных объектов, используя подход блочной записи (кредиты на : 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 200 OK для запроса HTTP POST.

Верно ли мое предположение? Если да, как можно добиться того, чтобы загрузка с моего внешнего интерфейса (или почтальона) происходила порциями (например, 15 МБ), которые затем могут быть подтверждены API более быстрым способом, чем целые 2 ГБ? Даже создание URL-адреса SAS для загрузки в azure blob и возврата URL-адреса обратно в браузер было бы хорошо, но я не уверен, как легко это можно интегрировать - также есть максимальные размеры блоков afaik, поэтому для 2 ГБ мне, вероятно, потребуется создать несколько блоков. Если это предложение, было бы здорово получить хороший пример здесь, НО также приветствуются другие идеи!

Это важная часть моей конечной точки контроллера API в C#. Net Core 2.2:

        [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("BlockId{0}",
                                                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
...

И это пример HTTP POST через почтальона:

postman pic

Я установил maxAllowedContentLength & requestTimeout в web.config для тестирования уже:

requestLimits maxAllowedContentLength = "4294967295"

и

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

1 Ответ

2 голосов
/ 27 января 2020

Если вы хотите загрузить большой файл большого двоичного объекта в хранилище Azure, получите токен SAS из своего бэкэнда и загрузите этот файл напрямую со стороны клиента, я думаю, это будет лучшим решением, поскольку оно не добавит нагрузку на ваш бэкэнд. Вы можете использовать приведенный ниже код, чтобы получить токен SAS с разрешением на запись на 2 часа только для своего клиента:

    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, вы можете использовать его для загрузки файлов в хранилище 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> 

Я загрузил ZIP-файл размером 3,6 ГБ в течение 20 минут, и он отлично работает для меня, SDK откроет несколько потоков и загрузит ваш большой файл по частям: enter image description here enter image description here enter image description here

Примечание: в этом случае, пожалуйста, убедитесь, что вы включили CORS для вашего учетная запись хранения, чтобы stat c html мог отправлять запросы в Azure службу хранения.

Надеюсь, это поможет.

...