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

分片上传流程

分片上传(Multipart Upload)分为以下三个步骤:

  1. 初始化一个分片上传事件。

    调用oss.initMultipartUpload方法返回OSS创建的全局唯一的uploadId。

  2. 上传分片。

    调用oss.uploadPart方法上传分片数据。

    说明
    • 对于同一个uploadId,分片号(PartNumber)标识了该分片在整个文件内的相对位置。如果使用同一个分片号上传了新的数据,则OSS上该分片已有的数据将会被覆盖。
    • OSS将收到的分片数据的MD5值放在ETag头内返回给用户。
    • OSS计算上传数据的MD5值,并与SDK计算的MD5值比较,如果不一致则返回InvalidDigest错误码。
  3. 完成分片上传。

    所有分片上传完成后,调用oss.CompleteMultipartUpload方法将所有Part合并成完整的文件。

分片上传完整示例

以下通过一个完整的示例对分片上传的流程进行逐步解析:

// 填写Bucket名称,例如examplebucket。
String bucketName = "examplebucket";
// 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
String objectName = "exampledir/exampleobject.txt";
// 填写本地文件完整路径,例如/storage/emulated/0/oss/examplefile.txt。
String localFilepath = "/storage/emulated/0/oss/examplefile.txt";

// 初始化分片上传。
InitiateMultipartUploadRequest init = new InitiateMultipartUploadRequest(bucketName, objectName);
InitiateMultipartUploadResult initResult = oss.initMultipartUpload(init);
// 返回uploadId,它是分片上传事件的唯一标识。您可以根据该uploadId发起相关操作,例如取消分片上传、查询分片上传等。
String uploadId = initResult.getUploadId();

// 设置单个Part的大小,单位为字节,取值范围为100 KB~5 GB。
int partCount = 100 * 1024;
// 分片上传。
List<PartETag> partETags = new ArrayList<>();
for (int i = 1; i < 5; i++) {
    byte[] data = new byte[partCount];

    RandomAccessFile raf = new RandomAccessFile(localFilepath, "r");
    long skip = (i-1) * partCount;
    raf.seek(skip);
    raf.readFully(data, 0, partCount);

    UploadPartRequest uploadPart = new UploadPartRequest();
    uploadPart.setBucketName(bucketName);
    uploadPart.setObjectKey(objectKey);
    uploadPart.setUploadId(uploadId);
    // 设置分片号,从1开始标识。每一个上传的Part都有一个分片号,取值范围是1~10000。
    uploadPart.setPartNumber(i); 
    uploadPart.setPartContent(data);
    try {
        UploadPartResult result = oss.uploadPart(uploadPart);
        PartETag partETag = new PartETag(uploadPart.getPartNumber(), result.getETag());
        partETags.add(partETag);
    } catch (ServiceException serviceException) {
        OSSLog.logError(serviceException.getErrorCode());
    }
}
Collections.sort(partETags, new Comparator<PartETag>() {
    @Override
    public int compare(PartETag lhs, PartETag rhs) {
        if (lhs.getPartNumber() < rhs.getPartNumber()) {
            return -1;
        } else if (lhs.getPartNumber() > rhs.getPartNumber()) {
            return 1;
        } else {
            return 0;
        }
    }
});

// 完成分片上传。
CompleteMultipartUploadRequest complete = new CompleteMultipartUploadRequest(bucketName, objectName, uploadId, partETags);

