OSS提供的分片上传(Multipart Upload)功能,将要上传的较大的文件(Object)分成多个数据块(OSS中称之为Part)来分别上传,上传完成后再调用CompleteMultipartUpload接口将这些Part组合成一个Object来达到断点续传的效果。

分片上传流程

分片上传的基本流程如下:

  1. 将要上传的文件按照一定的大小分片。
  2. 初始化一个分片上传任务(InitiateMultipartUpload)。
  3. 逐个或并行上传分片(UploadPart)。
  4. 完成上传(CompleteMultipartUpload)。
fig_uploadpaet

该过程需注意以下几点:

  • 除了最后一块Part,其他Part的大小不能小于100 KB,否则会导致调用CompleteMultipartUpload接口失败。
  • 要上传的文件切分成Part之后,文件顺序是通过上传过程中指定的partNumber来确定的,实际执行中并没有顺序要求,因此可以实现并发上传。

    具体的并发个数并不是越多速度越快,要结合用户自身的网络情况和设备负载综合考虑。网络情况较好时,建议增大分片大小。反之,减小分片大小。

  • 默认情况下,已经上传但还没有调用CompleteMultipartUpload的Part是不会被自动回收的,因此如果要终止上传并删除占用的空间请调用AbortMultipartUpload。如果需要自动回收上传的Part,请参见生命周期管理

初始化分片上传

以下代码用于初始化分片上传。

__block NSString * uploadId = nil;
__block NSMutableArray * partInfos = [NSMutableArray new];
NSString * uploadToBucket = @"<bucketName>";
// objectKey等同于objectName,表示上传文件到OSS时需要指定包含文件后缀在内的完整路径,例如abc/efg/123.jpg
NSString * uploadObjectkey = @"<objectKey>";
// OSSInitMultipartUploadRequest用于指定上传文件的名称以及上传文件所属的存储空间的名称。
OSSInitMultipartUploadRequest * init = [OSSInitMultipartUploadRequest new];
init.bucketName = uploadToBucket;
init.objectKey = uploadObjectkey;
// init.contentType = @"application/octet-stream";
// multipartUploadInit返回的结果中包含UploadId,UploadId是分片上传的唯一标识。
OSSTask * initTask = [client multipartUploadInit:init];
[initTask waitUntilFinished];
if (!initTask.error) {
    OSSInitMultipartUploadResult * result = initTask.result;
    uploadId = result.uploadId;
} else {
    NSLog(@"multipart upload failed, error: %@", initTask.error);
    return;
}

上传分片

以下代码用于上传分片。

// 指定要上传的文件。
NSString * filePath = [docDir stringByAppendingPathComponent:@"***"];
// 获取文件大小。
uint64_t fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil] fileSize];
// 设置分片上传数量。
int chuckCount = *;
// 设置分片大小。
uint64_t offset = fileSize/chuckCount;
for (int i = 1; i <= chuckCount; i++) {
    OSSUploadPartRequest * uploadPart = [OSSUploadPartRequest new];
    uploadPart.bucketName = uploadToBucket;
    uploadPart.objectkey = uploadObjectkey;
    uploadPart.uploadId = uploadId;
    uploadPart.partNumber = i; // part number start from 1

    NSFileHandle* readHandle = [NSFileHandle fileHandleForReadingAtPath:filePath];
    [readHandle seekToFileOffset:offset * (i -1)];

    NSData* data = [readHandle readDataOfLength:offset];
    uploadPart.uploadPartData = data;

    OSSTask * uploadPartTask = [client uploadPart:uploadPart];

    [uploadPartTask waitUntilFinished];

    if (!uploadPartTask.error) {
        OSSUploadPartResult * result = uploadPartTask.result;
        uint64_t fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:uploadPart.uploadPartFileURL.absoluteString error:nil] fileSize];
        [partInfos addObject:[OSSPartInfo partInfoWithPartNum:i eTag:result.eTag size:fileSize]];
    } else {
        NSLog(@"upload part error: %@", uploadPartTask.error);
        return;
    }
}

上述代码调用uploadPart来上传每一个分片。

  • 每一个分片上传请求需指定UploadId和PartNum。
  • uploadPart接口并不会立即校验上传。只有完成分片上传时才会校验Part的大小。
  • Part编号范围是1~10000。如果超出该范围,OSS将返回InvalidArgument的错误码。
  • 每次上传Part时都要把流定位到此次上传片开头所对应的位置。
  • 每次上传Part之后,OSS的返回结果中会包含每个分片的ETag值,ETag值为Part数据MD5值,您需要将ETag值和Part编号组合成PartETag并保存,用于后续完成分片上传。

