Object的分块上传

除了通过putObject()方法上传文件到BOS以外,BOS还提供了另外一种上传模式:分块上传(Multipart Upload)。用户可以在如下的应用场景内(但不仅限于此),使用分块上传模式,如:

  • 需要支持断点上传。
  • 上传超过5GB大小的文件。
  • 网络条件较差,和BOS的服务器之间的连接经常断开。
  • 需要流式地上传文件。
  • 上传文件之前,无法确定上传文件的大小。

分块完成Multipart Upload

假设有一个文件,本地路径为/path/to/file.zip,由于文件比较大,使用分块上传其传输到BOS中。

  • 基本流程

    1. 初始化Multipart Upload。
    2. 上传分块。
    3. 完成分块上传。

初始化Multipart Upload

使用initiateMultipartUpload方法来初始化一个分块上传事件:

  • 示例代码

    BOSInitiateMultipartUploadRequest* initMPRequest = [[BOSInitiateMultipartUploadRequest alloc] init];
    initMPRequest.bucket = @"<bucketname>";
    initMPRequest.key = @"<objectname>";
    initMPRequest.contentType = @"<content type>";
    
    __block BOSInitiateMultipartUploadResponse* initMPResponse = nil;
    BCETask* task = [client initiateMultipartUpload:initMPRequest];
    task.then(^(BCEOutput* output) {
      if (output.response) {
          initMPResponse = (BOSInitiateMultipartUploadResponse*)output.response;
          NSLog(@"initiate multipart upload success!");
      }
    
      if (output.error) {
          NSLog(@"initiate multipart upload failure");
      }
    });
    [task waitUtilFinished];
    
    NSString* uploadID = initMPResponse.uploadId;
    

    说明:initiateMultipartUpload的返回结果中含有uploadId,它是区分分块上传事件的唯一标识,在后面的操作中,我们将用到它。

上传分块

将文件分块上传。

  • 示例代码

    // 计算分块个数
    NSString* file = @"/path/to/file.zip";
    NSDictionary<NSString*, id>* attr = [[NSFileManager defaultManager] attributesOfItemAtPath:file error:nil];
    uint64_t fileSize = attr.fileSize;
    uint64_t partSize = 1024 * 1024 * 5L;
    uint64_t partCount = fileSize / partSize;
    if (fileSize % partSize != 0) {
      ++partCount;
    }
    
    NSMutableArray<BOSPart*>* parts = [NSMutableArray array];
    NSFileHandle* handle = [NSFileHandle fileHandleForReadingAtPath:@"/path/to/file.zip"];
    for (uint64_t i = 0; i < partCount; ++i) {
      // seek
      uint64_t skip = partSize * i;
      [handle seekToFileOffset:skip];
      uint64_t size = (partSize < fileSize - skip) ? partSize : fileSize - skip;
    
      // data
      NSData* data = [handle readDataOfLength:size];
    
      // request
      BOSUploadPartRequest* uploadPartRequest = [[BOSUploadPartRequest alloc] init];
      uploadPartRequest.bucket = @"<bucketname>";
      uploadPartRequest.key = @"<objectname>";
      uploadPartRequest.objectData.data = data;
      uploadPartRequest.partNumber = i + 1;
      uploadPartRequest.uploadId = uploadID;
    
      __block BOSUploadPartResponse* uploadPartResponse = nil;
      task = [client uploadPart:uploadPartRequest];
      task.then(^(BCEOutput* output) {
          if (output.response) {
              uploadPartResponse = (BOSUploadPartResponse*)output.response;
              BOSPart* part = [[BOSPart alloc] init];
              part.partNumber = i + 1;
              part.eTag = uploadPartResponse.eTag;
              [parts addObject:part];
          }
      });
      [task waitUtilFinished];
    }
    

    注意:上面代码的核心是调用uploadPart方法来上传每一个分块,但是要注意以下几点:

    • uploadPart方法要求除最后一个Part以外,其他的Part大小都要大于等于5MB。但是Upload Part接口并不会立即校验上传Part的大小;只有当Complete Multipart Upload的时候才会校验。
    • 为了保证数据在网络传输过程中不出现错误,建议您在uploadPart后,使用每个分块BOS返回的Content-MD5值分别验证已上传分块数据的正确性。当所有分块数据合成一个Object后,不再含MD5值。
    • Part号码的范围是1~10000。如果超出这个范围,BOS将返回InvalidArgument的错误码。
    • 每次上传Part时都要把流定位到此次上传块开头所对应的位置。
    • 每次上传Part之后,BOS的返回结果会包含一个BOSPart对象,它是上传块的ETag与块编号(PartNumber)的组合,在后续完成分块上传的步骤中会用到它,因此需要将其保存起来。一般来讲这些BOSPart对象将被保存到数组中。

