Amazon S3上传、分段上传、重启续传介绍(Android)

Amazon Simple Storage Service (Amazon S3) 是一种对象存储服务,提供行业领先的可扩展性、数据可用性、安全性和性能。

Amazon S3 提供了一个简单 Web 服务接口,可用于随时在 Web 上的任何位置存储和检索任何数量的数据。此服务让所有开发人员都能访问同一个具备高扩展性、可靠性、安全性和快速价廉的数据存储基础设施。

本文分别介绍一次上传整个文件、将大文件分为几段上传以及重启后续传文件的内容。

1.上传文件

当上传的文件不是太大时,可以采用一次上传整个文件的方式上传,即使失败了,重新开始上传也不会太浪费时间。

先上代码:

String fileName = "filePath/fileName.png";
Region region = Region.getRegion(regionStr);
AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
AmazonS3 s3 = new AmazonS3Client(credentials, region);
try {
    PutObjectRequest request = new PutObjectRequest(bucketName, key, new File(fileName));
    ObjectMetadata metadata = new ObjectMetadata();
    metadata.setContentType("plain/text");
    metadata.addUserMetadata("x-amz-meta-title", "someTitle");
    request.setMetadata(metadata);
    PutObjectResult result = s3.putObject(request);
    Log.i("S3upload", "result: " + result);
}catch (Exception e){
    Log.i("S3upload", "S3 error :" + e);
}

使用S3上传首先要有region(AWS 区域),accessKey,secretKey,bucketName(文件将要上载到的现有存储桶的名称),这些数据都是AWS会提供。

代码中PutObjectRequest request = new PutObjectRequest(bucketName, key, new File(fileName));

这一句中的参数key为文件所要在存储桶中显示上传的文件路径以及文件名(举例:folder1/folder2/fileName.txt)

最后这个result,如果是整个文件上传,感觉并没有什么用,因为如果上传失败,会抛出错误,成功result里面也没有成功失败的参数,所以感觉没有用到这个result。

2.分段上传

上传文件时,有时候需要上传大文件,但是当网络不好、或者断网时,容易导致之前上传的内容失败,功亏一篑。这时就可以使用分段上传。重启续传是在分段上传的基础上完成的。首先先介绍分段上传。

分段上传就是将文件分为几部分分开上传,哪一部分上传失败就重传这部分内容,最后所有部分上传完成,再合在一起。

也是先上代码:

File file = new File(filePath);
long contentLength = file.length();
long partSize = 5 * 1024 * 1024;
long filePosition = 0;
int part = 1;

AWSCredentials credentials = new BasicSessionCredentials(mAccessKey, mSecretKey, mSessionToken);
Region region = Region.getRegion(mRegion);
AmazonS3 s3 = new AmazonS3Client(credentials, region);

InitiateMultipartUploadRequest initRequest = new InitiateMultipartUploadRequest(mBucketName, key);
InitiateMultipartUploadResult initResponse = s3.initiateMultipartUpload(initRequest);

for (; filePosition < contentLength; ) {
    if (filePosition > contentLength || filePosition == contentLength) {
        break;
    }
    partSize = Math.min(partSize, (contentLength - filePosition));
    Log.i(TAG, "[S3UploadFile][S3Part]part upload : " + partSize);

    UploadPartRequest uploadRequest = new UploadPartRequest().withBucketName(mBucketName)
            .withKey(key).withUploadId(initResponse.getUploadId()).withPartNumber(part)
            .withFileOffset(filePosition).withFile(file).withPartSize(partSize);

    uploadRequest.setGeneralProgressListener(new ProgressListener() {
        @Override
        public void progressChanged(ProgressEvent progressEvent) {
            if (progressEvent.getEventCode() == ProgressEvent.PART_COMPLETED_EVENT_CODE) {
            }
            if (progressEvent.getEventCode() == ProgressEvent.PART_FAILED_EVENT_CODE) {
            }
        }
    });

    try {
        UploadPartResult uploadResult = s3.uploadPart(uploadRequest);
        mPartETags.add(uploadResult.getPartETag());

        part ++;
        filePosition += partSize;

        Log.i(TAG, "[S3UploadFile][S3Part]result ETag: " + uploadResult.getETag()
                + ", PartNumber:" + uploadResult.getPartNumber());
    } catch (Exception e) {
        Log.i(TAG, "[S3UploadFile][S3Part]S3 part error :" + e);
    }
}