// 上传回调。完成分片上传请求时可以设置CALLBACK_SERVER参数,请求完成后会向指定的Server Address发送回调请求。可通过返回结果的completeResult.getServerCallbackReturnBody()查看servercallback结果。
complete.setCallbackParam(new HashMap<String, String>() {
    {
        put("callbackUrl", CALLBACK_SERVER); //修改为您的服务器地址。
        put("callbackBody", "test");
    }
});
CompleteMultipartUploadResult completeResult = oss.completeMultipartUpload(complete);
OSSLog.logError("-------------- serverCallback: " + completeResult.getServerCallbackReturnBody());

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

  • 每一个分片上传请求均需指定uploadId和PartNumber。PartNumber的范围是1~10000。如果超出该范围,OSS将返回InvalidArgument的错误码。
  • uploadPart要求除最后一个Part外,其他的Part大小都要大于100 KB。uploadPart仅在完成分片上传时校验Part的大小。
  • 每次上传Part时都要将流定位至此次上传片开头所对应的位置。
  • 每次上传Part之后,OSS的返回结果会包含一个Part的ETag值,ETag值为Part数据的MD5值,您需要将ETag值和块编号组合成PartEtag并保存,用于后续完成分片上传。

本地文件分片上传

您可以通过同步方式或者异步方式分片上传本地文件到OSS。

说明 分片上传完整示例是按照分片上传流程逐步实现的完整代码,本地文件分片上传的代码是将分片上传完整示例中的代码进行了封装,您只需要使用MultipartUploadRequest即可实现分片上传。
  • 调用同步接口分片上传本地文件

    以下代码用于以同步方式分片上传examplefile.txt文件到目标存储空间examplebucket中exampledir目录下的exampleobject.txt文件。

    // 填写Bucket名称,例如examplebucket。
    String bucketName = "examplebucket";
    // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
    String objectName = "exampledir/exampleobject.txt";
    // 填写本地文件完整路径,例如/storage/emulated/0/oss/examplefile.txt。
    String localFilepath = "/storage/emulated/0/oss/examplefile.txt";
    
    ObjectMetadata meta = new ObjectMetadata();
    // 设置文件元信息等。
    meta.setHeader("x-oss-object-acl", "public-read-write");
    MultipartUploadRequest rq = new MultipartUploadRequest(bucketName, objectName, localFilepath, meta);
    // 设置PartSize。PartSize默认值为256 KB,最小值为100 KB。
    rq.setPartSize(1024 * 1024);
    rq.setProgressCallback(new OSSProgressCallback<MultipartUploadRequest>() {
        @Override
        public void onProgress(MultipartUploadRequest request, long currentSize, long totalSize) {
            OSSLog.logDebug("[testMultipartUpload] - " + currentSize + " " + totalSize, false);
        }
    });
    
    CompleteMultipartUploadResult result = oss.multipartUpload(rq);

    对于Android10及之后版本的分区存储,您可以使用文件的Uri上传文件到OSS。

    // 填写Bucket名称,例如examplebucket。
    String bucketName = "examplebucket";
    // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
    String objectName = "exampledir/exampleobject.txt";
    
    ObjectMetadata meta = new ObjectMetadata();
    // 设置文件元信息等。
    meta.setHeader("x-oss-object-acl", "public-read-write");
    MultipartUploadRequest rq = new MultipartUploadRequest(bucketName, objectName, fileUri, meta);
    // 设置PartSize。PartSize默认值为256 KB,最小值为100 KB。
    rq.setPartSize(1024 * 1024);
    rq.setProgressCallback(new OSSProgressCallback<MultipartUploadRequest>() {
        @Override
        public void onProgress(MultipartUploadRequest request, long currentSize, long totalSize) {
            OSSLog.logDebug("[testMultipartUpload] - " + currentSize + " " + totalSize, false);
        }
    });
    
    CompleteMultipartUploadResult result = oss.multipartUpload(rq);
  • 调用异步接口分片上传本地文件

    以下代码用于以异步方式分片上传examplefile.txt文件到目标存储空间examplebucket中exampledir目录下的exampleobject.txt文件。

    // 填写Bucket名称,例如examplebucket。
    String bucketName = "examplebucket";
    // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
    String objectName = "exampledir/exampleobject.txt";
    // 填写本地文件完整路径,例如/storage/emulated/0/oss/examplefile.txt。
    String localFilepath = "/storage/emulated/0/oss/examplefile.txt";
    
    MultipartUploadRequest request = new MultipartUploadRequest(bucketName, objectName, localFilepath);
    
    request.setProgressCallback(new OSSProgressCallback<MultipartUploadRequest>() {
        @Override
        public void onProgress(MultipartUploadRequest request, long currentSize, long totalSize) {
            OSSLog.logDebug("[testMultipartUpload] - " + currentSize + " " + totalSize, false);
        }
    });
    
    OSSAsyncTask task = oss.asyncMultipartUpload(request, new OSSCompletedCallback<MultipartUploadRequest, CompleteMultipartUploadResult>() {
        @Override
        public void onSuccess(MultipartUploadRequest request, CompleteMultipartUploadResult result) {
            OSSLog.logInfo(result.getServerCallbackReturnBody());
        }
    
        @Override
        public void onFailure(MultipartUploadRequest request, ClientException clientException, ServiceException serviceException) {
            OSSLog.logError(serviceException.getRawMessage());
        }
    });
    
    //Thread.sleep(100);
    // 取消分片上传。
    //task.cancel();   
    
    task.waitUntilFinished();

    对于Android10及之后版本的分区存储,您可以使用文件的Uri上传文件到OSS。

    // 填写Bucket名称,例如examplebucket。
    String bucketName = "examplebucket";
    // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
    String objectName = "exampledir/exampleobject.txt";
    
    MultipartUploadRequest request = new MultipartUploadRequest(bucketName, objectName, fileUri);
    
    request.setProgressCallback(new OSSProgressCallback<MultipartUploadRequest>() {
        @Override
        public void onProgress(MultipartUploadRequest request, long currentSize, long totalSize) {
            OSSLog.logDebug("[testMultipartUpload] - " + currentSize + " " + totalSize, false);
        }
    });
    
    OSSAsyncTask task = oss.asyncMultipartUpload(request, new OSSCompletedCallback<MultipartUploadRequest, CompleteMultipartUploadResult>() {
        @Override
        public void onSuccess(MultipartUploadRequest request, CompleteMultipartUploadResult result) {
            OSSLog.logInfo(result.getServerCallbackReturnBody());
        }
    
        @Override
        public void onFailure(MultipartUploadRequest request, ClientException clientException, ServiceException serviceException) {
            OSSLog.logError(serviceException.getRawMessage());
        }
    });
    
    //Thread.sleep(100);
    // 取消分片上传。
    //task.cancel();   
    
    task.waitUntilFinished();

