Exportar (0) Imprimir
Expandir Tudo

Carregamentos confiáveis no armazenamento blob através de um controle do HTML5

Atualizado: dezembro de 2014

Autor: Rahul Rai, consultor associado, Microsoft Global Delivery

Código-fonte: http://code.msdn.microsoft.com/Silverlight-Azure-Blob-3b773e26

Use um AJAX, MVC 3 e uma API de Arquivo HTML5 para criar um controle robusto de carregamento de arquivos para carregar arquivos grandes de forma segura e confiável para o armazenamento blob do Azure com uma cláusula de andamento de operação de monitoramento e cancelamento de operação.

Os mecanismos tradicionais de carregamento de arquivos não têm capacidade de processamento do arquivo do lado do cliente e, portanto, não podem carregar arquivos em fragmentos. Os carregamentos de arquivos em fragmentos fornece opções úteis, como tentar carregar somente os blocos que não conseguiram acessar o servidor, monitoramento de andamento simples e carregamento de arquivos grandes.

O HTML5 oferece melhorias em linguagem e multimídia. Agora, você pode criar um controle de carregamento de arquivo mais seguro e tolerante a falhas usando a API de Arquivo HTML5 e blob de bloco do Azure.

Para criar o controle de carregamento de arquivo, desenvolvemos três componentes:

  1. O JavaScript do lado do cliente que aceita e processa um arquivo carregado pelo usuário.

  2. O código do lado do servidor que processa fragmentos de arquivos enviados pelo JavaScript.

  3. A interface do usuário do lado do cliente que invoca o JavaScript.

Pré-requisitos:

  • Navegador de HTML5 compatível (Internet Explorer 10+, FireFox 3.6+ ou Google Chrome 7+)

  • MVC 3

  • JQuery

  • API de Armazenamento do Azure

Para criar essa solução, use o seguinte algoritmo:

  1. Aceite um arquivo do usuário e verifique a capacidade do navegador de lidar com uma HTML5 FileList.

  2. Envie metadados do arquivo, como nome do arquivo, tamanho do arquivo, número de blocos, etc., para o servidor na forma de XmlHttpRequest JQuery e receba resposta JSON do servidor. Se o servidor salvou com êxito essas informações, inicie o processamento de cada fragmento do arquivo.

  3. Até chegar ao fim do arquivo:

    1. Leia um 1 MB (configurável) do arquivo, anexe um identificador para a solicitação e envie-o para o servidor com uma ID de bloco (um número gerado em sequência), onde um método Ação no controlador MVC aceita o blob de HTML5 e o carrega como um blob de bloco no armazenamento do Azure.

    2. Peça uma resposta na forma de uma mensagem JSON do servidor e, quando conseguir, processe o bloco seguinte. Se a mensagem JSON tiver dados sobre a falha de operação, renderize esses dados no cliente e anule a operação.

  4. Se for encontrado um erro ao enviar o blob de JavaScript, pause a operação por 5 segundos (configuráveis) e tente novamente a operação para o blob determinado mais três vezes (configuráveis). Se o blob ainda não for carregado, anule a operação.

  5. Se todos os blobs forem transmitidos com êxito ao servidor, o método Action do Controlador MVC confirma o blob Azure enviando a solicitação Put Block List e envia o status da operação para JavaScript como uma mensagem JSON.

  6. A qualquer momento, você pode cancelar a operação e o sistema força uma saída da rotina chamando abort() no identificador anexado à solicitação atual na etapa 3a acima.

O diagrama a seguir mostra as etapas do processo:

Processo de upload do blob HTML 5

