Exportar (0) Imprimir
Expandir todo

Cargar archivos con confianza en el almacenamiento de blobs a través de un control HTML5

Actualizado: diciembre de 2014

Autor: Rahul Rai, consultor asociado, Microsoft Global Delivery

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

Use una API de archivos de HTML5, AJAX y MVC 3 para generar un control de carga de archivos sólido y cargar archivos de gran tamaño de manera segura y con confianza en el almacenamiento de blobs de Azure con capacidad para supervisar el progreso de la operación y la cancelación de la operación.

Los mecanismos de carga de archivos tradicionales carecen de capacidades de procesamiento de archivos en el lado del cliente y, por lo tanto, no se pueden hacer cargas de archivos fragmentadas. La fragmentación de cargas de archivos le permiten realizar acciones útiles, como reintentar la carga solo de los bloques que no pudieron llegar al servidor, facilitar la supervisión del progreso y cargar archivos grandes.

HTML5 proporciona mejoras en el lenguaje y en el contenido multimedia. Ahora podemos crear un control de carga de archivos más segura y tolerante a errores mediante la API de archivos HTML5 y el blob en bloques de Azure.

Para crear el control de carga de archivos, necesitamos desarrollar tres componentes:

  1. JavaScript en el lado del cliente, que acepta y procesa un archivo cargado por el usuario.

  2. Código en el lado del servidor, que procesa los fragmentos del archivo enviados por JavaScript.

  3. Interfaz de usuario en el lado del cliente, que invoca a JavaScript.

Requisitos previos:

  • Explorador compatible con HTML5 (Internet Explorer 10+, FireFox 3.6+ o Google Chrome 7+)

  • MVC 3

  • JQuery

  • API de almacenamiento de Azure

Para generar esta solución, utilice el siguiente algoritmo:

  1. Acepte un archivo del usuario y compruebe la capacidad del explorador para administrar una lista de archivos en HTML5.

  2. Envíe los metadatos del archivo, como el nombre de archivo, el tamaño del archivo, el número de bloques, etc., al servidor con el formato JQuery XmlHttpRequest y reciba la respuesta JSON del servidor. Si el servidor ha guardado correctamente esta información, comenzará a procesar cada fragmento de archivo.

  3. Hasta que se alcance el final del archivo:

    1. Lea un segmento de 1 MB (configurable) del archivo, adjunte un identificador a la solicitud y envíela al servidor con un identificador de bloque (un número generado de forma secuencial), donde un método Action del controlador MVC acepta el blob de HTML5 y lo carga como un blob en bloques al almacenamiento de Azure.

    2. Obtenga una respuesta con el formato de un mensaje JSON del servidor y, si es correcta, procese el siguiente bloque. Si el mensaje JSON tiene datos de error de operación, represente estos datos en el cliente y anule la operación.

  4. Si se detecta un error en el envío del blob desde JavaScript, detenga la operación durante cinco segundos (configurable) y reinténtela para ese blob tres veces más (configurable). Si no puede cargar el blob, anule la operación.

  5. Si todos los blobs se transmiten correctamente al servidor, el método Action del controlador MVC confirma el blob de Azure mediante el envío de la solicitud Put Block List y envía el estado de la operación a JavaScript como un mensaje JSON.

  6. En cualquier momento puede cancelar la operación. El sistema fuerza una salida de la rutina llamando a abort() en el identificador asociado a la solicitud actual en el paso 3a anterior.

El siguiente diagrama muestra los pasos del proceso:

Proceso de carga de blobs HTML 5