列举已上传分片

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

以下代码用于列举已上传分片。

// 填写Bucket名称,例如examplebucket。
String bucketName = "examplebucket";
// 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
String objectName = "exampledir/exampleobject.txt";
// 填写uploadId。
String uploadId = "0004B999EF518A1FE585B0C9360D";

// 列举分片。
ListPartsRequest listParts = new ListPartsRequest(bucketName, objectName, uploadId);
ListPartsResult result = oss.listParts(listParts);

List<PartETag> partETagList = new ArrayList<PartETag>();
for (PartSummary part : result.getParts()) {
    partETagList.add(new PartETag(part.getPartNumber(), part.getETag()));
}
注意 默认情况下,如果存储空间中的分片上传事件的数量大于1000,则OSS仅返回1000个Multipart Upload信息,且返回结果中IsTruncated的值为false,并返回NextPartNumberMarker作为下次读取的起点。

取消分片上传事件

调用oss.abortMultipartUpload方法来取消分片上传事件。当一个分片上传事件被取消后,无法再使用该uploadId进行任何操作,已上传的分片数据会被删除。

以下代码用于取消分片上传事件。

// 填写Bucket名称,例如examplebucket。
String bucketName = "examplebucket";
// 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
String objectName = "exampledir/exampleobject.txt";
// 填写uploadId。
String uploadId = "0004B999EF518A1FE585B0C9360D";

// 取消分片上传。
AbortMultipartUploadRequest abort = new AbortMultipartUploadRequest(bucketName, objectName, uploadId);
AbortMultipartUploadResult abortResult = oss.abortMultipartUpload(abort);