Caricamenti affidabili nell'archiviazione BLOB tramite un controllo HTML5

 

Autore:Rahul Rai, Associate Consultant, Microsoft Global Delivery

Codice sorgente:http://code.msdn.microsoft.com/Silverlight-Azure-Blob-3b773e26

Usare un'applicazione API, AJAX e MVC 3 con file HTML5 per compilare un controllo caricamento file potente per caricare file di grandi dimensioni in modo sicuro e affidabile nell'archiviazione BLOB di Azure con la possibilità di monitorare l'avanzamento e l'annullamento dell'operazione.

I meccanismi di caricamento file tradizionali non dispongono di funzionalità di elaborazione file lato client e, pertanto, non possono eseguire caricamenti di file in blocchi. La suddivisione in blocchi dei caricamenti di file è molto utile, in quanto consente di riprovare a caricare solo i blocchi che non hanno raggiunto il server, di monitorare facilmente l'avanzamento e di caricare file di grandi dimensioni.

HTML5 offre miglioramenti nel linguaggio e nel multimediale. È ora possibile compilare un controllo caricamento file più sicuro e con maggiore tolleranza d'errore usando l'API file HTML5 e il BLOB in blocchi di Azure.

Per compilare il controllo caricamento file, è necessario sviluppare tre componenti:

  1. JavaScript sul lato client che accetta ed elabora un file caricato dall'utente.

  2. Codice sul lato server che elabora blocchi di file inviati da JavaScript.

  3. Interfaccia utente sul lato client che richiama JavaScript.

Prerequisiti:

  • Browser supportato da HTML5 (Internet Explorer 10+, FireFox 3.6+ o Google Chrome 7+)

  • MVC 3

  • JQuery

  • API di archiviazione di Azure

Per compilare la soluzione, usare l'algoritmo seguente:

  1. Accettare un file dall'utente e verificare la capacità del browser di gestire file HTML5.

  2. Inviare i metadati del file, ad esempio il nome del file, le dimensioni, il numero di blocchi e così via, al server sotto forma di JQuery XmlHttpRequeste ricevere una risposta JSON dal server. Se il server ha salvato correttamente queste informazioni, avviare l'elaborazione di ogni blocco di file.

  3. Finché non viene raggiunta la fine del file:

    1. Leggere una sezione del file da 1 MB (configurabile), collegare un identificatore alla richiesta e inviarlo al server con un ID di blocco (un numero generato in sequenza), dove un metodo Action nel controller MVC accetta il BLOB di HTML5 e lo carica come BLOB in blocchi nell'archiviazione di Azure.

    2. Ottenere una risposta sotto forma di messaggio JSON dal server e al completamento dell'operazione elaborare il blocco successivo. Se il messaggio JSON contiene dati sull'errore dell'operazione, eseguire il rendering dei dati sul client e interrompere l'operazione.

  4. Se viene rilevato un errore nell'invio del BLOB da JavaScript, conservare l'operazione per 5 secondi (configurabile) e riprovare l'operazione per il BLOB specifico altre tre volte (configurabile). Se il caricamento del BLOB continua a generare errori, interrompere l'operazione.

  5. Se tutti i BLOB vengono trasmessi correttamente al server, il metodo Action nel controller MVC esegue il commit del BLOB di Azure inviando la richiesta Put Block List e invia lo stato dell'operazione a JavaScript come messaggio JSON.

  6. In qualsiasi momento, è possibile annullare l'operazione e il sistema forzerà l'uscita dalla routine chiamando abort() sull'handle associato alla richiesta corrente del passaggio 3a precedente.

Il diagramma seguente illustra i passaggi del processo:

Processo di caricamento BLOB in HTML 5