完成分块上传

  • 示例代码

    BOSCompleteMultipartUploadRequest* compMultipartRequest = [[BOSCompleteMultipartUploadRequest alloc] init];
    compMultipartRequest.bucket = @"<bucketname>";
    compMultipartRequest.key = @"<objectname>";
    compMultipartRequest.uploadId = uploadID;
    compMultipartRequest.parts = parts;
    
    __block BOSCompleteMultipartUploadResponse* complResponse = nil;
    task = [client completeMultipartUpload:compMultipartRequest];
    task.then(^(BCEOutput* output) {
      if (output.response) {
          complResponse = (BOSCompleteMultipartUploadResponse*)output.response;
          NSLog(@"complte multiparts success!");
      }
    
      if (output.error) {
          NSLog(@"complte multiparts failure %@", output.error);
      }
    });
    [task waitUtilFinished];
    

    说明:上面代码中的 parts 是第二步中保存的parts的列表,BOS收到用户提交的Part列表后,会逐一验证每个数据Part的有效性。当所有的数据Part验证通过后,BOS将把这些数据part组合成一个完整的Object。

  • 完整示例

    #import <BaiduBCEBasic/BaiduBCEBasic.h>
    #import <BaiduBCEBOS/BaiduBCEBOS.h>
    
    void example(void) {
    // 初始化
    BCECredentials* credentials = [[BCECredentials alloc] init];
    credentials.accessKey = @"<access key>";
    credentials.secretKey = @"<secret key>";
    BOSClientConfiguration* configuration = [[BOSClientConfiguration alloc] init];
    configuration.credentials = credentials;
    
    BOSClient* client = [[BOSClient alloc] initWithConfiguration:configuration];
    
    // 初始化分块上传
    BOSInitiateMultipartUploadRequest* initMPRequest = [[BOSInitiateMultipartUploadRequest alloc] init];
    initMPRequest.bucket = @"<bucketname>";
    initMPRequest.key = @"<objectname>";
    initMPRequest.contentType = @"<content type>";
    
     __block BOSInitiateMultipartUploadResponse* initMPResponse = nil;
     BCETask* task = [client initiateMultipartUpload:initMPRequest];
     task.then(^(BCEOutput* output) {
         if (output.response) {
             initMPResponse = (BOSInitiateMultipartUploadResponse*)output.response;
             NSLog(@"initiate multipart upload success!");
         }
    
         if (output.error) {
             NSLog(@"initiate multipart upload failure");
         }
     });
     [task waitUtilFinished];
    
     NSString* uploadID = initMPResponse.uploadId;
    
     // 计算分块个数
     NSString* file = @"/path/to/file.zip";
     NSDictionary<NSString*, id>* attr = [[NSFileManager defaultManager] attributesOfItemAtPath:file error:nil];
     uint64_t fileSize = attr.fileSize;
     uint64_t partSize = 1024 * 1024 * 5L;
     uint64_t partCount = fileSize / partSize;
     if (fileSize % partSize != 0) {
         ++partCount;
     }
    
     NSMutableArray<BOSPart*>* parts = [NSMutableArray array];
    
     NSFileHandle* handle = [NSFileHandle fileHandleForReadingAtPath:@"/path/to/file.zip"];
     for (uint64_t i = 0; i < partCount; ++i) {
         // seek
         uint64_t skip = partSize * i;
         [handle seekToFileOffset:skip];
         uint64_t size = (partSize < fileSize - skip) ? partSize : fileSize - skip;
    
         // data
         NSData* data = [handle readDataOfLength:size];
    
         // request
         BOSUploadPartRequest* uploadPartRequest = [[BOSUploadPartRequest alloc] init];
         uploadPartRequest.bucket = @"<bucketname>";
         uploadPartRequest.key = @"<objectname>";
         uploadPartRequest.objectData.data = data;
         uploadPartRequest.partNumber = i + 1;
         uploadPartRequest.uploadId = uploadID;
    
         __block BOSUploadPartResponse* uploadPartResponse = nil;
         task = [client uploadPart:uploadPartRequest];
         task.then(^(BCEOutput* output) {
             if (output.response) {
                 uploadPartResponse = (BOSUploadPartResponse*)output.response;
                 BOSPart* part = [[BOSPart alloc] init];
                 part.partNumber = i + 1;
                 part.eTag = uploadPartResponse.eTag;
                 [parts addObject:part];
             }
         });
         [task waitUtilFinished];
     }
    
     BOSCompleteMultipartUploadRequest* compMultipartRequest = [[BOSCompleteMultipartUploadRequest alloc] init];
     compMultipartRequest.bucket = @"<bucketname>";
     compMultipartRequest.key = @"<objectname>";
     compMultipartRequest.uploadId = uploadID;
     compMultipartRequest.parts = parts;
    
     __block BOSCompleteMultipartUploadResponse* complResponse = nil;
     task = [client completeMultipartUpload:compMultipartRequest];
     task.then(^(BCEOutput* output) {
         if (output.response) {
             complResponse = (BOSCompleteMultipartUploadResponse*)output.response;
             NSLog(@"complte multiparts success!");
         }
    
         if (output.error) {
             NSLog(@"complte multiparts failure %@", output.error);
         }
     });
     [task waitUtilFinished];
    }
    