Agora, vamos implementar a solução dividindo o algoritmo em etapas. Observe que implementações triviais ficaram de fora para manter a simplicidade:

  1. Aceitando o arquivo de usuário e verifique os recursos do navegador.

    1. Crie um modo de exibição do MVC compatível com o HTML5 com os seguintes elementos (visão geral de alto nível):

      <input type="file" id="FileInput" multiple="false"/>
      <input type="button" id="upload" name="Upload" onclick="startUpload('FileInput', 1048576, 'uploadProgress', 'statusMessage', 'upload', 'cancel');" />
      <input type="button" id="cancel" name="Cancel" onclick="cancelUpload();" class="button" />
      
    2. Crie um arquivo JavaScript e implemente startUpload() que é chamado com o botão Carregar. Clique e teste a compatibilidade do navegador para FileList. Além disso, como o objeto de enumeração que temos é não modificável, recomendamos congelar esses objetos.

      function startUpload(fileElementId, blockLength, uploadProgressElement, statusLabel, uploadButton, cancelButton) {
          Object.freeze(operationType);
          uploader = Object.create(ChunkedUploader);
          if (!window.FileList) {
              uploader.statusLabel = document.getElementById(statusLabel);
              uploader.displayLabel(operationType.UNSUPPORTED_BROWSER);
              return;
          }...
      
    3. Crie um protótipo (de acordo com as diretrizes da ECMAScript5) para ChunkUpload com todo o conhecimento de arquivo encapsulado nele.

      var ChunkedUploader = {
          constructor: function (controlElements) {
              this.file = controlElements.fileControl.files[0];
              this.fileControl = controlElements.fileControl;
              this.statusLabel = controlElements.statusLabel;
              this.progressElement = controlElements.progressElement;
              this.uploadButton = controlElements.uploadButton;
              this.cancelButton = controlElements.cancelButton;
              this.totalBlocks = controlElements.totalBlocks;
          },
      ... /*UI functions omitted */
      
    4. Crie uma instância desse protótipo e inicialize os seus membros de dados do startUpload().

      function startUpload(fileElementId, blockLength, uploadProgressElement, statusLabel, uploadButton, cancelButton) {
      ...
      uploader = Object.create(ChunkedUploader);
      ...
          uploader.constructor({
              "fileControl": document.getElementById(fileElementId),
              "statusLabel": document.getElementById(statusLabel),
              "progressElement": document.getElementById(uploadProgressElement),
              "uploadButton": document.getElementById(uploadButton),
              "cancelButton": document.getElementById(cancelButton),
              "totalBlocks": 0
          });
      ...
      }
      
      
  2. Envie metadados do arquivo ao servidor e salve as informações.

    1. Envie atributos de arquivo como uma mensagem JSON ao servidor e, ao receber uma mensagem de êxito, continue com o carregamento em fragmentos.

      function startUpload(fileElementId, blockLength, uploadProgressElement, statusLabel, uploadButton, cancelButton) {
      ...
      $.ajax({
              type: "POST",
              async: true,
              url: '/Home/PrepareMetaData',
              data: {
                  'blocksCount': uploader.totalBlocks,
                  'fileName': uploader.file.name,
                  'fileSize': uploader.file.size
              },
              dataType: "json",
              error: function () {
                  uploader.displayLabel(operationType.METADATA_FAILED);
                  uploader.resetControls();
              },
              success: function (operationState) {
                  if (operationState === true) {
                      sendFile(blockLength);
                  }
              }
          });
      ...
      }
      
    2. Implemente PrepareMetadata Action no controlador Início.

      [HttpPost]
              public ActionResult PrepareMetaData(int blocksCount, string fileName, long fileSize)
              {
                  var container = CloudStorageAccount.Parse(ConfigurationManager.AppSettings[Constants.ConfigurationSectionKey]).CreateCloudBlobClient().GetContainerReference(Constants.ContainerName);
                  container.CreateIfNotExist();
                  var fileToUpload = new FileUploadModel()
                      {
                          BlockCount = blocksCount,
                          FileName = fileName,
                          FileSize = fileSize,
                          BlockBlob = container.GetBlockBlobReference(fileName),
                          StartTime = DateTime.Now,
                          IsUploadCompleted = false,
                          UploadStatusMessage = string.Empty
                      };
                  Session.Add(Constants.FileAttributesSession, fileToUpload);
                  return Json(true);
              }
      
  3. Carregue o arquivo em fragmentos.

    1. Crie uma função que envie fragmentos de arquivos ao servidor como FormData com um identificador adicional de partes. Não use o XMLHttpRequest para enviar diretamente solicitações como Balanceamento de Carga de Rede (NLB), pois isso pode remover cabeçalhos de solicitação, tornando as informações dos fragmentos sem sentido. Observe que a função de fatia do HTML5 é implementada atualmente diferente por navegadores diferentes, então a função é prefixada pelo fornecedor no momento.

      • Para o FireFox: mozslice()

      • Para o Chrome: webkitslice()

      var sendFile = function (blockLength) {
      ...
          sendNextChunk = function () {
              fileChunk = new FormData();
              uploader.renderProgress(incrimentalIdentifier);
              if (uploader.file.slice) {
                  fileChunk.append('Slice', uploader.file.slice(start, end));
              }
              else if (uploader.file.webkitSlice) {
                  fileChunk.append('Slice', uploader.file.webkitSlice(start, end));
              }
              else if (uploader.file.mozSlice) {
                  fileChunk.append('Slice', uploader.file.mozSlice(start, end));
              }
              else {
                  uploader.displayLabel(operationType.UNSUPPORTED_BROWSER);
                  return;
              }
              jqxhr = $.ajax({
                  async: true,
                  url: ('/Home/UploadBlock/' + incrimentalIdentifier),
                  data: fileChunk,
                  cache: false,
                  contentType: false,
                  processData: false,
                  type: 'POST',
                  error: function (request, error) { ...
                  },
                  success: function (notice) {
                      ...
                  }
              });
          };
      
          sendNextChunk();
      };
      
    2. Implemente uma ação UploadBlock no controlador Início que usa um identificador incremental como parâmetro, carrega o fragmento e envia a mensagem JSON ao script que indica o status da operação. Se o identificador for incrementado até o último bloco, confirme todos os blocos enviando uma solicitação PutBlockList.

      [HttpPost]
              [ValidateInput(false)]
              public ActionResult UploadBlock(int id)
              {
                  byte[] chunk = new byte[Request.InputStream.Length];
                  Request.InputStream.Read(chunk, 0, Convert.ToInt32(Request.InputStream.Length));
                  if (Session[Constants.FileAttributesSession] != null)
                  {
                      var model = (FileUploadModel)Session[Constants.FileAttributesSession];
                      using (var chunkStream = new MemoryStream(chunk))
                      {
                          var blockId = Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0:D4}", id)));
                          try
                          {
                              model.BlockBlob.PutBlock(blockId, chunkStream, null, new BlobRequestOptions() { RetryPolicy = RetryPolicies.Retry(3, TimeSpan.FromSeconds(10)) });
                          }
                          catch (StorageException e)
                          {
                              ...
                              return Json(new { error = true, isLastBlock = false, message = model.UploadStatusMessage });
                          }
                      }
      
                      if (id == model.BlockCount)
                      {
                          ...
                          try
                          {
                              var blockList = Enumerable.Range(1, (int)model.BlockCount).ToList<int>().ConvertAll(rangeElement => Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0:D4}", rangeElement))));
                              model.BlockBlob.PutBlockList(blockList);
                              ...
                          }
                          catch (StorageException e)
                          {
                              ...
                          }
                          finally
                          {
                              Session.Clear();
                          }
      
                          return Json(new { error = errorInOperation, isLastBlock = model.IsUploadCompleted, message = model.UploadStatusMessage });
                      }
                  }
           else
                  {
                      return Json(new { error = true, isLastBlock = false, message = string.Format(Resources.FailedToUploadFileMessage, Resources.SessonExpired) });
                  }
      
      
                  return Json(new { error = false, isLastBlock = false, message = string.Empty });
              }
      
  4. Recursivamente, chame a função de JavaScript do manipulador de eventos de sucesso de JQueryXHR em sendNextChunk() até o final do arquivo ser atingido. Se um erro for relatado pelo servidor, analise a mensagem JSON e anule a operação. Se o pacote não conseguir acessar o servidor (manipulador de eventos de erro do JQueryXHR), tente carregar o fragmento com atrasos até que o número máximo de tentativas e, depois, anule a operação.

    sendNextChunk = function () {
            ...
            jqxhr = $.ajax({
                ...
                error: function (request, error) {
                    if (error !== 'abort' && retryCount < maxRetries) {
                        ++retryCount;
                        setTimeout(sendNextChunk, retryAfterSeconds * 1000);
                    }
    
                    if (error === 'abort') {
                        ...
                    }
                    else {
                        if (retryCount === maxRetries) {
                            ...
                        }
                        else {
                            uploader.displayLabel(operationType.RESUME_UPLOAD);
                        }
                    }
    
                    return;
                },
                success: function (notice) {
                    if (notice.error || notice.isLastBlock) {
                        ...
                        return;
                    }
    
                    ++incrimentalIdentifier;
                    start = (incrimentalIdentifier - 1) * blockLength;
                    end = Math.min(incrimentalIdentifier * blockLength, uploader.file.size) - 1;
                    retryCount = 0;
                    sendNextChunk();
                }
            });
        };
    
        sendNextChunk();
    };
    
  5. Se todos os identificadores incrementais atingirem a contagem total de blocos, a ação UploadBlock no controlador Início compromete os blocos enviando uma solicitação PutBlockList.

  6. Para cancelar a operação a qualquer momento, cancele a solicitação AJAX atual à qual você vinculou um identificador chamando abort() na solicitação.

    var sendFile = function (blockLength) {
    ...
            jqxhr = $.ajax({
                ...
                }
            });
        };
    
    ...
    };
    
    cancelUpload = function () {
        if (jqxhr !== null) {
            jqxhr.abort();
        }};
    