完成分片上传

以下代码中的partInfos为分片上传过程中保存的PartETag列表,OSS收到您提交的Part列表后,会逐一验证每个数据Part的有效性。当所有的数据Part验证通过后,OSS会将这些Part组合成一个完整的文件。

以下代码用于完成分片上传。

OSSCompleteMultipartUploadRequest * complete = [OSSCompleteMultipartUploadRequest new];
complete.bucketName = uploadToBucket;
complete.objectKey = uploadObjectkey;
complete.uploadId = uploadId;
complete.partInfos = partInfos;

OSSTask * completeTask = [client completeMultipartUpload:complete];

[[completeTask continueWithBlock:^id(OSSTask *task) {
    if (!task.error) {
        OSSCompleteMultipartUploadResult * result = task.result;
        // ...
    } else {
        // ...
    }
    return nil;
}] waitUntilFinished];

完成分片上传请求时可以设置servercallback函数,请求完成后会向指定的Server Address发送回调请求。您可以通过返回结果中的result.serverReturnJsonString查看servercallback结果。

OSSCompleteMultipartUploadRequest * complete = [OSSCompleteMultipartUploadRequest new];
complete.bucketName = @"<bucketName>";
complete.objectKey = @"<objectKey>";
complete.uploadId = uploadId;
complete.partInfos = partInfos;
complete.callbackParam = @{
                          @"callbackUrl": @"<server address>",
                          @"callbackBody": @"<test>"
                          };
complete.callbackVar = @{
                        @"var1": @"value1",
                        @"var2": @"value2"
                        };
OSSTask * completeTask = [client completeMultipartUpload:complete];
[[completeTask continueWithBlock:^id(OSSTask *task) {
    if (!task.error) {
        OSSCompleteMultipartUploadResult * result = task.result;
        NSLog(@"server call back return : %@", result.serverReturnJsonString);
    } else {
        // ...
    }
    return nil;
}] waitUntilFinished];

如果要校验分片上传到OSS的文件和本地文件是否一致,可以在上传文件时携带文件的Content-MD5值,OSS服务器会帮助您进行Content-MD5校验。只有在OSS服务器接收到的文件MD5值和Content-MD5一致时才可以上传成功,从而保证上传文件的数据完整性。

以下代码用于MD5校验设置。

OSSUploadPartRequest * uploadPart = [OSSUploadPartRequest new];
uploadPart.bucketName = TEST_BUCKET;
uploadPart.uploadId = uploadId;
....
uploadPart.contentMd5 = [OSSUtil fileMD5String:filepath];

列举分片

调用listParts方法获取某个上传事件所有已上传的分片。

OSSListPartsRequest * listParts = [OSSListPartsRequest new];
listParts.bucketName = @"<bucketName>";
listParts.objectKey = @"<objectkey>";
listParts.uploadId = @"<uploadid>";

OSSTask * listPartTask = [client listParts:listParts];

[listPartTask continueWithBlock:^id(OSSTask *task) {
    if (!task.error) {
        NSLog(@"list part result success!");
        OSSListPartsResult * listPartResult = task.result;
        for (NSDictionary * partInfo in listPartResult.parts) {
            NSLog(@"each part: %@", partInfo);
        }
    } else {
        NSLog(@"list part result error: %@", task.error);
    }
    return nil;
}];
注意 默认情况下,如果存储空间中的分片上传事件的数量大于1000,则OSS仅返回1000个Multipart Upload信息,且返回结果中IsTruncated的值为false,并返回NextPartNumberMarker作为下次读取的起点。

取消分片上传

以下代码用于取消了对应UploadId的分片上传请求。

OSSAbortMultipartUploadRequest * abort = [OSSAbortMultipartUploadRequest new];
abort.bucketName = @"<bucketName>";
abort.objectKey = @"<objectKey>";
abort.uploadId = uploadId;

OSSTask * abortTask = [client abortMultipartUpload:abort];

[abortTask waitUntilFinished];

if (!abortTask.error) {
    OSSAbortMultipartUploadResult * result = abortTask.result;
    uploadId = result.uploadId;
} else {
    NSLog(@"multipart upload failed, error: %@", abortTask.error);
    return;
}