取消分块上传

用户可以使用abortMultipartUpload方法取消分块上传。

  • 示例代码

    BOSAbortMultipartUploadRequest* abortRequest = [[BOSAbortMultipartUploadRequest alloc] init];
    abortRequest.bucket = @"bucket";
    abortRequest.key = @"<objectname>";
    abortRequest.uploadId = uploadID;
    
    __block BOSAbortMultipartUploadResponse* abortResponse = nil;
    task = [client abortMultipartUpload:abortRequest];
    task.then(^(BCEOutput* output) {
      if (output.response) {
          abortResponse = (BOSAbortMultipartUploadResponse*)output.response;
          NSLog(@"abort multiparts success!");
      }
    
      if (output.error) {
          NSLog(@"abort multiparts failure %@", output.error);
      }
    });
    [task waitUtilFinished];
    

获取未完成的分块上传

用户可以使用listMultipartUploads方法获取Bucket内未完成的分块上传事件。

  • 基本流程

    1. 创建BOSListMultipartUploadsRequest类的实例,传入<BucketName>参数。
    2. 创建BOSClient类的实例,执行BOSClient listMultipartUploads方法。
    3. listMultipartUploads返回所有未完成的分块上传信息。
  • 示例代码

    BOSListMultipartUploadsRequest* listMultipartRequest = [[BOSListMultipartUploadsRequest alloc] init];
    listMultipartRequest.bucket = @"<bucketname>";
    
    __block BOSListMultipartUploadsResponse* listMultipartResponse = nil;
    task = [client listMultipartUploads:listMultipartRequest];
    task.then(^(BCEOutput* output) {
      if (output.response) {
          listMultipartResponse = (BOSListMultipartUploadsResponse*)output.response;
          NSLog(@"list multipart success");
      }
    
      if (output.error) {
          NSLog(@"list multipart failure %@", output.error);
      }
    });
    [task waitUtilFinished];
    

    注意:

    • 默认情况下,如果Bucket中的分块上传事件的数目大于1000,则只会返回1000个Object,并且返回结果中IsTruncated的值为True,同时返回nextKeyMarker作为下次读取的起点。
    • 若想获取更多分块上传事件,可以使用keyMarker参数分次读取。
  • 完整示例

    #import <BaiduBCEBasic/BaiduBCEBasic.h>
    #import <BaiduBCEBOS/BaiduBCEBOS.h>
    
    void example(void) {
      // 初始化
      BCECredentials* credentials = [[BCECredentials alloc] init];
      credentials.accessKey = @"<access key>";
      credentials.secretKey = @"<secret key>";
      BOSClientConfiguration* configuration = [[BOSClientConfiguration alloc] init];
      configuration.credentials = credentials;
    
      BOSClient* client = [[BOSClient alloc] initWithConfiguration:configuration];
    
      BOSListMultipartUploadsRequest* listMultipartRequest = [[BOSListMultipartUploadsRequest alloc] init];
      listMultipartRequest.bucket = @"<bucketname>";
    
      __block BOSListMultipartUploadsResponse* listMultipartResponse = nil;
      BCETask* task = [client listMultipartUploads:listMultipartRequest];
      task.then(^(BCEOutput* output) {
          if (output.response) {
              listMultipartResponse = (BOSListMultipartUploadsResponse*)output.response;
              NSLog(@"list multipart success");
          }
    
          if (output.error) {
              NSLog(@"list multipart failure %@", output.error);
          }
      });
      [task waitUtilFinished];
    
      for (BOSMultipartUpload* upload in listMultipartResponse.uploads) {
          NSLog(@"upload id : %@", upload.uploadId);
      }
    }
    

