Object的分块上传
更新时间:2022-10-21
除了通过putObject()方法上传文件到BOS以外,BOS还提供了另外一种上传模式:分块上传(Multipart Upload)。用户可以在如下的应用场景内(但不仅限于此),使用分块上传模式,如:
- 需要支持断点上传。
- 上传超过5GB大小的文件。
- 网络条件较差,和BOS的服务器之间的连接经常断开。
- 需要流式地上传文件。
- 上传文件之前,无法确定上传文件的大小。
分块完成Multipart Upload
假设有一个文件,本地路径为/path/to/file.zip
,由于文件比较大,使用分块上传其传输到BOS中。基本流程:
- 初始化Multipart Upload。
- 上传分块。
- 完成分块上传。
初始化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内未完成的分块上传事件。
基本流程
- 创建BOSListMultipartUploadsRequest类的实例,传入
<BucketName>
参数。 - 创建BOSClient类的实例,执行BOSClient listMultipartUploads方法。
- 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
方法获取某个上传事件中所有已上传的块。
基本流程
- 创建BOSListPartsRequest类的实例,传入
<BucketName>
,<ObjectKey>
,<UploadId>
参数。 - 创建BOSClient类的实例,执行BOSClient listParts方法。
- 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);
}
}