CompleteMultipartUploadRequest compRequest = new CompleteMultipartUploadRequest(mBucketName, key,
        initResponse.getUploadId(), mPartETags);
s3.completeMultipartUpload(compRequest);

分段上传分为几步:

1.初始化AmazonS3,并传入InitiateMultipartUploadRequest对象,获得InitiateMultipartUploadResult对象。

2.开始循环上传每个分段,首先计算出当前上传分段的大小(partSize),然后初始化UploadPartRequest,最后上传分段(s3.uploadPart)。

3.循环结束后,根据每个分段上传完成后的ETag,合并所有分段为一个文件(s3.completeMultipartUpload)

需要注意的是,分段上传中文件的每个分段(不包括最后一个分段)最小为5M(也就是代码中partSize),如果partSize小于5M,每个分段上传都可以成功,但是到最后合并每个分段时(completeMultipartUpload),会报错,会提示你分段的大小不合格。

最开始的时候初始化,使用的参数与整个文件直接上传的参数差不多。

然后开始循环上传,根据partSize与剩余没有上传的部分进行比较,得出这一分段需要上传的大小。将这一分段的信息设置到UploadPartRequest,设置的信息分别为bucketName(文件将要上载到的现有存储桶的名称)、key(在存储桶中显示上传的文件路径以及文件名)、uploadId(文件上传的id)、partNumber(这个分段是第几段)、fileOffset(从文件的哪个字节开始上传)、file、partSize(这个分段的大小),设置这些信息后,就可以开始上传分段了。

如果您想要知道这个分段上传是否成功,可以注册监听(setGeneralProgressListener),根据返回的eventCode可以知道是否上传成功,ProgressEvent.PART_COMPLETED_EVENT_CODE为上传成功,这里可以做成上传成功后,再上传后面的部分,ProgressEvent.PART_FAILED_EVENT_CODE为上传失败,这里可以做成上传失败,part与filePosition就不加,再继续上传失败的部分。(注意UploadPartResult获取会比ProgressListener的成功失败快一些,所以这里需要一下特殊的判断条件来卡住后面的上传)

UploadPartRequest设置完毕后,使用s3的uploadPart方法将Request上传,得到UploadPartResult,这里要从result里面获取ETag信息出来,方便后续合并分段时使用。

最后所有的分段上传完毕,初始化CompleteMultipartUploadRequest对象,这里需要uploadId,以及每个分段上传的ETag,使用s3.completeMultipartUpload合并分段。

3.重启续传

重启续传需要在上一节分段上传的基础上完成,在每完成一段的上传时,将上传的信息保存到本地存储,重启后,读取文件中的信息,续传。

想要重启后,继续上传之前没有上传完的文件,首先要保证你上传使用信息一致,才能继续传,使用信息不一致,可能会重新开始另一个上传。

来一步步看上传需要什么信息:

AWSCredentials credentials = new BasicSessionCredentials(mAccessKey, mSecretKey, mSessionToken);
Region region = Region.getRegion(mRegion);
AmazonS3 s3 = new AmazonS3Client(credentials, region);

InitiateMultipartUploadRequest initRequest = new InitiateMultipartUploadRequest(mBucketName, key);
InitiateMultipartUploadResult initResponse = s3.initiateMultipartUpload(initRequest);

初始化各种使用的类时,需要region,accessKey,secretKey,bucketName,key,sessionToken,其中sessionToken因为有过期的可能,可以与重启前不一致,其他信息保持一致

UploadPartRequest uploadRequest = new UploadPartRequest().withBucketName(mBucketName)
        .withKey(key).withUploadId(initResponse.getUploadId()).withPartNumber(part)
        .withFileOffset(filePosition).withFile(file).withPartSize(partSize);

构建UploadPartRequest的时候,需要bucketName,key,uploadId,partNumber,fileOffset,file,partSize,为了能够续传,必须要保持bucketName,key,uploadId一致,partNumber以及fileOffset要能够接得上之前已经上传的分段。

CompleteMultipartUploadRequest compRequest = new CompleteMultipartUploadRequest(mBucketName, key,
        initResponse.getUploadId(), mPartETags);
s3.completeMultipartUpload(compRequest);

最后从分段上传最后合并每段所需要的参数中,可以看到需要bucketName、key、uploadId以及ETags。

所以想要重启后续传之前的文件以及能够将重启前上传的分段合进去,就需要保存region,accessKey,secretKey,bucketName,key,uploadId,partNumber,fileOffset,ETags这些数据

(1) region,accessKey,secretKey,bucketName,key可以在一开始的时候就保存下来

(2) uploadId需要在构建InitiateMultipartUploadResult后,取出来保存。这里需要注意一下,新开的上传的uploadId是从InitiateMultipartUploadResult中取出来的,所以在构建了InitiateMultipartUploadResult类后,可以使用setUploadId将其中的uploadId修改了

InitiateMultipartUploadResult initResponse = s3.initiateMultipartUpload(initRequest);
initResponse.setUploadId(mUploadId);

(3) partNumber,fileOffset,ETags这些数据需要在确定上传成功后,将其保存起来,所以这时就必须要setGeneralProgressListener来监听分段是否上传成功,成功则保存信息,失败则继续传这一分段

贴一段例子:

public void upload(AmazonS3 s3, String filePath, String key, int part, long filePosition){
    ​File file = new File(filePath);
    long contentLength = file.length();
    long partSize = 5 * 1024 * 1024;

    InitiateMultipartUploadRequest initRequest = new InitiateMultipartUploadRequest(mBucketName, key);
    InitiateMultipartUploadResult initResponse = s3.initiateMultipartUpload(initRequest);

    //查看是否有uploadId,表示是续传
    if (mUploadId.length() > 0) {
        initResponse.setUploadId(mUploadId);
    } else {
        mUploadId = initResponse.getUploadId();
        //保存uplodId
        ...
    }

    for (; filePosition < contentLength; ) {
        if (!mIsPartComplete) {
            continue;
        }
        mIsPartComplete = false;
        if (mIsNeedAdd) {
            part++;
            filePosition += partSize;
            mIsNeedAdd = false;
            //保存partNumber,fileOffset,ETags
            ...
        }

        if (filePosition > contentLength || filePosition == contentLength) {
            break;
        }
        partSize = Math.min(partSize, (contentLength - filePosition));

        UploadPartRequest uploadRequest = new UploadPartRequest().withBucketName(mBucketName)
                .withKey(key).withUploadId(initResponse.getUploadId()).withPartNumber(part)
                .withFileOffset(filePosition).withFile(file).withPartSize(partSize);

        uploadRequest.setGeneralProgressListener(new ProgressListener() {
            @Override
            public void progressChanged(ProgressEvent progressEvent) {
                if (progressEvent.getEventCode() == ProgressEvent.PART_COMPLETED_EVENT_CODE) {
                    //成功上传,设置上传完成且传下一段的标志
                    mIsNeedAdd = true;
                    mIsPartComplete = true;
                }
                if (progressEvent.getEventCode() == ProgressEvent.PART_FAILED_EVENT_CODE) {
                    //失败上传,设置上传完成但是失败
                    mIsPartComplete = true;
                }
            }
        });

        // Upload the part and add the response's ETag to our list.
        try {
            UploadPartResult uploadResult = s3.uploadPart(uploadRequest);
            mPartETags.add(uploadResult.getPartETag());
            Log.i(TAG, "[S3UploadFile][S3Part]result ETag: " + uploadResult.getETag()
                    + ", PartNumber:" + uploadResult.getPartNumber());
        } catch (Exception e) {
        }
    }

    CompleteMultipartUploadRequest compRequest = new CompleteMultipartUploadRequest(mBucketName, key,
            initResponse.getUploadId(), mPartETags);
    s3.completeMultipartUpload(compRequest);
}

很多数据都是在程序运行时,从保存文件中取出来的,这里就不介绍怎么设置的这些数据了。

另,在使用S3上传时,并且已经在执行uploadPart时,一定要保证你上传的文件存在,如果在上传时,上传文件不存在了,那么会导致崩溃,这是try catch抓不住的错误,因为S3没有抛出,所以如果你的文件比如在SD卡上,SD卡又可能被人为的取掉,那么可以在上传前,将文件复制到人为不可以动到(除非root)文件夹下。

你可能感兴趣的:(android)