获取所有已上传的分块信息

用户可以使用listParts方法获取某个上传事件中所有已上传的块。

  • 基本流程

    1. 创建BOSListPartsRequest类的实例,传入<BucketName>,<ObjectKey>, <UploadId>参数。
    2. 创建BOSClient类的实例,执行BOSClient listParts方法。
    3. listParts返回所有已上传part的信息。
  • 示例代码

    BOSListPartsRequest* listPartsRequest = [[BOSListPartsRequest alloc] init];
    listPartsRequest.bucket = @"<bucketname>";
    listPartsRequest.key = @"<objectname>";
    listPartsRequest.uploadId = @"<upload id>";;
    
    __block BOSListPartsResponse* listPartsResponse = nil;
    BCETask* task = [client listParts:listPartsRequest];
    task.then(^(BCEOutput* output) {
      if (output.response) {
          listPartsResponse = (BOSListPartsResponse*)output.response;
          NSLog(@"list parts success!");
      }
    
      if (output.error) {
          NSLog(@"list part failure %@", output.error);
      }
    });
    [task waitUtilFinished];
    
    for (BOSPart* part in listPartsResponse.parts) {
        NSLog(@"part etag %@", part.eTag);
    }
    

    注意:

    • 默认情况下,如果Bucket中的分块上传事件的数目大于1000,则只会返回1000个Object,并且返回结果中IsTruncated的值为True,同时返回NextPartNumberMarker作为下次读取的起点。
    • 若想获取更多已上传的分块信息,可以使用PartNumberMarker参数分次读取。
  • 完整示例

    #import <BaiduBCEBasic/BaiduBCEBasic.h>
    #import <BaiduBCEBOS/BaiduBCEBOS.h>
    
    void example(void) {
    // 初始化
    BCECredentials* credentials = [[BCECredentials alloc] init];
    credentials.accessKey = @"<access key>";
    credentials.secretKey = @"<secret key>";
    BOSClientConfiguration* configuration = [[BOSClientConfiguration alloc] init];
    configuration.credentials = credentials;
    
    BOSClient* client = [[BOSClient alloc] initWithConfiguration:configuration];
    
    BOSListPartsRequest* listPartsRequest = [[BOSListPartsRequest alloc] init];
    listPartsRequest.bucket = @"<bucketname>";
    listPartsRequest.key = @"<objectname>";
    listPartsRequest.uploadId = @"<upload id>";;
    
    __block BOSListPartsResponse* listPartsResponse = nil;
    BCETask* task = [client listParts:listPartsRequest];
    task.then(^(BCEOutput* output) {
        if (output.response) {
            listPartsResponse = (BOSListPartsResponse*)output.response;
            NSLog(@"list parts success!");
       }
    
       if (output.error) {
            NSLog(@"list part failure %@", output.error);
       }
    });
    [task waitUtilFinished];
    
    for (BOSPart* part in listPartsResponse.parts) {
        NSLog(@"part etag %@", part.eTag);
    }
    }