Verrà ora implementata la soluzione suddividendo l'algoritmo in passaggi. Si noti che per semplicità sono state escluse le implementazioni più ovvie:

  1. Accettazione del file dall'utente e verifica delle capacità del browser.

    1. Creare una visualizzazione MVC supportata da HTML5 con gli elementi seguenti (riepilogo generale):

      <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. Creare un file JavaScript e implementare startUpload() richiamato dal pulsante Carica. Fare clic e verificare la compatibilità del browser per FileList. Inoltre, poiché l'oggetto di enumerazione disponibile non è modificabile, è consigliabile bloccare tali oggetti.

      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. Progettare un prototipo (in base alle linee guida di ECMAScript5) per ChunkUpload contenente tutte le informazioni sul file.

      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. Creare un'istanza da questo prototipo e inizializzare i relativi membri dati dall'interno di 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. Inviare i metadati del file al server e salvare le informazioni.

    1. Inviare gli attributi di file come messaggio JSON al server e alla ricezione di un messaggio di conferma, procedere con il caricamento in blocchi.

      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. Implementare PrepareMetadata Action nel controller 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. Suddividere in blocchi il caricamento del file.

    1. Creare una funzione che invia i blocchi di file al server come FormData con un identificatore di blocco incrementale. Non usare XMLHttpRequest per inviare direttamente le richieste poiché il bilanciamento carico di rete può rimuovere le intestazioni di richiesta rendendo inutili le informazioni sul blocco. Si noti che attualmente la funzione slice HTML5 viene implementata differentemente dai diversi browser, pertanto la funzione attualmente è preceduta dall'indicazione del fornitore.

      • Per FireFox: mozslice()

      • Per 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. Implementare un'azione UploadBlock nel controller Home che accetta un identificatore incrementale come parametro, carica il blocco e invia un messaggio JSON allo script indicando lo stato dell'operazione. Se l'identificatore viene incrementato fino all'ultimo blocco, eseguire il commit di tutti i blocchi inviando una richiesta 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. Chiamare in modo ricorsivo la funzione JavaScript dal gestore eventi riusciti di JQueryXHR in sendNextChunk() fino a quando non viene raggiunta la fine del file. Se viene restituito un errore dal server, analizzare il messaggio JSON e interrompere l'operazione. Se il pacchetto non riesce a raggiungere il server (gestore eventi di errore di JQueryXHR), riprovare a caricare il blocco con ritardi fino a quando non si raggiunge il numero massimo di tentativi e quindi interrompere l'operazione.

    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 tutti gli identificatori incrementali raggiungono il numero totale di blocchi, l'azione UploadBlock nel controller Home eseguirà il commit dei blocchi inviando una richiesta PutBlockList.

  6. Per annullare l'operazione in qualsiasi momento, annullare la richiesta AJAX corrente a cui è stato associato un identificatore chiamando abort() sulla richiesta.

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

Ho scritto una nota campo per la versione Silverlight del controllo caricamento file. Entrambi i controlli risolvono il problema di caricamenti affidabili tramite diversi approcci. Le differenze principali sono riepilogate nella tabella seguente:

Differenza

Versione di Silverlight

Versione di HTML5

Dipendenza dal browser

Viene eseguito su tutti i browser che supportano Silverlight

Viene eseguito su un numero minore di browser poiché gli standard HTML5 devono ancora essere ampiamente implementati

Modalità di caricamento file

Parallela

Sequenziale

Esposizione delle chiavi dell'account di archiviazione

Nel controllo viene esposta una firma di accesso condiviso

Non sono previste chiavi dell'account

Tempo impiegato per il rendering del controllo

Max

Min

Componente responsabile del caricamento

Controllo lato client

Gestore server

Tentativi di blocchi non riusciti

Supportato sul lato client

Supportato sul lato client e server

Requisiti di memoria client

Max

Min

Tempo impiegato per il caricamento file

Min

Max

Utilizzo della larghezza di banda

Max

Min

Dimensione file supportata

Media

Max

  1. Per effettuare i caricamenti in blocchi in parallelo anziché usare l'implementazione in sequenza corrente, è possibile inviare tutte le richieste XmlHttpRequests contemporaneamente invece di controllare il recapito corretto di ogni pacchetto prenominale anche se ciò potrebbe bloccare il server nel caso di un numero elevato di pacchetti.

  2. Se per la gestione dei metadati del file da caricare viene usata la memorizzazione nella cache di AppFabric, il controllo può essere facilmente scalato orizzontalmente senza richiedere alcuna modifica.

  3. Potrebbe essere aggiunto il supporto del caricamento di più file eseguendo il processo di caricamento per ogni file.

Mostra: