/** * Copyright 2015-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ package com.amazonaws.mobileconnectors.s3.transferutility; import android.util.Log; import com.amazonaws.AmazonClientException; import com.amazonaws.mobileconnectors.s3.transferutility.TransferService.NetworkInfoReceiver; import com.amazonaws.retry.RetryUtils; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.CompleteMultipartUploadRequest; import com.amazonaws.services.s3.model.InitiateMultipartUploadRequest; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PartETag; import com.amazonaws.services.s3.model.PutObjectRequest; import com.amazonaws.services.s3.model.UploadPartRequest; import com.amazonaws.services.s3.util.Mimetypes; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; class UploadTask implements Callable<Boolean> { private final static String TAG = "UploadTask"; private final AmazonS3 s3; private final TransferRecord upload; private final TransferDBUtil dbUtil; private final TransferStatusUpdater updater; private final NetworkInfoReceiver networkInfo; public UploadTask(TransferRecord uploadInfo, AmazonS3 s3, TransferDBUtil dbUtil, TransferStatusUpdater updater, NetworkInfoReceiver networkInfo) { this.upload = uploadInfo; this.s3 = s3; this.dbUtil = dbUtil; this.updater = updater; this.networkInfo = networkInfo; } /* * Runs upload task and returns whether successfully uploaded. */ @Override public Boolean call() throws Exception { if (!networkInfo.isNetworkConnected()) { updater.updateState(upload.id, TransferState.WAITING_FOR_NETWORK); return false; } updater.updateState(upload.id, TransferState.IN_PROGRESS); if (upload.isMultipart == 1 && upload.partNumber == 0) { /* * If part number = 0, this multipart upload record is not a real * part upload task, it's a summary for all the parts with part * numbers from 1 to N. We now need to create records for all its * upload parts. */ return uploadMultipartAndWaitForCompletion(); } else if (upload.isMultipart == 0) { /* * uploads in one Chunk, doesn't support pause and resume. */ return uploadSinglePartAndWaitForCompletion(); } return false; } private Boolean uploadMultipartAndWaitForCompletion() throws ExecutionException { /* * For a new multipart upload, upload.mMultipartId should be null. If * it's a resumed upload, upload.mMultipartId would not be null. */ long bytesAlreadyTransferrd = 0; if (upload.multipartId == null || upload.multipartId.isEmpty()) { PutObjectRequest putObjectRequest = createPutObjectRequest(upload); TransferUtility.appendMultipartTransferServiceUserAgentString(putObjectRequest); try { upload.multipartId = initiateMultipartUpload(putObjectRequest); } catch (AmazonClientException ace) { Log.e(TAG, "Error initiating multipart upload: " + upload.id + " due to " + ace.getMessage()); updater.throwError(upload.id, ace); updater.updateState(upload.id, TransferState.FAILED); return false; } dbUtil.updateMultipartId(upload.id, upload.multipartId); } else { /* * For a resumed upload, we should calculate the bytes already * transferred. */ bytesAlreadyTransferrd = dbUtil.queryBytesTransferredByMainUploadId(upload.id); if (bytesAlreadyTransferrd > 0) { Log.d(TAG, String.format("Resume transfer %d from %d bytes", upload.id, bytesAlreadyTransferrd)); } } updater.updateProgress(upload.id, bytesAlreadyTransferrd, upload.bytesTotal); List<UploadPartRequest> requestList = dbUtil.getNonCompletedPartRequestsFromDB(upload.id, upload.multipartId); Log.d(TAG, "multipart upload " + upload.id + " in " + requestList.size() + " parts."); ArrayList<Future<Boolean>> futures = new ArrayList<Future<Boolean>>(); for (UploadPartRequest request : requestList) { TransferUtility.appendMultipartTransferServiceUserAgentString(request); request.setGeneralProgressListener(updater.newProgressListener(upload.id)); futures.add(TransferThreadPool.submitTask(new UploadPartTask(request, s3, dbUtil))); } try { boolean isSuccess = true; /* * Future.get() will block the current thread until the method * returns. */ for (Future<Boolean> f : futures) { // UploadPartTask returns false when it's interrupted by user // and the state is set by caller boolean b = f.get(); isSuccess &= b; } if (!isSuccess) { return false; } } catch (InterruptedException e) { /* * Future.get() will catch InterruptedException, but it's not a * failure, it may be caused by a pause operation from applications. */ for (Future<?> f : futures) { f.cancel(true); } // abort by user Log.d(TAG, "Transfer " + upload.id + " is interrupted by user"); return false; } catch (ExecutionException ee) { // handle pause, cancel, etc if (ee.getCause() != null && ee.getCause() instanceof Exception) { Exception e = (Exception) ee.getCause(); if (RetryUtils.isInterrupted(e)) { /* * thread is interrupted by user. don't update the state as * it's set by caller who interrupted */ Log.d(TAG, "Transfer " + upload.id + " is interrupted by user"); return false; } else if (e.getCause() != null && e.getCause() instanceof IOException && !networkInfo.isNetworkConnected()) { Log.d(TAG, "Transfer " + upload.id + " waits for network"); updater.updateState(upload.id, TransferState.WAITING_FOR_NETWORK); } updater.throwError(upload.id, e); } updater.updateState(upload.id, TransferState.FAILED); return false; } try { completeMultiPartUpload(upload.id, upload.bucketName, upload.key, upload.multipartId); updater.updateProgress(upload.id, upload.bytesTotal, upload.bytesTotal); updater.updateState(upload.id, TransferState.COMPLETED); return true; } catch (AmazonClientException ace) { Log.e(TAG, "Failed to complete multipart: " + upload.id + " due to " + ace.getMessage()); updater.throwError(upload.id, ace); updater.updateState(upload.id, TransferState.FAILED); return false; } } private Boolean uploadSinglePartAndWaitForCompletion() { PutObjectRequest putObjectRequest = createPutObjectRequest(upload); long length = putObjectRequest.getFile().length(); TransferUtility.appendTransferServiceUserAgentString(putObjectRequest); updater.updateProgress(upload.id, 0, length); putObjectRequest.setGeneralProgressListener(updater.newProgressListener(upload.id)); try { s3.putObject(putObjectRequest); updater.updateProgress(upload.id, length, length); updater.updateState(upload.id, TransferState.COMPLETED); return true; } catch (Exception e) { if (RetryUtils.isInterrupted(e)) { /* * thread is interrupted by user. don't update the state as it's * set by caller who interrupted */ Log.d(TAG, "Transfer " + upload.id + " is interrupted by user"); return false; } else if (e.getCause() != null && e.getCause() instanceof IOException && !networkInfo.isNetworkConnected()) { Log.d(TAG, "Transfer " + upload.id + " waits for network"); updater.updateState(upload.id, TransferState.WAITING_FOR_NETWORK); } // all other exceptions Log.e(TAG, "Failed to upload: " + upload.id + " due to " + e.getMessage()); updater.throwError(upload.id, e); updater.updateState(upload.id, TransferState.FAILED); return false; } } private void completeMultiPartUpload(int mainUploadId, String bucket, String key, String multipartId) throws AmazonClientException { List<PartETag> partETags = dbUtil.queryPartETagsOfUpload(mainUploadId); CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest(bucket, key, multipartId, partETags); TransferUtility.appendMultipartTransferServiceUserAgentString(completeRequest); s3.completeMultipartUpload(completeRequest); } /** * Creates a multipart upload id for the upload request. * * @param putObjectRequest An PutObjectRequest object for the whole upload * @return A multipart upload id */ private String initiateMultipartUpload(PutObjectRequest putObjectRequest) throws AmazonClientException { InitiateMultipartUploadRequest initiateMultipartUploadRequest = null; initiateMultipartUploadRequest = new InitiateMultipartUploadRequest( putObjectRequest.getBucketName(), putObjectRequest.getKey()) .withCannedACL(putObjectRequest.getCannedAcl()) .withObjectMetadata(putObjectRequest.getMetadata()); TransferUtility .appendMultipartTransferServiceUserAgentString(initiateMultipartUploadRequest); String uploadId = s3.initiateMultipartUpload(initiateMultipartUploadRequest).getUploadId(); return uploadId; } /** * Creates a PutObjectRequest from the data in the TransferRecord * * @param por The request to fill * @param upload The data for the Object Metadata * @return Returns a PutObjectRequest with filled in metadata and parameters */ private PutObjectRequest createPutObjectRequest(TransferRecord upload) { File file = new File(upload.file); PutObjectRequest putObjectRequest = new PutObjectRequest(upload.bucketName, upload.key, file); ObjectMetadata om = new ObjectMetadata(); om.setContentLength(file.length()); if (upload.headerCacheControl != null) { om.setCacheControl(upload.headerCacheControl); } if (upload.headerContentDisposition != null) { om.setContentDisposition(upload.headerContentDisposition); } if (upload.headerContentEncoding != null) { om.setContentEncoding(upload.headerContentEncoding); } if (upload.headerContentType != null) { om.setContentType(upload.headerContentType); } else { om.setContentType(Mimetypes.getInstance().getMimetype(file)); } if (upload.expirationTimeRuleId != null) { om.setExpirationTimeRuleId(upload.expirationTimeRuleId); } if (upload.httpExpires != null) { om.setHttpExpiresDate(new Date(Long.valueOf(upload.httpExpires))); } if (upload.sseAlgorithm != null) { om.setSSEAlgorithm(upload.sseAlgorithm); } if (upload.userMetadata != null) { om.setUserMetadata(upload.userMetadata); } if (upload.md5 != null) { om.setContentMD5(upload.md5); } putObjectRequest.setMetadata(om); putObjectRequest.setCannedAcl(getCannedAclFromString(upload.cannedAcl)); return putObjectRequest; } private static final Map<String, CannedAccessControlList> cannedAclMap; static { cannedAclMap = new HashMap<String, CannedAccessControlList>(); for (CannedAccessControlList cannedAcl : CannedAccessControlList.values()) { cannedAclMap.put(cannedAcl.toString(), cannedAcl); } } private static CannedAccessControlList getCannedAclFromString(String cannedAcl) { return cannedAcl == null ? null : cannedAclMap.get(cannedAcl); } }