/** * Copyright 2015-2019 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.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.ConnectivityManager; import android.net.Uri; import com.amazonaws.AmazonWebServiceRequest; import com.amazonaws.logging.Log; import com.amazonaws.logging.LogFactory; import com.amazonaws.mobile.config.AWSConfiguration; import com.amazonaws.regions.Region; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.S3ClientOptions; import com.amazonaws.services.s3.internal.Constants; import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.util.VersionInfoUtils; import org.json.JSONObject; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import static com.amazonaws.services.s3.internal.Constants.GB; import static com.amazonaws.services.s3.internal.Constants.MAXIMUM_UPLOAD_PARTS; import static com.amazonaws.services.s3.internal.Constants.MB; /** * The transfer utility is a high-level class for applications to upload and * download files. It inserts upload and download records into the database and * starts a Service to execute the tasks in the background. Here is the usage: * *
* // Initializes TransferUtility * TransferUtility transferUtility = new TransferUtility(s3, getApplicationContext()); * // Starts a download * TransferObserver observer = transferUtility.download("bucket_name", "key", file); * observer.setTransferListener(new TransferListener() { * public void onStateChanged(int id, String newState) { * // Do something in the callback. * } * * public void onProgressChanged(int id, long bytesCurrent, long bytesTotal) { * // Do something in the callback. * } * * public void onError(int id, Exception e) { * // Do something in the callback. * } * }); ** * For pausing and resuming tasks: * *
* // Gets id of the transfer. * int id = observer.getId(); * * // Pauses the transfer. * transferUtility.pause(id); * * // Pause all the transfers. * transferUtility.pauseAllWithType(TransferType.ANY); * * // Resumes the transfer. * transferUtility.resume(id); * * // Resume all the transfers. * transferUtility.resumeAllWithType(TransferType.ANY); * ** * For canceling and deleting tasks: * *
* // Cancels the transfer. * transferUtility.cancel(id); * * // Cancel all the transfers. * transferUtility.cancelAllWithType(TransferType.ANY); * * // Deletes the transfer. * transferUtility.delete(id); **/ public class TransferUtility { private static final Log LOGGER = LogFactory.getLog(TransferUtility.class); /** * The status updater that updates the state and the progress of the transfer in * memory and persists to the database. */ private TransferStatusUpdater updater; /** * The dbUtil instance. */ private TransferDBUtil dbUtil; /** * Lock to synchronize access to the user-agent-string. */ private static final Object LOCK = new Object(); /** * An Android Networking utility that gives network specific information. */ final ConnectivityManager connManager; /** * Constants that indicate the type of the transfer operation. */ private static final String TRANSFER_ADD = "add_transfer"; private static final String TRANSFER_PAUSE = "pause_transfer"; private static final String TRANSFER_RESUME = "resume_transfer"; private static final String TRANSFER_CANCEL = "cancel_transfer"; /** * Default minimum part size for upload parts. Anything below this will use a * single upload */ static final int DEFAULT_MINIMUM_UPLOAD_PART_SIZE_IN_BYTES = 5 * MB; static final int MINIMUM_SUPPORTED_UPLOAD_PART_SIZE_IN_BYTES = 5 * MB; static final long MAXIMUM_SUPPORTED_UPLOAD_PART_SIZE_IN_BYTES = 5 * GB; private static String userAgentFromConfig = ""; private static void setUserAgentFromConfig(String userAgent) { synchronized (LOCK) { TransferUtility.userAgentFromConfig = userAgent; } } private static String getUserAgentFromConfig() { synchronized (LOCK) { if (TransferUtility.userAgentFromConfig == null || TransferUtility.userAgentFromConfig.trim().isEmpty()) { return ""; } return TransferUtility.userAgentFromConfig.trim() + "/"; } } private final AmazonS3 s3; private final String defaultBucket; private final TransferUtilityOptions transferUtilityOptions; /** * Builder class for TransferUtility */ public static class Builder { private AmazonS3 s3; private Context appContext; private String defaultBucket; private AWSConfiguration awsConfig; private TransferUtilityOptions transferUtilityOptions; protected Builder() { } /** * Sets the underlying S3 client used for transfers. * * @param s3Client The S3 client. * @return builder */ public Builder s3Client(final AmazonS3 s3Client) { this.s3 = s3Client; return this; } /** * Sets the context used. * * @param applicationContext The application context. * @return builder */ public Builder context(final Context applicationContext) { this.appContext = applicationContext.getApplicationContext(); return this; } /** * Sets the default bucket used for uploads and downloads. This allows you to * use the corresponding methods that do not require the bucket name to be * specified. * * @param bucket The bucket name. * @return builder */ public Builder defaultBucket(final String bucket) { this.defaultBucket = bucket; return this; } /** * Sets the region of the underlying S3 client and the default bucket used for * uploads and downloads. This allows you to use the corresponding methods that * do not require the bucket name to be specified. These values are retrieved * from the AWSConfiguration argument. * * Example awsconfiguration.json contents: { "S3TransferUtility": { "Default": { * "Bucket": "exampleBucket", "Region": "us-east-1" } } } * * @param awsConfiguration The configuration. * @return builder */ public Builder awsConfiguration(AWSConfiguration awsConfiguration) { this.awsConfig = awsConfiguration; return this; } /** * Sets the TransferUtilityOptions for this TransferUtility instance. Currently, * this includes the option to override the time interval to periodically resume * unfinished transfers by the TransferService and the size of the transfer * thread pool which is shared across the different transfers. * * Example: * * TransferUtilityOptions tuOptions = new TransferUtilityOptions(); * tuConfig.setTransferServiceCheckTimeInterval(5); * tuConfig.setTransferThreadPoolSize(10); * * TransferUtility tu = TransferUtility .builder() * .transferUtilityOptions(tuOptions) .build(); * * @param tuOptions The TransferUtility Options object * @return builder */ public Builder transferUtilityOptions(final TransferUtilityOptions tuOptions) { this.transferUtilityOptions = tuOptions; return this; } /** * * @return TransferUtility */ public TransferUtility build() { if (this.s3 == null) { throw new IllegalArgumentException( "AmazonS3 client is required please set using .s3Client(yourClient)"); } else if (this.appContext == null) { throw new IllegalArgumentException("Context is required please set using .context(applicationContext)"); } if (this.awsConfig != null) { try { final JSONObject tuConfig = this.awsConfig.optJsonObject("S3TransferUtility"); this.s3.setRegion(Region.getRegion(tuConfig.getString("Region"))); this.defaultBucket = tuConfig.getString("Bucket"); // Checks if awsconfiguration.json has local testing flag to dangerously connect to HTTP endpoint. // Defaults to false unless specified. final boolean canConnectToHTTPEndpoint = tuConfig.has(Constants.LOCAL_TESTING_FLAG_NAME) ? tuConfig.getBoolean(Constants.LOCAL_TESTING_FLAG_NAME) : false; // Mutates AmazonS3Client object to have local endpoint if (canConnectToHTTPEndpoint) { this.s3.setEndpoint(Constants.LOCAL_TESTING_ENDPOINT); this.s3.setS3ClientOptions(S3ClientOptions.builder() // Prevents reformatting host address to accommodate AWS service hostname pattern .setPathStyleAccess(true) // Skips data integrity check after each transfer because correct // hashing algorithm isn't yet implemented in local storage server .skipContentMd5Check(true) .build()); } TransferUtility.setUserAgentFromConfig(this.awsConfig.getUserAgent()); } catch (Exception e) { throw new IllegalArgumentException("Failed to read S3TransferUtility " + "please check your setup or awsconfiguration.json file", e); } } if (this.transferUtilityOptions == null) { this.transferUtilityOptions = new TransferUtilityOptions(); } return new TransferUtility(this.s3, this.appContext, this.defaultBucket, this.transferUtilityOptions); } } /** * Minimum calls required. * TransferUtility.builder().s3Client(s3).context(context).build() * * @return The builder object to construct a TransferUtility. */ public static Builder builder() { return new Builder(); } /** * Constructor. * * @param s3 The client to use when making requests to Amazon S3 * @param context The current context * @param defaultBucket The name of the default S3 bucket * @param tuOptions The TransferUtility Options object */ private TransferUtility(AmazonS3 s3, Context context, String defaultBucket, TransferUtilityOptions tuOptions) { this.s3 = s3; this.defaultBucket = defaultBucket; this.transferUtilityOptions = tuOptions; this.dbUtil = new TransferDBUtil(context.getApplicationContext()); this.updater = TransferStatusUpdater.getInstance(context.getApplicationContext()); TransferThreadPool.init(this.transferUtilityOptions.getTransferThreadPoolSize()); this.connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); } /** * Constructs a new TransferUtility specifying the client to use and initializes * configuration of TransferUtility and a key for S3 client weak reference. * * @param s3 The client to use when making requests to Amazon S3 * @param context The current context * * @deprecated Please use * TransferUtility.builder().s3Client(s3).context(context).build() */ @Deprecated public TransferUtility(AmazonS3 s3, Context context) { this.s3 = s3; this.defaultBucket = null; this.transferUtilityOptions = new TransferUtilityOptions(); this.dbUtil = new TransferDBUtil(context.getApplicationContext()); this.updater = TransferStatusUpdater.getInstance(context.getApplicationContext()); TransferThreadPool.init(this.transferUtilityOptions.getTransferThreadPoolSize()); this.connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); } private String getDefaultBucketOrThrow() { if (this.defaultBucket == null) { throw new IllegalArgumentException( "TransferUtility has not been " + "configured with a default bucket. Please use the " + "corresponding method that specifies bucket name or " + "configure the default bucket name in construction of " + "the object. See TransferUtility.builder().defaultBucket() " + "or TransferUtility.builder().awsConfiguration()"); } return this.defaultBucket; } /** * Starts downloading the S3 object specified by the bucket and the key to the * given file. The file must be a valid file. Directory isn't supported. Note * that if the given file exists, it'll be overwritten. * * @param bucket The name of the bucket containing the object to download. * @param key The key under which the object to download is stored. * @param file The file to download the object's data to. * @return A TransferObserver used to track download progress and state */ public TransferObserver download(String bucket, String key, File file) { return download(bucket, key, file, null); } /** * Starts downloading the S3 object specified by the default bucket and * the key to the given file. The file must be a valid file. Directory isn't * supported. Note that if the given file exists, it'll be overwritten. * * @param key The key under which the object to download is stored. * @param file The file to download the object's data to. * @return A TransferObserver used to track download progress and state */ public TransferObserver download(String key, File file) { return download(getDefaultBucketOrThrow(), key, file, null); } /** * Starts downloading the S3 object specified by the bucket and the key to the * given file. The file must be a valid file. Directory isn't supported. Note * that if the given file exists, it'll be overwritten. * * @param bucket The name of the bucket containing the object to download. * @param key The key under which the object to download is stored. * @param file The file to download the object's data to. * @param listener a listener to attach to transfer observer. * @return A TransferObserver used to track download progress and state */ public TransferObserver download(String bucket, String key, File file, TransferListener listener) { if (file == null || file.isDirectory()) { throw new IllegalArgumentException("Invalid file: " + file); } final Uri uri = dbUtil.insertSingleTransferRecord(TransferType.DOWNLOAD, bucket, key, file, transferUtilityOptions); final int recordId = Integer.parseInt(uri.getLastPathSegment()); if (file.isFile()) { LOGGER.warn("Overwrite existing file: " + file); file.delete(); } // Creating the observer before the job is submitted because the listener needs to be registered // with TransferStatusUpdater when the job is being submitted. TransferObserver transferObserver = new TransferObserver(recordId, dbUtil, bucket, key, file, listener); submitTransferJob(TRANSFER_ADD, recordId); return transferObserver; } /** * Starts downloading the S3 object specified by the default bucket and * the key to the given file. The file must be a valid file. Directory isn't * supported. Note that if the given file exists, it'll be overwritten. * * @param key The key under which the object to download is stored. * @param file The file to download the object's data to. * @param listener a listener to attach to transfer observer. * @return A TransferObserver used to track download progress and state */ public TransferObserver download(String key, File file, TransferListener listener) { return download(getDefaultBucketOrThrow(), key, file, listener); } /** * Starts uploading the file to the given bucket, using the given key. The file * must be a valid file. Directory isn't supported. * * @param bucket The name of the bucket to upload the new object to. * @param key The key in the specified bucket by which to store the new * object. * @param file The file to upload. * @return A TransferObserver used to track upload progress and state */ public TransferObserver upload(String bucket, String key, File file) { return upload(bucket, key, file, new ObjectMetadata()); } /** * Starts uploading the file to the default bucket, using the given key. * The file must be a valid file. Directory isn't supported. * * @param key The key in the specified bucket by which to store the new object. * @param file The file to upload. * @return A TransferObserver used to track upload progress and state */ public TransferObserver upload(String key, File file) { return upload(getDefaultBucketOrThrow(), key, file, new ObjectMetadata()); } /** * Starts uploading the file to the given bucket, using the given key. The file * must be a valid file. Directory isn't supported. * * @param bucket The name of the bucket to upload the new object to. * @param key The key in the specified bucket by which to store the new * object. * @param file The file to upload. * @param cannedAcl The canned ACL to associate with this object * @return A TransferObserver used to track upload progress and state */ public TransferObserver upload(String bucket, String key, File file, CannedAccessControlList cannedAcl) { return upload(bucket, key, file, new ObjectMetadata(), cannedAcl); } /** * Starts uploading the file to the default bucket, using the given key. * The file must be a valid file. Directory isn't supported. * * @param key The key in the specified bucket by which to store the new * object. * @param file The file to upload. * @param cannedAcl The canned ACL to associate with this object * @return A TransferObserver used to track upload progress and state */ public TransferObserver upload(String key, File file, CannedAccessControlList cannedAcl) { return upload(getDefaultBucketOrThrow(), key, file, new ObjectMetadata(), cannedAcl); } /** * Starts uploading the file to the given bucket, using the given key. The file * must be a valid file. Directory isn't supported. * * @param bucket The name of the bucket to upload the new object to. * @param key The key in the specified bucket by which to store the new * object. * @param file The file to upload. * @param metadata The S3 metadata to associate with this object * @return A TransferObserver used to track upload progress and state */ public TransferObserver upload(String bucket, String key, File file, ObjectMetadata metadata) { return upload(bucket, key, file, metadata, null); } /** * Starts uploading the file to the default bucket, using the given key. * The file must be a valid file. Directory isn't supported. * * @param key The key in the specified bucket by which to store the new * object. * @param file The file to upload. * @param metadata The S3 metadata to associate with this object * @return A TransferObserver used to track upload progress and state */ public TransferObserver upload(String key, File file, ObjectMetadata metadata) { return upload(getDefaultBucketOrThrow(), key, file, metadata, null); } /** * Starts uploading the file to the given bucket, using the given key. The file * must be a valid file. Directory isn't supported. * * @param bucket The name of the bucket to upload the new object to. * @param key The key in the specified bucket by which to store the new * object. * @param file The file to upload. * @param metadata The S3 metadata to associate with this object * @param cannedAcl The canned ACL to associate with this object * @return A TransferObserver used to track upload progress and state */ public TransferObserver upload(String bucket, String key, File file, ObjectMetadata metadata, CannedAccessControlList cannedAcl) { return upload(bucket, key, file, metadata, cannedAcl, null); } /** * Starts uploading the file to the default bucket, using the given key. * The file must be a valid file. Directory isn't supported. * * @param key The key in the specified bucket by which to store the new * object. * @param file The file to upload. * @param metadata The S3 metadata to associate with this object * @param cannedAcl The canned ACL to associate with this object * @return A TransferObserver used to track upload progress and state */ public TransferObserver upload(String key, File file, ObjectMetadata metadata, CannedAccessControlList cannedAcl) { return upload(getDefaultBucketOrThrow(), key, file, metadata, cannedAcl, null); } /** * Starts uploading the file to the given bucket, using the given key. The file * must be a valid file. Directory isn't supported. * * @param bucket The name of the bucket to upload the new object to. * @param key The key in the specified bucket by which to store the new * object. * @param file The file to upload. * @param metadata The S3 metadata to associate with this object * @param cannedAcl The canned ACL to associate with this object * @param listener a listener to attach to transfer observer. * @return A TransferObserver used to track upload progress and state */ public TransferObserver upload(String bucket, String key, File file, ObjectMetadata metadata, CannedAccessControlList cannedAcl, TransferListener listener) { if (file == null || file.isDirectory() || !file.exists()) { throw new IllegalArgumentException("Invalid file: " + file); } int recordId; if (shouldUploadInMultipart(file)) { recordId = createMultipartUploadRecords(bucket, key, file, metadata, cannedAcl); } else { final Uri uri = dbUtil.insertSingleTransferRecord(TransferType.UPLOAD, bucket, key, file, metadata, cannedAcl, transferUtilityOptions); recordId = Integer.parseInt(uri.getLastPathSegment()); } // Creating the observer before the job is submitted because the listener needs to be registered // with TransferStatusUpdater when the job is being submitted. TransferObserver transferObserver = new TransferObserver(recordId, dbUtil, bucket, key, file, listener); submitTransferJob(TRANSFER_ADD, recordId); return transferObserver; } /** * Starts uploading the file to the default bucket, using the given key. * The file must be a valid file. Directory isn't supported. * * @param key The key in the specified bucket by which to store the new * object. * @param file The file to upload. * @param metadata The S3 metadata to associate with this object * @param cannedAcl The canned ACL to associate with this object * @param listener a listener to attach to transfer observer. * @return A TransferObserver used to track upload progress and state */ public TransferObserver upload(String key, File file, ObjectMetadata metadata, CannedAccessControlList cannedAcl, TransferListener listener) { return upload(getDefaultBucketOrThrow(), key, file, metadata, cannedAcl, listener); } /** * Starts uploading the inputStream to the default bucket, using the given key. * * @param key The key in the specified bucket by which to store the new object. * @param inputStream The input stream to upload. * @return A TransferObserver used to track upload progress and state */ public TransferObserver upload(String key, InputStream inputStream) throws IOException { return upload(key, inputStream, UploadOptions.builder().build()); } /** * Starts uploading the inputStream to the given bucket, using the given key. * * @param key The key in the specified bucket by which to store the new object. * @param inputStream The input stream to upload. * @param options An UploadOptions which hold all of the optional parameters * i.e. bucket, metadata, cannedAcl and transferListener. * @return A TransferObserver used to track upload progress and state */ public TransferObserver upload(String key, InputStream inputStream, UploadOptions options) throws IOException { File file = writeInputStreamToFile(inputStream); return upload( options.getBucket() != null ? options.getBucket() : getDefaultBucketOrThrow(), key, file, options.getMetadata() != null ? options.getMetadata() : new ObjectMetadata(), options.getCannedAcl(), options.getTransferListener() ); } /** * Gets a TransferObserver instance to track the record with the given id. * * @param id A transfer id. * @return The TransferObserver instance which is observing the record. */ public TransferObserver getTransferById(int id) { Cursor c = null; try { c = dbUtil.queryTransferById(id); if (c.moveToNext()) { final TransferObserver to = new TransferObserver(id, dbUtil); to.updateFromDB(c); return to; } } finally { if (c != null) { c.close(); } } return null; } /** * Gets a list of TransferObserver instances which are observing records with * the given type. * * @param type The type of the transfer "any". * @return A list of TransferObserver instances. */ public List