Eu escrevi uma anotação de campo sobre a versão Silverlight de controle de carregamento de arquivos. Os dois controles resolvem o problema de carregamentos robustos por meio de abordagens diferentes. As principais diferenças estão resumidas nesta tabela:

 

Ponto de diferença Versão do Silverlight Versão do HTML5

Dependência de navegador

Opera em todos os navegadores compatíveis com o Silverlight

Opera em menos navegadores, pois os padrões do HTML5 ainda estão sendo amplamente implementados

Modo de carregamento de arquivo

Paralelo

Sequencial

Exposição das chaves da conta de armazenamento

SAS é exposto ao controle

Não tem conhecimento das chaves de conta

Tempo necessário para processar o controle

Alta

Baixo

Componente responsável pelo carregamento

Controle final do cliente

Manipulador de servidor

Falha ao bloquear tentativas

Suporte no lado do cliente

Suporte no lado do cliente e do servidor

Requisitos de memória do cliente

Alta

Baixo

Tempo necessário para carregar arquivo

Baixo

Alta

Utilização de largura de banda

Alta

Baixo

Tamanho de arquivo com suporte

Média

Alta

  1. Para fazer carregamentos de bloco em paralelo no lugar da implementação sequencial atual, envie todas as XmlHttpRequests ao mesmo tempo em vez de verificar se a entrega de cada pacote prenominal foi bem-sucedida, embora isso possa obstruir o servidor se houver muitos pacotes.

  2. Se o cache do AppFabric for usado para manter os metadados do arquivo para carregar, o controle pode expandir facilmente sem precisar de modificações.

  3. O suporte para carregamento de vários arquivos pode ser adicionado ao processar o carregamento de cada arquivo.

A Microsoft está realizando uma pesquisa online para saber sua opinião sobre o site do MSDN. Se você optar por participar, a pesquisa online lhe será apresentada quando você sair do site do MSDN.

Deseja participar?
Mostrar:
© 2015 Microsoft