Implementemos ahora la solución dividiendo el algoritmo en pasos. Tenga en cuenta que se han omitido las implementaciones triviales para simplificar el procedimiento:

  1. Aceptar el archivo del usuario y comprobar de las capacidades del explorador.

    1. Cree una vista de MVC compatible con HTML5 con los siguientes elementos (información general de alto nivel):

      <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. Cree un archivo de JavaScript e implemente startUpload(), que se invoca desde el botón Cargar. Haga clic y compruebe la compatibilidad del explorador con la lista de archivos. Además, dado que el objeto de enumeración no es modificable, se trata de una buena práctica para inmovilizar tales 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. Diseñe un prototipo (según las directrices de ECMAScript5) para ChunkUpload con todos los conocimientos de archivo encapsulados en él.

      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. Cree una instancia de este prototipo e inicialice sus miembros de datos desde 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. Enviar los metadatos del archivo al servidor y guardar la información.

    1. Envíe atributos de archivo como un mensaje JSON al servidor y, al recibir el mensaje de confirmación, continúe con la carga fragmentada.

      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 en el controlador Home.

      [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. Cargar los fragmentos del archivo.

    1. Cree una función que envíe fragmentos del archivo al servidor como FormData con un identificador de fragmento incremental. No utilice XMLHttpRequest para enviar directamente las solicitudes, ya que el equilibrio de carga de red (NLB) puede eliminar los encabezados de solicitud y la información de fragmentos dejará de tener sentido. Tenga en cuenta que actualmente cada explorador implementa de forma distinta la función de división de HTML5, por lo que se añade el prefijo de proveedor a la función.

      • Para FireFox: mozslice()

      • Para 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 una acción UploadBlock en el controlador Home que toma un identificador incremental como parámetro, carga el fragmento y envía el mensaje JSON al script indicando el estado de la operación. Si el identificador se incrementa hasta el último bloque, confirme todos los bloques enviando una solicitud 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. Llame de manera recursiva a la función de JavaScript desde el controlador de eventos correcto de JQueryXHR en sendNextChunk() hasta que se alcance el final del archivo. Si el servidor informa de un error, analice el mensaje JSON y anule la operación. Si el paquete no puede tener acceso al servidor (controlador de eventos de error de JQueryXHR), intente cargar de nuevo el fragmento con retrasos hasta que alcance el número máximo de reintentos y, a continuación, anule la operación.

    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. Si todos los identificadores incrementales alcanzan el recuento total de bloques, la acción UploadBlock del controlador Home confirmará los bloques enviando una solicitud PutBlockList.

  6. Para cancelar la operación en cualquier momento, cancele la solicitud de AJAX actual a la que ha asociado un identificador llamando a abort() en la solicitud.

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

He escrito una nota de campo sobre la versión Silverlight del control de carga de archivos. Ambos controles solucionan el problema de una carga eficiente a través de diferentes enfoques. Las principales diferencias se resumen en la tabla siguiente:

 

Diferencia Versión de Silverlight Versión de HTML5

Dependencia de explorador

Se ejecuta en todos los exploradores compatibles con Silverlight

Se ejecuta en el menor número posible de exploradores, ya que los estándares de HTML5 apenas están implementados

Modo de carga de archivos

Paralelo

Secuencial

Exposición de las claves de la cuenta de almacenamiento

SAS se expone al control

No tiene conocimiento de las claves de cuenta

Tiempo necesario para representar el control

Alto

Bajo

Componente responsable de la carga

Control en el cliente

Controlador de servidor

Reintentos erróneos en el bloque

Se admite en el cliente

Se admite en el servidor y en el cliente

Requisitos de memoria de cliente

Alto

Bajo

Tiempo necesario para cargar archivo

Bajo

Alto

Utilización del ancho de banda

Alto

Bajo

Tamaño de archivo admitido

Mediana

Alto

  1. Para realizar cargas de fragmentos en paralelo en lugar de la implementación secuencial actual, puede enviar todas las XmlHttpRequests al mismo tiempo en lugar de comprobar si la entrega es correcta para cada paquete prenominal, aunque esto podría colapsar el servidor si hay un número elevado de paquetes.

  2. Si se utiliza el almacenamiento en caché de AppFabric para mantener los metadatos de archivo que se va a cargar, el control puede escalar fácilmente sin necesidad de modificaciones.

  3. Se puede agregar compatibilidad para la carga de varios archivos llevando a cabo el proceso de carga para cada archivo.

Mostrar:
© 2015 Microsoft