上云无忧 > 文档中心 > 百度智能云对象存储BOS大文件分块上传原理
对象存储BOS
百度智能云对象存储BOS大文件分块上传原理

文档简介:
用户在使用浏览器上传文件到 BOS 的时候,如果遇到文件过大,需要先将文件分块然后再上传。上传过程中有可能会遇到页面关闭、浏览器崩溃、网络连接中断等问题,从而导致上传失败。BOS 支持分块上传和断点续传功能。分块上传请参见“ Object 的分块上传”,下面介绍“断点续传”的实现方法。
*此产品及展示信息均由百度智能云官方提供。免费试用 咨询热线:400-826-7010,为您提供专业的售前咨询,让您快速了解云产品,助您轻松上云! 微信咨询
  免费试用、价格特惠

原理篇二:大文件分块上传

用户在使用浏览器上传文件到 BOS 的时候,如果遇到文件过大,需要先将文件分块然后再上传。上传过程中有可能会遇到页面关闭、浏览器崩溃、网络连接中断等问题,从而导致上传失败。BOS 支持分块上传和断点续传功能。分块上传请参见“ Object 的分块上传”,下面介绍“断点续传”的实现方法。

实现原理

在我们使用文件分块上传(multipartUpload)的时候, BOS 首先会为这个上传过程分配一个 uploadId 。然后我们将一个文件被分成了若干 part ,每个 part 独立上传,上传完成后,BOS 服务会为这个 part 生成一个 eTag 。当所有 part 都上传完成的时候,BOS 服务根据这些 eTag 和 uploadId 把正确的 part 找出来,并组合成原本的文件。

在这个过程中,BOS 并不需要所有的 part 一下子全部上传完毕,而是可以分多次进行。这也就是说,上传过程中,当页面意外关闭时,我们可以不必从头开始重新上传,而只需要把未上传成功的 part 的再次上传就可以。当然,前提是我们需要把此次上传的 uploadId 和上传完成的 part 的 etag 保存下来(不过,更推荐的做法是通过 listParts 接口来查询更精确的已上传分块信息)。在上传一个 part 之前,可以先检查一下,这个 part 是否已经上传过了,如果以前已上传成功,那就直接跳过这个 part 的上传过程。

对于 uploadId 的存储,需要满足不受页面关闭的影响,比较理想的做法是存储在 localStorage 中。

本地存储

在保存 uploadId 时,我们需要为它指定一个 key ,让不同的文件、不同的上传过程区分开。本示例采用文件名、文件大小、分区大小、 bucket 名称、 object 名称组合成这个 key :

var generateLocalKey = function (blob, chunkSize, bucket, object) {
     return [blob.name, blob.size, chunkSize, bucket, object].join('&');
 };

注意:用这个方式生成的 key 并不准确,如果两次上传过程中,选择了两个文件名相同、文件大小相同,但内容不同的文件,那么用这样的方式并不能正确区分这两个文件。更严谨的方式是根据文件名和文件内容计算 MD5 ,并以此为 key 。

存储方式我们选择 localStorage :

var getUploadId = function (key) {
     return localStorage.getItem(key);
 };  
var setUploadId = function (key, uploadId) {
     return localStorage.setItem(key, uploadId);
 };  
var removeUploadId = function (key) {
     return localStorage.removeItem(key);
 };

初始化分块上传

在初始化分块上传时,有两种可能:

  • 如果已经存在此文件的 uploadId ,那么跳过 initiateMultipartUpload() 方法,改为调用 listParts() 来获取已上传分块信息;
  • 如果没有此文件的 uploadId,那么调用 initiateMultipartUpload() 方法获得新的 uploadId ,并将这个 uploadId 保存在 localStorage 中。
// ...省略BosClient初始化过程
    // var bosClient = new BosClient(bosConfig);

      var initiateMultipartUpload = function (file, chunkSize, bucket, object) {
         // 根据文件生成localStorage的key
         var key = generateLocalKey(file, chunkSize, bucket, object);
          // 获取对应的`uploadId`
         var uploadId = getUploadId(key);
          if (uploadId) {
             // `uploadId`存在,说明有未完成的分块上传。
             // 那么调用`listParts()`获取已上传分块信息。
             return BosClient.listParts(bucket, object, uploadId)
                 .then(function (response) {
                     // response.body.parts里包含了已上传分块的信息
                     response.body.uploadId = uploadId;
                     return response;
                 });
         }
         else {
             // `uploadId`不存在,那么用正常的流程初始化
             return BosClient.initiateMultipartUpload(bucket, object)
                 .then(function (response) {
                     // response.body.uploadId为新生成的`uploadId`
                     response.body.parts = [];
                      // 为了下次能使用断点续传,我们需要把新生成的`uploadId`保存下来
                     setUploadId(key, response.body.uploadId);
                     return response;
                 });
         }
     }

分块上传

在对大文件分割分块时,我们可以跟以上传的分块列表进行比较,以确定是否需要真的进行上传。

