此页面有用吗?
您对此内容的反馈非常重要。 请告诉我们您的想法。
更多反馈?
1500 个剩余字符
导出 (0) 打印
全部展开

通过 HTML5 控件可靠上载到 Blob 存储

更新时间: 2015年4月

作者:Rahul Rai,Microsoft 全球交付部助理顾问

源代码: http://code.msdn.microsoft.com/Silverlight-Azure-Blob-3b773e26

使用 HTML5 文件 API、AJAX 和 MVC 3 可以生成强健的文件上载控件,用于安全可靠地将极大型文件上载到 Azure Blob 存储,以及针对操作的进行和操作的取消设置监视。

传统的文件上载机制缺少客户端文件处理功能,因此,无法执行分块文件上载。分块文件上载提供有用的选项,例如,仅重试上载无法传入服务器的块、轻松监视进度和上载大型文件。

HTML5 在语言和多媒体方面做了改进。现在,我们可以使用 HTML5 文件 API 和 Azure 块 Blob 生成容错能力更高、更安全的上载控件。

若要生成文件上载控件,我们需要开发三个组件:

  1. 接受并处理用户上载文件的客户端 JavaScript。

  2. 处理 JavaScript 发送的文件区块的服务器端代码。

  3. 调用 JavaScript 的客户端 UI。

先决条件:

  • HTML5 支持的浏览器(Internet Explorer 10+、FireFox 3.6+ 或 Google Chrome 7+)

  • MVC 3

  • JQuery

  • Azure 存储 API

若要生成此解决方案,请使用以下算法:

  1. 接受来自用户的文件,并检查浏览器处理 HTML5 FileList 的能力。

  2. 将文件的元数据(如文件名、文件大小、块数等)以 JQuery XmlHttpRequest 形式发送到服务器,并从服务器接收 JSON 响应。如果服务器成功保存此信息,则开始处理每个文件区块。

  3. 到达文件末尾时:

    1. 读取 1 MB(可配置)的文件切片,将一个标识符附加到请求,然后将请求连同一个块 ID(按顺序生成的编号)发送到服务器。MVC 控制器中的 Action 方法接受 HTML5 Blob,并将其作为块 Blob 上载到 Azure 存储空间。

    2. 从服务器获取 JSON 消息形式的响应,成功获取后,将处理下一个块。如果 JSON 消息包含有关操作失败的数据,则在客户端上呈现此数据,并中止操作。

  4. 如果在从 JavaScript 发送 Blob 时遇到错误,则暂停操作 5 秒(可配置),然后针对特定 Blob 重试该操作三次(可配置)。如果 Blob 仍然无法上载,则中止操作。

  5. 如果所有 Blob 都已成功传输到服务器,MVC 控制器中的 Action 方法将通过发出 Put Block List 请求来提交 Azure Blob,然后将操作状态以 JSON 消息的形式发送到 JavaScript。

  6. 无论何时,你都可以取消操作,此时,系统将通过对上述步骤 3a 中附加到当前请求的句柄调用 abort() 来强制退出例程。

下图演示该过程的步骤:

HTML 5 Blob 上载过程

现在,让我们通过分解步骤中的算法来实现该解决方案。请注意,为简单起见,我们省去了一些无关紧要的实现步骤:

  1. 接受来自用户的文件并检查浏览器的能力。

    1. 创建 HTML5 支持的 MVC 视图,其中包含以下元素(高级概览):

      <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. 创建一个 JavaScript 文件,并实现从“上载”按钮调用的 startUpload()。单击并测试浏览器与 FileList 的兼容性。此外,由于我们使用的枚举对象不可修改,因此,最好是冻结此类对象。

      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. ChunkUpload 设计一个封装了所有文件信息的原型(根据 ECMAScript5 指南)。

      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. 从此原型创建一个实例,并从 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. 将文件元数据发送到服务器并保存信息。

    1. 将文件属性以 JSON 消息的形式发送到服务器,收到成功消息后,继续进行分块上载。

      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. 控制器上实现 PrepareMetadata Action

      [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. 分块上载文件。

    1. 创建一个函数,用于将文件区块连同一个递增的区块标识符以 FormData 的形式发送到服务器。请不要使用 XMLHttpRequest 直接发送请求,因为网络负载平衡 (NLB) 可能会剥离请求标头,使区块信息变得没有意义。请注意,不同的浏览器当前以不同的方式实现 HTML5 切片函数,因此,该函数当前添加了供应商前缀。

      • 对于 Firefox:mozslice()

      • 对于 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. 控制器中实现使用递增标识符作为参数的 UploadBlock 操作,上载区块,然后将指明操作状态的 JSON 消息发送到脚本。如果该标识符递增到了最后一个块,则通过发送 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. sendNextChunk() 中以递归方式从 JQueryXHR 的成功事件处理程序调用 JavaScript 函数,直到到达文件末尾。如果服务器报告了错误,则分析 JSON 消息并中止操作。如果数据包无法到达服务器(JQueryXHR 的错误事件处理程序),则根据延迟重试上载区块,直到达到了最大重试次数,然后中止操作。

    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. 如果所有递增标识符达到了总块数,则控制器中的 UploadBlock 操作将通过发送 PutBlockList 请求提交块。

  6. 若要随时取消操作,请通过对该请求调用 abort(),来取消你已向其绑定了标识符的当前 AJAX 请求。

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

我已编写了有关文件上载控件 Silverlight 版本的字段注释。这两个控件都可以解决通过不同方法进行可靠上载的问题。下表汇总了主要差别:

 

差异点 Silverlight 版本 HTML5 版本

浏览器依赖性

在所有支持 Silverlight 的浏览器上运行

在更少的浏览器上运行,因为 HTML5 标准尚未广泛实现

文件上载模式

并行

顺序

公开存储帐户密钥

SAS 向控件公开

不知道帐户密钥

呈现控件所要花费的时间

负责上载的组件

客户端最终控件

服务器处理程序

失败块重试

客户端支持

客户端和服务器端支持

客户端内存要求

上载文件所要花费的时间

带宽利用率

支持的文件大小

中型

  1. 若要以并行方式进行区块上载而不像目前一样进行顺序上载,你可以一次性发出所有 XmlHttpRequests,而不用在一次,而不用检查是否已成功传送封装的数据包,不过,如果要上载大量的数据包,这种方式可能会阻塞服务器。

  2. 如果使用 AppFabric 缓存来保留要上载的文件的元数据,该控件可以轻松向外扩展,而无需进行任何修改。

  3. 可通过针对每个文件执行上载进程,来添加对多文件上载的支持。

显示:
© 2015 Microsoft