function getEtag(partNumber, parts){
         // 从已上传part列表中找出特定partNumber的part的eTag
         for(var i = 0, l = parts.length; i < l; i++){
             if (parts[i].partNumber === partNumber) {
                 return parts[i].eTag;
             }
         }
         return null;
     }
      function getTasks (file, uploadId, chunkSize, bucket, object, parts) {
         var leftSize = file.size;
         var offset = 0;
         var partNumber = 1;

         var tasks = [];

         while (leftSize > 0) {
             var partSize = Math.min(leftSize, chunkSize);
             var task = {
                 file: file,
                 uploadId: uploadId,
                 bucket: bucket,
                 object: object,
                 partNumber: partNumber,
                 partSize: partSize,
                 start: offset,
                 stop: offset + partSize - 1
             };

              // 如果在已上传完成的分块列表中找到这个分块的etag,那么记录下来
             var etag = getEtag(partNumber, parts);
             if (etag){
                 task.etag = etag;
             }
              tasks.push(task);
              leftSize -= partSize;
             offset += partSize;
             partNumber += 1;
         }
          return tasks;
     }

在进行分块上传处理的时候,根据是否已带有etag字段来决定是否需要上传:

    function uploadPartFile(state, bosClient) {
         return function(task, callback) {
             if (task.etag) {
                 // 如果有etag字段,则直接跳过上传
                 callback(null, {
                     http_headers: {
                         etag: task.etag
                     },
                     body: {}
                 });
             }
             else {
                 // 否则进行上传
                 var blob = task.file.slice(task.start, task.stop + 1);
                 bosClient.uploadPartFromBlob(task.bucketName, task.key, task.uploadId, task.partNumber,
 task.partSize, blob)
                     .then(function(res) {
                         ++state.loaded;
                        callbacknull,res);
                     })
                     .catch(function(err) {
                         callback(err);
                     });
             }
         };
     }

流程代码

我们对每个步骤的代码做了一些小修改,但整个流程的代码与分块上传很类似:

var chunkSize = 5 * 1024 * 1024; // 分块大小
    var uploadId;
    initiateMultipartUpload(file, chunkSize, bucket, object)
         .then(function(response) {
             uploadId = response.body.uploadId; // uploadId,可能是服务器刚刚生成的,也可能是从localStorage获取的
             var parts = response.body.parts || []; // 已上传的分块列表。如果是新上传,则为空数组
             var deferred = sdk.Q.defer();
             var tasks = getTasks(blob, uploadId, chunkSize, bucket, key, parts);
             var state = {
                 lengthComputable: true,
                 loaded: parts.length, // 已上传的分块数
                 total: tasks.length
             }; 
             // 如果已上传的分块数大于0,可以先修改一下文件上传进度
             bosClient.emit('progress', state);
             // 为了管理分块上传,使用了async(https://github.com/caolan/async)库来进行异步处理
             var THREADS = 2; // 同时上传的分块数量
             async.mapLimit(tasks, THREADS, uploadPartFile(state, bosClient), function(err, results) {
                 if (err) {
                     deferred.reject(err);
                 } else {
                     deferred.resolve(results);
                 }
             });
             return deferred.promise;
         })
         .then(function(allResponse) {
             var partList = [];
             allResponse.forEach(function(response, index) {
                 // 生成分块清单
                 partList.push({
                     partNumber: index + 1,
                     eTag: response.http_headers.etag
                 });
             }); 

             // 所有分块上传完成后,可以删除对应的`uploadId`了
             removeUploadId(key, uploadId);

             return bosClient.completeMultipartUpload(bucket, key, uploadId, partList); // 完成上传
         })
         .then(function (res) {
             // 上传完成
         })
         .catch(function (err) {
             // 上传失败,添加您的代码
             console.error(err);
         });
相似文档
  • Bce-bos-uploader 支持 STS(Security Token Service) 临时授权的方式。服务端生成一组具体特定操作权限、具有一定时效性的临时 AK/SK ,这组临时的 AK/SK 可以暴露给浏览器端直接使用。用户只需要将服务端返回的 AK/SK 及 SessionToken 设置为 bce-bos-uploader 对应的 bos-ak、bos-sk 和 uptoken 参数。
  • 移动互联时代手机上传数据的场景随处可见,为了方便开发者聚焦于产品的业务逻辑,用户可以直接将文件存储到 BOS 上。 BOS 产品基于 STS 授权方式为用户提供了安全的上传和下载方式, BOS 还支持图片处理服务。BOS 具有成本低、支持海量存储和弹性扩展的特性,能帮助开发者更方便实现移动 APP 业务的开发。
  • 下载完 APP 并安装完成后可以直接通过应用服务器地址访问 BOS ,并进行图片处理。应用服务器地址是指搭建移动应用的后台服务器,默认开启的端口为8080。关于 BOS 的区域和 Bucket 设置都需要在应用服务器进行配置。
  • 上传图片到 BOS 过程中 APP、APP Server 和 BOS 的交互过程如下图所示: APP 上传图片时向 APP Server 发送获取上传方式请求。 APP Server 向 STS 服务器请求 BOS 使用 STS 访问的 AK/SK。
  • 示例代码以 Java 语言为例讲解美图 APP 的实现,代码分为 APP 客户端和应用服务器端两部分。 APP 端代码主要包括 BOSClient 初始化、从 APP Server 端获取 BOS 信息、及上传文件到 BOS 三个功能模块。
官方微信
联系客服
400-826-7010
7x24小时客服热线
分享
  • QQ好友
  • QQ空间
  • 微信
  • 微博
返回顶部