diff --git a/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/AWSS3StoragePathDownloadTest.kt b/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/AWSS3StoragePathDownloadTest.kt new file mode 100644 index 000000000..b14f97270 --- /dev/null +++ b/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/AWSS3StoragePathDownloadTest.kt @@ -0,0 +1,271 @@ +/* + * Copyright 2021 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.amplifyframework.storage.s3 + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.amplifyframework.auth.AuthPlugin +import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin +import com.amplifyframework.core.Amplify +import com.amplifyframework.core.async.Cancelable +import com.amplifyframework.core.async.Resumable +import com.amplifyframework.hub.HubChannel +import com.amplifyframework.hub.HubEvent +import com.amplifyframework.hub.SubscriptionToken +import com.amplifyframework.storage.StorageCategory +import com.amplifyframework.storage.StorageChannelEventName +import com.amplifyframework.storage.StoragePath +import com.amplifyframework.storage.TransferState +import com.amplifyframework.storage.TransferState.Companion.getState +import com.amplifyframework.storage.operation.StorageDownloadFileOperation +import com.amplifyframework.storage.options.StorageDownloadFileOptions +import com.amplifyframework.storage.options.StorageUploadFileOptions +import com.amplifyframework.storage.s3.options.AWSS3StorageDownloadFileOptions +import com.amplifyframework.storage.s3.test.R +import com.amplifyframework.storage.s3.util.WorkmanagerTestUtils.initializeWorkmanagerTestUtil +import com.amplifyframework.testutils.FileAssert +import com.amplifyframework.testutils.random.RandomTempFile +import com.amplifyframework.testutils.sync.SynchronousAuth +import com.amplifyframework.testutils.sync.SynchronousStorage +import java.io.File +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import org.junit.After +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.BeforeClass +import org.junit.Test + +/** + * Instrumentation test for operational work on download. + */ +class AWSS3StoragePathDownloadTest { + // Create a file to download to + private val downloadFile: File = RandomTempFile() + private val options = StorageDownloadFileOptions.defaultInstance() + // Create a set to remember all the subscriptions + private val subscriptions = mutableSetOf() + + companion object { + private val EXTENDED_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(60) + private const val LARGE_FILE_SIZE = 10 * 1024 * 1024L // 10 MB + private const val SMALL_FILE_SIZE = 100L + private val LARGE_FILE_NAME = "large-${System.currentTimeMillis()}" + private val LARGE_FILE_PATH = StoragePath.fromString("public/$LARGE_FILE_NAME") + private val SMALL_FILE_NAME = "small-${System.currentTimeMillis()}" + private val SMALL_FILE_PATH = StoragePath.fromString("public/$SMALL_FILE_NAME") + + lateinit var storageCategory: StorageCategory + lateinit var synchronousStorage: SynchronousStorage + lateinit var largeFile: File + lateinit var smallFile: File + + /** + * Initialize mobile client and configure the storage. + * Upload the test files ahead of time. + */ + @JvmStatic + @BeforeClass + fun setUpOnce() { + val context = ApplicationProvider.getApplicationContext() + initializeWorkmanagerTestUtil(context) + SynchronousAuth.delegatingToCognito(context, AWSCognitoAuthPlugin() as AuthPlugin<*>) + + // Get a handle to storage + storageCategory = TestStorageCategory.create(context, R.raw.amplifyconfiguration) + synchronousStorage = SynchronousStorage.delegatingTo(storageCategory) + + val uploadOptions = StorageUploadFileOptions.defaultInstance() + + // Upload large test file + largeFile = RandomTempFile(LARGE_FILE_NAME, LARGE_FILE_SIZE) + synchronousStorage.uploadFile(LARGE_FILE_PATH, largeFile, uploadOptions, EXTENDED_TIMEOUT_MS) + + // Upload small test file + smallFile = RandomTempFile(SMALL_FILE_NAME, SMALL_FILE_SIZE) + synchronousStorage.uploadFile(SMALL_FILE_PATH, smallFile, uploadOptions) + } + } + + /** + * Unsubscribe from everything after each test. + */ + @After + fun tearDown() { + // Unsubscribe from everything + for (token in subscriptions) { + Amplify.Hub.unsubscribe(token) + } + } + + @Test + fun testDownloadSmallFile() { + synchronousStorage.downloadFile(SMALL_FILE_PATH, downloadFile, options) + FileAssert.assertEquals(smallFile, downloadFile) + } + + @Test + fun testDownloadLargeFile() { + synchronousStorage.downloadFile( + LARGE_FILE_PATH, + downloadFile, + options, + EXTENDED_TIMEOUT_MS + ) + FileAssert.assertEquals(largeFile, downloadFile) + } + + @Test + fun testDownloadFileIsCancelable() { + val canceled = CountDownLatch(1) + val opContainer = AtomicReference() + val errorContainer = AtomicReference() + + // Listen to Hub events for cancel + val cancelToken = Amplify.Hub.subscribe(HubChannel.STORAGE) { hubEvent: HubEvent<*> -> + if (StorageChannelEventName.DOWNLOAD_STATE.toString() == hubEvent.name) { + val state = getState(hubEvent.data as String) + if (TransferState.CANCELED == state) { + canceled.countDown() + } + } + } + subscriptions.add(cancelToken) + + // Begin downloading a large file + val op = storageCategory.downloadFile( + LARGE_FILE_PATH, + downloadFile, + options, + { + if (it.currentBytes > 0 && canceled.count > 0) { + opContainer.get().cancel() + } + }, + { errorContainer.set(RuntimeException("Download completed without canceling.")) }, + { newValue -> errorContainer.set(newValue) } + ) + opContainer.set(op) + + // Assert that the required conditions have been met + assertTrue(canceled.await(EXTENDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)) + assertNull(errorContainer.get()) + } + + @Test + fun testDownloadFileIsResumable() { + val completed = CountDownLatch(1) + val resumed = CountDownLatch(1) + val opContainer = AtomicReference() + val errorContainer = AtomicReference() + + // Listen to Hub events to resume when operation has been paused + val resumeToken = Amplify.Hub.subscribe(HubChannel.STORAGE) { hubEvent: HubEvent<*> -> + if (StorageChannelEventName.DOWNLOAD_STATE.toString() == hubEvent.name) { + val state = getState(hubEvent.data as String) + if (TransferState.PAUSED == state) { + opContainer.get().resume() + resumed.countDown() + } + } + } + subscriptions.add(resumeToken) + + // Begin downloading a large file + val op = storageCategory.downloadFile( + LARGE_FILE_PATH, + downloadFile, + options, + { + if (it.currentBytes > 0 && resumed.count > 0) { + opContainer.get().pause() + } + }, + { completed.countDown() }, + { errorContainer.set(it) } + ) + opContainer.set(op) + + // Assert that all the required conditions have been met + assertTrue(resumed.await(EXTENDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)) + assertTrue(completed.await(EXTENDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)) + assertNull(errorContainer.get()) + FileAssert.assertEquals(largeFile, downloadFile) + } + + @Test + fun testGetTransferOnPause() { + val completed = CountDownLatch(1) + val resumed = CountDownLatch(1) + val opContainer = AtomicReference>() + val transferId = AtomicReference() + val errorContainer = AtomicReference() + // Listen to Hub events to resume when operation has been paused + val resumeToken = Amplify.Hub.subscribe(HubChannel.STORAGE) { hubEvent: HubEvent<*> -> + if (StorageChannelEventName.DOWNLOAD_STATE.toString() == hubEvent.name) { + val state = getState(hubEvent.data as String) + if (TransferState.PAUSED == state) { + opContainer.get().clearAllListeners() + storageCategory.getTransfer( + transferId.get(), + { + val getOp = it as StorageDownloadFileOperation<*> + getOp.resume() + resumed.countDown() + getOp.setOnSuccess { completed.countDown() } + }, + { errorContainer.set(it) } + ) + } + } + } + subscriptions.add(resumeToken) + + // Begin downloading a large file + val op = storageCategory.downloadFile( + LARGE_FILE_PATH, + downloadFile, + options, + { + if (it.currentBytes > 0 && resumed.count > 0) { + opContainer.get().pause() + } + }, + { }, + { errorContainer.set(it) } + ) + + opContainer.set(op) + transferId.set(op.transferId) + + // Assert that all the required conditions have been met + assertTrue(resumed.await(EXTENDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)) + assertTrue(completed.await(EXTENDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)) + assertNull(errorContainer.get()) + FileAssert.assertEquals(largeFile, downloadFile) + } + + @Test + fun testDownloadLargeFileWithAccelerationEnabled() { + val awsS3Options = AWSS3StorageDownloadFileOptions.builder().setUseAccelerateEndpoint(true).build() + synchronousStorage.downloadFile( + LARGE_FILE_PATH, + downloadFile, + awsS3Options, + EXTENDED_TIMEOUT_MS + ) + } +} diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/AWSS3StoragePlugin.java b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/AWSS3StoragePlugin.java index 3866406c7..3b01b9d86 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/AWSS3StoragePlugin.java +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/AWSS3StoragePlugin.java @@ -57,6 +57,9 @@ import com.amplifyframework.storage.s3.operation.AWSS3StorageDownloadFileOperation; import com.amplifyframework.storage.s3.operation.AWSS3StorageGetPresignedUrlOperation; import com.amplifyframework.storage.s3.operation.AWSS3StorageListOperation; +import com.amplifyframework.storage.s3.operation.AWSS3StoragePathDownloadFileOperation; +import com.amplifyframework.storage.s3.operation.AWSS3StoragePathUploadFileOperation; +import com.amplifyframework.storage.s3.operation.AWSS3StoragePathUploadInputStreamOperation; import com.amplifyframework.storage.s3.operation.AWSS3StorageRemoveOperation; import com.amplifyframework.storage.s3.operation.AWSS3StorageUploadFileOperation; import com.amplifyframework.storage.s3.operation.AWSS3StorageUploadInputStreamOperation; @@ -68,6 +71,8 @@ import com.amplifyframework.storage.s3.request.AWSS3StorageDownloadFileRequest; import com.amplifyframework.storage.s3.request.AWSS3StorageGetPresignedUrlRequest; import com.amplifyframework.storage.s3.request.AWSS3StorageListRequest; +import com.amplifyframework.storage.s3.request.AWSS3StoragePathDownloadFileRequest; +import com.amplifyframework.storage.s3.request.AWSS3StoragePathUploadRequest; import com.amplifyframework.storage.s3.request.AWSS3StorageRemoveRequest; import com.amplifyframework.storage.s3.request.AWSS3StorageUploadRequest; import com.amplifyframework.storage.s3.service.AWSS3StorageService; @@ -271,7 +276,7 @@ public StorageGetUrlOperation getUrl( ) { return getUrl(path, StorageGetUrlOptions.defaultInstance(), onSuccess, onError); } - + @NonNull @Override @SuppressWarnings("deprecation") @@ -419,8 +424,28 @@ public StorageDownloadFileOperation downloadFile( @NonNull Consumer onSuccess, @NonNull Consumer onError ) { - // TODO - return null; + boolean useAccelerateEndpoint = + options instanceof AWSS3StorageDownloadFileOptions && + ((AWSS3StorageDownloadFileOptions) options).useAccelerateEndpoint(); + + AWSS3StoragePathDownloadFileRequest request = new AWSS3StoragePathDownloadFileRequest( + path, + local, + useAccelerateEndpoint + ); + + AWSS3StoragePathDownloadFileOperation operation = new AWSS3StoragePathDownloadFileOperation( + request, + storageService, + executorService, + authCredentialsProvider, + onProgress, + onSuccess, + onError + ); + operation.start(); + + return operation; } @SuppressWarnings("deprecation") @@ -526,8 +551,31 @@ public StorageUploadFileOperation uploadFile( @NonNull Consumer onSuccess, @NonNull Consumer onError ) { - // TODO: Implement - return null; + boolean useAccelerateEndpoint = options instanceof AWSS3StorageUploadFileOptions && + ((AWSS3StorageUploadFileOptions) options).useAccelerateEndpoint(); + AWSS3StoragePathUploadRequest request = new AWSS3StoragePathUploadRequest<>( + path, + local, + options.getContentType(), + options instanceof AWSS3StorageUploadFileOptions + ? ((AWSS3StorageUploadFileOptions) options).getServerSideEncryption() + : ServerSideEncryption.NONE, + options.getMetadata(), + useAccelerateEndpoint + ); + + AWSS3StoragePathUploadFileOperation operation = new AWSS3StoragePathUploadFileOperation( + request, + storageService, + executorService, + authCredentialsProvider, + onProgress, + onSuccess, + onError + ); + operation.start(); + + return operation; } @SuppressWarnings("deprecation") @@ -631,8 +679,32 @@ public StorageUploadInputStreamOperation uploadInputStream( @NonNull Consumer onSuccess, @NonNull Consumer onError ) { - // TODO: Implement - return null; + boolean useAccelerateEndpoint = options instanceof AWSS3StorageUploadInputStreamOptions && + ((AWSS3StorageUploadInputStreamOptions) options).useAccelerateEndpoint(); + AWSS3StoragePathUploadRequest request = new AWSS3StoragePathUploadRequest<>( + path, + local, + options.getContentType(), + options instanceof AWSS3StorageUploadInputStreamOptions + ? ((AWSS3StorageUploadInputStreamOptions) options).getServerSideEncryption() + : ServerSideEncryption.NONE, + options.getMetadata(), + useAccelerateEndpoint + ); + + AWSS3StoragePathUploadInputStreamOperation operation = + new AWSS3StoragePathUploadInputStreamOperation( + request, + storageService, + executorService, + authCredentialsProvider, + onProgress, + onSuccess, + onError + ); + operation.start(); + + return operation; } @SuppressWarnings("deprecation") diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/extensions/StorageExceptionExtensions.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/extensions/StorageExceptionExtensions.kt index dd7828d4d..81a142b93 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/extensions/StorageExceptionExtensions.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/extensions/StorageExceptionExtensions.kt @@ -5,8 +5,8 @@ import com.amplifyframework.storage.StoragePathValidationException internal fun StoragePathValidationException.Companion.invalidStoragePathException() = StorageException( "Invalid StoragePath provided", - StoragePathValidationException("Invalid StoragePath provided", "Path must be NonEmpty and start with /"), - "Path must be NonEmpty and start with /" + StoragePathValidationException("Invalid StoragePath provided", "Path must not be empty or start with /"), + "Path must not be empty or start with /" ) internal fun StoragePathValidationException.Companion.unsupportedStoragePathException() = StorageException( diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/extensions/StoragePath.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/extensions/StoragePath.kt new file mode 100644 index 000000000..0508f46e5 --- /dev/null +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/extensions/StoragePath.kt @@ -0,0 +1,41 @@ +package com.amplifyframework.storage.s3.extensions + +import com.amplifyframework.auth.AuthCredentialsProvider +import com.amplifyframework.storage.IdentityIdProvidedStoragePath +import com.amplifyframework.storage.StorageException +import com.amplifyframework.storage.StoragePath +import com.amplifyframework.storage.StoragePathValidationException +import com.amplifyframework.storage.StringStoragePath +import kotlinx.coroutines.runBlocking + +internal fun StoragePath.toS3ServiceKey(authCredentialsProvider: AuthCredentialsProvider): String { + val stringPath = when (this) { + is StringStoragePath -> { + resolvePath() + } + is IdentityIdProvidedStoragePath -> { + val identityId = try { + runBlocking { + authCredentialsProvider.getIdentityId() + } + } catch (e: Exception) { + throw StorageException( + "Failed to fetch identity ID", + e, + "See included exception for more details and suggestions to fix." + ) + } + resolvePath(identityId) + } + + else -> { + throw StoragePathValidationException.unsupportedStoragePathException() + } + } + + if (stringPath.startsWith("/") || stringPath.isEmpty()) { + throw StoragePathValidationException.invalidStoragePathException() + } + + return stringPath +} diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageDownloadFileOperation.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageDownloadFileOperation.kt index 907331d05..97a18388e 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageDownloadFileOperation.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageDownloadFileOperation.kt @@ -38,8 +38,8 @@ import java.util.concurrent.ExecutorService * An operation to download a file from AWS S3. */ @Deprecated( - "Class should not be public and explicitly cast to. " + - "Internal usages are moving to AWSS3StorageDownloadFileOperationV2" + "Class should not be public and explicitly cast to. Case to StorageDownloadFileOperation." + + "Internal usages are moving to AWSS3StoragePathDownloadFileOperation" ) class AWSS3StorageDownloadFileOperation @JvmOverloads internal constructor( transferId: String, diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageGetPresignedUrlOperation.java b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageGetPresignedUrlOperation.java index d70b94b69..1d3f4eab4 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageGetPresignedUrlOperation.java +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageGetPresignedUrlOperation.java @@ -32,8 +32,8 @@ /** * An operation to retrieve pre-signed object URL from AWS S3. - * @deprecated Class should not be public and explicitly cast to. - * Internal usages are moving to AWSS3StorageGetPresignedUrlOperationV2 + * @deprecated Class should not be public and explicitly cast to. Cast to StorageGetUrlOperation. + * Internal usages are moving to AWSS3StoragePathGetPresignedUrlOperation */ @Deprecated public final class AWSS3StorageGetPresignedUrlOperation diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageListOperation.java b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageListOperation.java index e4f275ce9..70b57353a 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageListOperation.java +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageListOperation.java @@ -32,9 +32,9 @@ import java.util.concurrent.ExecutorService; /** - * Internal usages are moving to AWSS3StorageListOperationV2 + * Internal usages are moving to AWSS3StoragePathListOperation * An operation to list items from AWS S3. - * @deprecated Class should not be public and explicitly cast to. + * @deprecated Class should not be public and explicitly cast to. Cast to StorageListOperation */ @Deprecated public final class AWSS3StorageListOperation extends StorageListOperation { diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathDownloadFileOperation.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathDownloadFileOperation.kt index 952e4aa13..989a118a3 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathDownloadFileOperation.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathDownloadFileOperation.kt @@ -19,17 +19,13 @@ import com.amplifyframework.core.Amplify import com.amplifyframework.core.Consumer import com.amplifyframework.hub.HubChannel import com.amplifyframework.hub.HubEvent -import com.amplifyframework.storage.IdentityIdProvidedStoragePath import com.amplifyframework.storage.StorageChannelEventName import com.amplifyframework.storage.StorageException -import com.amplifyframework.storage.StoragePathValidationException -import com.amplifyframework.storage.StringStoragePath import com.amplifyframework.storage.TransferState import com.amplifyframework.storage.operation.StorageDownloadFileOperation import com.amplifyframework.storage.result.StorageDownloadFileResult import com.amplifyframework.storage.result.StorageTransferProgress -import com.amplifyframework.storage.s3.extensions.invalidStoragePathException -import com.amplifyframework.storage.s3.extensions.unsupportedStoragePathException +import com.amplifyframework.storage.s3.extensions.toS3ServiceKey import com.amplifyframework.storage.s3.request.AWSS3StoragePathDownloadFileRequest import com.amplifyframework.storage.s3.service.StorageService import com.amplifyframework.storage.s3.transfer.TransferListener @@ -37,14 +33,13 @@ import com.amplifyframework.storage.s3.transfer.TransferObserver import java.io.File import java.util.UUID import java.util.concurrent.ExecutorService -import kotlinx.coroutines.runBlocking /** * An operation to download a file from AWS S3. */ internal class AWSS3StoragePathDownloadFileOperation( private val transferId: String = UUID.randomUUID().toString(), - private val request: AWSS3StoragePathDownloadFileRequest?, + private val request: AWSS3StoragePathDownloadFileRequest, private var file: File, private val storageService: StorageService, private val executorService: ExecutorService, @@ -92,49 +87,20 @@ internal class AWSS3StoragePathDownloadFileOperation( if (transferObserver != null) { return } - val downloadRequest = request ?: return executorService.submit { + val serviceKey = try { + request.path.toS3ServiceKey(authCredentialsProvider) + } catch (se: StorageException) { + onError.accept(se) + return@submit + } try { - - val path = when (val storagePath = downloadRequest.path) { - is StringStoragePath -> { - storagePath.resolvePath() - } - is IdentityIdProvidedStoragePath -> { - val identityId = try { - runBlocking { - authCredentialsProvider.getIdentityId() - } - } catch (e: Exception) { - onError?.accept( - StorageException( - "Failed to fetch identity ID", - e, - "See included exception for more details and suggestions to fix." - ) - ) - return@submit - } - storagePath.resolvePath(identityId) - } - - else -> { - onError?.accept(StoragePathValidationException.unsupportedStoragePathException()) - return@submit - } - } - - if (!path.startsWith("/")) { - onError?.accept(StoragePathValidationException.invalidStoragePathException()) - return@submit - } - transferObserver = storageService.downloadToFile( transferId, - path, - downloadRequest.local, - downloadRequest.useAccelerateEndpoint + serviceKey, + request.local, + request.useAccelerateEndpoint ) transferObserver?.setTransferListener(DownloadTransferListener()) } catch (e: Exception) { @@ -229,7 +195,6 @@ internal class AWSS3StoragePathDownloadFileOperation( onSuccess?.accept(StorageDownloadFileResult.fromFile(file)) return } - TransferState.FAILED -> {} else -> {} } } @@ -238,7 +203,7 @@ internal class AWSS3StoragePathDownloadFileOperation( onProgress?.accept(StorageTransferProgress(bytesCurrent, bytesTotal)) } - override fun onError(id: Int, ex: java.lang.Exception) { + override fun onError(id: Int, ex: Exception) { Amplify.Hub.publish( HubChannel.STORAGE, HubEvent.create(StorageChannelEventName.DOWNLOAD_ERROR, ex) diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathUploadFileOperation.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathUploadFileOperation.kt new file mode 100644 index 000000000..d326e7091 --- /dev/null +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathUploadFileOperation.kt @@ -0,0 +1,233 @@ +/* + * Copyright 2022 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.amplifyframework.storage.s3.operation + +import com.amplifyframework.auth.AuthCredentialsProvider +import com.amplifyframework.core.Amplify +import com.amplifyframework.core.Consumer +import com.amplifyframework.hub.HubChannel +import com.amplifyframework.hub.HubEvent +import com.amplifyframework.storage.ObjectMetadata +import com.amplifyframework.storage.StorageChannelEventName +import com.amplifyframework.storage.StorageException +import com.amplifyframework.storage.TransferState +import com.amplifyframework.storage.operation.StorageUploadFileOperation +import com.amplifyframework.storage.result.StorageTransferProgress +import com.amplifyframework.storage.result.StorageUploadFileResult +import com.amplifyframework.storage.s3.ServerSideEncryption +import com.amplifyframework.storage.s3.extensions.toS3ServiceKey +import com.amplifyframework.storage.s3.request.AWSS3StoragePathUploadRequest +import com.amplifyframework.storage.s3.service.StorageService +import com.amplifyframework.storage.s3.transfer.TransferListener +import com.amplifyframework.storage.s3.transfer.TransferObserver +import java.io.File +import java.util.UUID +import java.util.concurrent.ExecutorService + +/** + * An operation to upload a file from AWS S3. + */ +internal class AWSS3StoragePathUploadFileOperation @JvmOverloads internal constructor( + transferId: String, + request: AWSS3StoragePathUploadRequest, + private val storageService: StorageService, + private val executorService: ExecutorService, + private val authCredentialsProvider: AuthCredentialsProvider, + private var transferObserver: TransferObserver? = null, + onProgress: Consumer? = null, + onSuccess: Consumer? = null, + onError: Consumer? = null +) : StorageUploadFileOperation>( + request, + transferId, + onProgress, + onSuccess, + onError +) { + + constructor( + request: AWSS3StoragePathUploadRequest, + storageService: StorageService, + executorService: ExecutorService, + authCredentialsProvider: AuthCredentialsProvider, + onProgress: Consumer, + onSuccess: Consumer, + onError: Consumer + ) : this( + UUID.randomUUID().toString(), + request, + storageService, + executorService, + authCredentialsProvider, + null, + onProgress, + onSuccess, + onError + ) + + init { + transferObserver?.setTransferListener(UploadTransferListener()) + } + + override fun start() { + // Only start if it hasn't already been started + if (transferObserver != null) { + return + } + val uploadRequest = request ?: return + + executorService.submit { + val serviceKey = try { + uploadRequest.path.toS3ServiceKey(authCredentialsProvider) + } catch (se: StorageException) { + onError.accept(se) + return@submit + } + + try { + val file = uploadRequest.local + + // Set up the metadata + val objectMetadata = ObjectMetadata() + objectMetadata.userMetadata = uploadRequest.metadata + objectMetadata.metaData[ObjectMetadata.CONTENT_TYPE] = uploadRequest.contentType + val storageServerSideEncryption = uploadRequest.serverSideEncryption + if (ServerSideEncryption.NONE != storageServerSideEncryption) { + objectMetadata.metaData[ObjectMetadata.SERVER_SIDE_ENCRYPTION] = + storageServerSideEncryption.getName() + } + transferObserver = storageService.uploadFile( + transferId, + serviceKey, + file, + objectMetadata, + uploadRequest.useAccelerateEndpoint + ) + transferObserver?.setTransferListener(UploadTransferListener()) + } catch (exception: Exception) { + onError?.accept( + StorageException( + "Issue uploading file.", + exception, + "See included exception for more details and suggestions to fix." + ) + ) + } + } + } + + override fun pause() { + executorService.submit { + transferObserver?.let { + try { + storageService.pauseTransfer(it) + } catch (exception: java.lang.Exception) { + onError?.accept( + StorageException( + "Something went wrong while attempting to pause your " + + "AWS S3 Storage upload file operation", + exception, + "See attached exception for more information and suggestions" + ) + ) + } + } + } + } + + override fun resume() { + executorService.submit { + transferObserver?.let { + try { + storageService.resumeTransfer(it) + } catch (exception: java.lang.Exception) { + onError?.accept( + StorageException( + "Something went wrong while attempting to resume your " + + "AWS S3 Storage upload file operation", + exception, + "See attached exception for more information and suggestions" + ) + ) + } + } + } + } + + override fun cancel() { + executorService.submit { + transferObserver?.let { + try { + storageService.cancelTransfer(it) + } catch (exception: java.lang.Exception) { + onError?.accept( + StorageException( + "Something went wrong while attempting to cancel your " + + "AWS S3 Storage upload file operation", + exception, + "See attached exception for more information and suggestions" + ) + ) + } + } + } + } + + override fun getTransferState(): TransferState { + return transferObserver?.transferState ?: TransferState.UNKNOWN + } + + override fun setOnSuccess(onSuccess: Consumer?) { + super.setOnSuccess(onSuccess) + val serviceKey = transferObserver?.key + if (transferState == TransferState.COMPLETED && serviceKey != null) { + onSuccess?.accept(StorageUploadFileResult(serviceKey, serviceKey)) + } + } + + private inner class UploadTransferListener : TransferListener { + override fun onStateChanged(id: Int, state: TransferState, key: String) { + Amplify.Hub.publish( + HubChannel.STORAGE, + HubEvent.create(StorageChannelEventName.UPLOAD_STATE, state.name) + ) + when (state) { + TransferState.COMPLETED -> { + onSuccess?.accept(StorageUploadFileResult(key, key)) + return + } + else -> {} + } + } + + override fun onProgressChanged(id: Int, bytesCurrent: Long, bytesTotal: Long) { + onProgress?.accept(StorageTransferProgress(bytesCurrent, bytesTotal)) + } + + override fun onError(id: Int, ex: Exception) { + Amplify.Hub.publish( + HubChannel.STORAGE, + HubEvent.create(StorageChannelEventName.UPLOAD_ERROR, ex) + ) + onError?.accept( + StorageException( + "Something went wrong with your AWS S3 Storage upload file operation", + ex, + "See attached exception for more information and suggestions" + ) + ) + } + } +} diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathUploadInputStreamOperation.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathUploadInputStreamOperation.kt new file mode 100644 index 000000000..4c48c3ad2 --- /dev/null +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathUploadInputStreamOperation.kt @@ -0,0 +1,232 @@ +/* + * Copyright 2022 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.amplifyframework.storage.s3.operation + +import com.amplifyframework.auth.AuthCredentialsProvider +import com.amplifyframework.core.Amplify +import com.amplifyframework.core.Consumer +import com.amplifyframework.hub.HubChannel +import com.amplifyframework.hub.HubEvent +import com.amplifyframework.storage.ObjectMetadata +import com.amplifyframework.storage.StorageChannelEventName +import com.amplifyframework.storage.StorageException +import com.amplifyframework.storage.TransferState +import com.amplifyframework.storage.operation.StorageUploadInputStreamOperation +import com.amplifyframework.storage.result.StorageTransferProgress +import com.amplifyframework.storage.result.StorageUploadInputStreamResult +import com.amplifyframework.storage.s3.ServerSideEncryption +import com.amplifyframework.storage.s3.extensions.toS3ServiceKey +import com.amplifyframework.storage.s3.request.AWSS3StoragePathUploadRequest +import com.amplifyframework.storage.s3.service.StorageService +import com.amplifyframework.storage.s3.transfer.TransferListener +import com.amplifyframework.storage.s3.transfer.TransferObserver +import java.io.InputStream +import java.util.UUID +import java.util.concurrent.ExecutorService + +/** + * An operation to upload an InputStream to AWS S3. + */ +internal class AWSS3StoragePathUploadInputStreamOperation @JvmOverloads internal constructor( + transferId: String, + private val request: AWSS3StoragePathUploadRequest, + private val storageService: StorageService, + private val executorService: ExecutorService, + private val authCredentialsProvider: AuthCredentialsProvider, + private var transferObserver: TransferObserver? = null, + onProgress: Consumer? = null, + onSuccess: Consumer? = null, + onError: Consumer? = null +) : StorageUploadInputStreamOperation>( + request, + transferId, + onProgress, + onSuccess, + onError +) { + + constructor( + request: AWSS3StoragePathUploadRequest, + storageService: StorageService, + executorService: ExecutorService, + authCredentialsProvider: AuthCredentialsProvider, + onProgress: Consumer, + onSuccess: Consumer, + onError: Consumer + ) : this( + UUID.randomUUID().toString(), + request, + storageService, + executorService, + authCredentialsProvider, + null, + onProgress, + onSuccess, + onError + ) + + init { + transferObserver?.setTransferListener(UploadTransferListener()) + } + + override fun start() { + // Only start if it hasn't already been started + if (transferObserver != null) { + return + } + + executorService.submit { + val serviceKey = try { + request.path.toS3ServiceKey(authCredentialsProvider) + } catch (se: StorageException) { + onError.accept(se) + return@submit + } + + try { + val inputStream = request.local + // Set up the metadata + val objectMetadata = ObjectMetadata() + objectMetadata.userMetadata = request.metadata + objectMetadata.metaData[ObjectMetadata.CONTENT_TYPE] = request.contentType + val storageServerSideEncryption = + request.serverSideEncryption + if (ServerSideEncryption.NONE != storageServerSideEncryption) { + objectMetadata.metaData[ObjectMetadata.SERVER_SIDE_ENCRYPTION] = + storageServerSideEncryption.getName() + } + transferObserver = storageService.uploadInputStream( + transferId, + serviceKey, + inputStream, + objectMetadata, + request.useAccelerateEndpoint + ) + transferObserver?.setTransferListener(UploadTransferListener()) + } catch (exception: Exception) { + onError?.accept( + StorageException( + "Issue uploading InputStream.", + exception, + "See included exception for more details and suggestions to fix." + ) + ) + } + } + } + + override fun pause() { + executorService.submit { + transferObserver?.let { + try { + storageService.pauseTransfer(it) + } catch (exception: java.lang.Exception) { + onError?.accept( + StorageException( + "Something went wrong while attempting to pause your " + + "AWS S3 Storage upload InputStream operation", + exception, + "See attached exception for more information and suggestions" + ) + ) + } + } + } + } + + override fun resume() { + executorService.submit { + transferObserver?.let { + try { + storageService.resumeTransfer(it) + } catch (exception: java.lang.Exception) { + onError?.accept( + StorageException( + "Something went wrong while attempting to resume your " + + "AWS S3 Storage upload InputStream operation", + exception, + "See attached exception for more information and suggestions" + ) + ) + } + } + } + } + + override fun cancel() { + executorService.submit { + transferObserver?.let { + try { + storageService.cancelTransfer(it) + } catch (exception: java.lang.Exception) { + onError?.accept( + StorageException( + "Something went wrong while attempting to cancel your " + + "AWS S3 Storage upload InputStream operation", + exception, + "See attached exception for more information and suggestions" + ) + ) + } + } + } + } + + override fun getTransferState(): TransferState { + return transferObserver?.transferState ?: TransferState.UNKNOWN + } + + override fun setOnSuccess(onSuccess: Consumer?) { + super.setOnSuccess(onSuccess) + val serviceKey = transferObserver?.key + if (transferState == TransferState.COMPLETED && serviceKey != null) { + onSuccess?.accept(StorageUploadInputStreamResult(serviceKey, serviceKey)) + } + } + + private inner class UploadTransferListener : TransferListener { + override fun onStateChanged(id: Int, state: TransferState, key: String) { + Amplify.Hub.publish( + HubChannel.STORAGE, + HubEvent.create(StorageChannelEventName.UPLOAD_STATE, state.name) + ) + when (state) { + TransferState.COMPLETED -> { + onSuccess?.accept(StorageUploadInputStreamResult(key, key)) + return + } + else -> {} + } + } + + override fun onProgressChanged(id: Int, bytesCurrent: Long, bytesTotal: Long) { + onProgress?.accept(StorageTransferProgress(bytesCurrent, bytesTotal)) + } + + override fun onError(id: Int, ex: Exception) { + Amplify.Hub.publish( + HubChannel.STORAGE, + HubEvent.create(StorageChannelEventName.UPLOAD_ERROR, ex) + ) + onError?.accept( + StorageException( + "Something went wrong with your AWS S3 Storage upload InputStream operation", + ex, + "See attached exception for more information and suggestions" + ) + ) + } + } +} diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageRemoveOperation.java b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageRemoveOperation.java index 7f9cb660e..d7fdc7324 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageRemoveOperation.java +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageRemoveOperation.java @@ -31,8 +31,8 @@ /** * An operation to remove a file from AWS S3. - * @deprecated Class should not be public and explicitly cast to. - * Internal usages are moving to AWSS3StorageRemoveOperationV2 + * @deprecated Class should not be public and explicitly cast to. Cast to StorageRemoveOperation. + * Internal usages are moving to AWSS3StoragePathRemoveOperation */ @Deprecated public final class AWSS3StorageRemoveOperation extends StorageRemoveOperation { diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageUploadFileOperation.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageUploadFileOperation.kt index e10a2d21f..535b361a9 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageUploadFileOperation.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageUploadFileOperation.kt @@ -40,8 +40,8 @@ import java.util.concurrent.ExecutorService * An operation to upload a file from AWS S3. */ @Deprecated( - "Class should not be public and explicitly cast to. " + - "Internal usages are moving to AWSS3StorageUploadFileOperationV2" + "Class should not be public and explicitly cast to. Cast to StorageUploadFileOperation" + + "Internal usages are moving to AWSS3StoragePathUploadFileOperation" ) class AWSS3StorageUploadFileOperation @JvmOverloads internal constructor( transferId: String, @@ -196,10 +196,9 @@ class AWSS3StorageUploadFileOperation @JvmOverloads internal constructor( override fun setOnSuccess(onSuccess: Consumer?) { super.setOnSuccess(onSuccess) - request?.let { - if (transferState == TransferState.COMPLETED) { - onSuccess?.accept(StorageUploadFileResult.fromKey(it.key)) - } + val serviceKey = transferObserver?.key + if (transferState == TransferState.COMPLETED && serviceKey != null) { + onSuccess?.accept(StorageUploadFileResult(serviceKey, serviceKey)) } } diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageUploadInputStreamOperation.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageUploadInputStreamOperation.kt index 24d334564..2806926b6 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageUploadInputStreamOperation.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageUploadInputStreamOperation.kt @@ -41,8 +41,8 @@ import java.util.concurrent.ExecutorService * An operation to upload an InputStream from AWS S3. */ @Deprecated( - "Class should not be public and explicitly cast to. " + - "Internal usages are moving to AWSS3StorageUploadInputStreamOperationV2" + "Class should not be public and explicitly cast to. Cast to StorageUploadInputStreamOperation." + + "Internal usages are moving to AWSS3StoragePathUploadInputStreamOperation" ) class AWSS3StorageUploadInputStreamOperation @JvmOverloads internal constructor( transferId: String, @@ -203,10 +203,9 @@ class AWSS3StorageUploadInputStreamOperation @JvmOverloads internal constructor( override fun setOnSuccess(onSuccess: Consumer?) { super.setOnSuccess(onSuccess) - request?.let { - if (transferState == TransferState.COMPLETED) { - onSuccess?.accept(StorageUploadInputStreamResult.fromKey(it.key)) - } + val serviceKey = transferObserver?.key + if (transferState == TransferState.COMPLETED && serviceKey != null) { + onSuccess?.accept(StorageUploadInputStreamResult(serviceKey, serviceKey)) } } diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/request/AWSS3StoragePathUploadRequest.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/request/AWSS3StoragePathUploadRequest.kt new file mode 100644 index 000000000..162e9d811 --- /dev/null +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/request/AWSS3StoragePathUploadRequest.kt @@ -0,0 +1,31 @@ +/* + * Copyright 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.amplifyframework.storage.s3.request + +import com.amplifyframework.storage.StoragePath +import com.amplifyframework.storage.s3.ServerSideEncryption + +/** + * Parameters to provide to S3 that describe a request to upload a + * file or input stream. + */ +internal data class AWSS3StoragePathUploadRequest( + val path: StoragePath, + val local: L, + val contentType: String?, + val serverSideEncryption: ServerSideEncryption, + val metadata: Map, + val useAccelerateEndpoint: Boolean +) diff --git a/aws-storage-s3/src/test/java/com/amplifyframework/storage/StoragePathTest.kt b/aws-storage-s3/src/test/java/com/amplifyframework/storage/StoragePathTest.kt new file mode 100644 index 000000000..38c3add8c --- /dev/null +++ b/aws-storage-s3/src/test/java/com/amplifyframework/storage/StoragePathTest.kt @@ -0,0 +1,25 @@ +package com.amplifyframework.storage + +import org.junit.Assert.assertEquals +import org.junit.Test + +class StoragePathTest { + + @Test + fun `string storage path`() { + val expectedString = "storage/path" + + val path = StoragePath.fromString(expectedString) as StringStoragePath + + assertEquals(expectedString, path.resolvePath()) + } + + @Test + fun `identity id storage path`() { + val expectedString = "photos/123/1.jpg" + + val path = StoragePath.fromIdentityId { "photos/$it/1.jpg" } as IdentityIdProvidedStoragePath + + assertEquals(expectedString, path.resolvePath("123")) + } +} diff --git a/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathDownloadFileOperationTest.kt b/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathDownloadFileOperationTest.kt new file mode 100644 index 000000000..e3e07c40c --- /dev/null +++ b/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathDownloadFileOperationTest.kt @@ -0,0 +1,227 @@ +/* + * Copyright 2022 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.amplifyframework.storage.s3.operation + +import com.amplifyframework.auth.AuthCredentialsProvider +import com.amplifyframework.core.Consumer +import com.amplifyframework.storage.StorageException +import com.amplifyframework.storage.StoragePath +import com.amplifyframework.storage.StoragePathValidationException +import com.amplifyframework.storage.s3.extensions.invalidStoragePathException +import com.amplifyframework.storage.s3.extensions.unsupportedStoragePathException +import com.amplifyframework.storage.s3.request.AWSS3StoragePathDownloadFileRequest +import com.amplifyframework.storage.s3.service.StorageService +import com.google.common.util.concurrent.MoreExecutors +import io.mockk.coEvery +import io.mockk.mockk +import io.mockk.verify +import java.io.File +import org.junit.Before +import org.junit.Test + +class AWSS3StoragePathDownloadFileOperationTest { + + private lateinit var awsS3StorageDownloadFileOperation: AWSS3StoragePathDownloadFileOperation + private lateinit var storageService: StorageService + private lateinit var authCredentialsProvider: AuthCredentialsProvider + + @Before + fun setup() { + storageService = mockk(relaxed = true) + authCredentialsProvider = mockk() + } + + @Test + fun `success string storage path`() { + // GIVEN + val path = StoragePath.fromString("public/123") + val tempFile = File.createTempFile("new", "file.tmp") + val expectedServiceKey = "public/123" + val request = AWSS3StoragePathDownloadFileRequest( + path, + tempFile, + false + ) + val onError = mockk>(relaxed = true) + awsS3StorageDownloadFileOperation = AWSS3StoragePathDownloadFileOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + {}, + {}, + onError + ) + + // WHEN + awsS3StorageDownloadFileOperation.start() + + // THEN + verify(exactly = 0) { onError.accept(any()) } + verify { + storageService.downloadToFile( + awsS3StorageDownloadFileOperation.transferId, + expectedServiceKey, + tempFile, + false + ) + } + } + + @Test + fun `success identityId storage path`() { + // GIVEN + coEvery { authCredentialsProvider.getIdentityId() } returns "123" + val path = StoragePath.fromIdentityId { "protected/$it/picture.jpg" } + val tempFile = File.createTempFile("new", "file.tmp") + val expectedServiceKey = "protected/123/picture.jpg" + val request = AWSS3StoragePathDownloadFileRequest( + path, + tempFile, + false + ) + val onError = mockk>(relaxed = true) + awsS3StorageDownloadFileOperation = AWSS3StoragePathDownloadFileOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + {}, + {}, + onError + ) + + // WHEN + awsS3StorageDownloadFileOperation.start() + + // THEN + verify(exactly = 0) { onError.accept(any()) } + verify { + storageService.downloadToFile( + awsS3StorageDownloadFileOperation.transferId, + expectedServiceKey, + tempFile, + false + ) + } + } + + @Test + fun `invalid storage path fails with invalid path`() { + // GIVEN + coEvery { authCredentialsProvider.getIdentityId() } returns "123" + val path = StoragePath.fromIdentityId { "/protected/$it/picture.jpg" } + val tempFile = File.createTempFile("new", "file.tmp") + val request = AWSS3StoragePathDownloadFileRequest( + path, + tempFile, + false + ) + val onError = mockk>(relaxed = true) + awsS3StorageDownloadFileOperation = AWSS3StoragePathDownloadFileOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + {}, + {}, + onError + ) + + // WHEN + awsS3StorageDownloadFileOperation.start() + + // THEN + verify { onError.accept(StoragePathValidationException.invalidStoragePathException()) } + verify(exactly = 0) { + storageService.downloadToFile(any(), any(), any(), any()) + } + } + + @Test + fun `invalid storage path fails with failed identityId resolution`() { + // GIVEN + val expectedException = Exception("test") + coEvery { authCredentialsProvider.getIdentityId() } throws expectedException + val path = StoragePath.fromIdentityId { "protected/$it/picture.jpg" } + val tempFile = File.createTempFile("new", "file.tmp") + val request = AWSS3StoragePathDownloadFileRequest( + path, + tempFile, + false + ) + val onError = mockk>(relaxed = true) + awsS3StorageDownloadFileOperation = AWSS3StoragePathDownloadFileOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + {}, + {}, + onError + ) + + // WHEN + awsS3StorageDownloadFileOperation.start() + + // THEN + verify { + onError.accept( + StorageException( + "Failed to fetch identity ID", + expectedException, + "See included exception for more details and suggestions to fix." + ) + ) + } + verify(exactly = 0) { + storageService.downloadToFile(any(), any(), any(), any()) + } + } + + @Test + fun `invalid storage path fails with unsupported storage path type`() { + // GIVEN + val path = UnsupportedStoragePath() + val tempFile = File.createTempFile("new", "file.tmp") + val request = AWSS3StoragePathDownloadFileRequest( + path, + tempFile, + false + ) + val onError = mockk>(relaxed = true) + awsS3StorageDownloadFileOperation = AWSS3StoragePathDownloadFileOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + {}, + {}, + onError + ) + + // WHEN + awsS3StorageDownloadFileOperation.start() + + // THEN + verify { onError.accept(StoragePathValidationException.unsupportedStoragePathException()) } + verify(exactly = 0) { + storageService.downloadToFile(any(), any(), any(), any()) + } + } + + class UnsupportedStoragePath : StoragePath() +} diff --git a/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathUploadFileOperationTest.kt b/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathUploadFileOperationTest.kt new file mode 100644 index 000000000..59ee395c2 --- /dev/null +++ b/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathUploadFileOperationTest.kt @@ -0,0 +1,245 @@ +/* + * Copyright 2022 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.amplifyframework.storage.s3.operation + +import com.amplifyframework.auth.AuthCredentialsProvider +import com.amplifyframework.core.Consumer +import com.amplifyframework.storage.StorageException +import com.amplifyframework.storage.StoragePath +import com.amplifyframework.storage.StoragePathValidationException +import com.amplifyframework.storage.s3.ServerSideEncryption +import com.amplifyframework.storage.s3.extensions.invalidStoragePathException +import com.amplifyframework.storage.s3.extensions.unsupportedStoragePathException +import com.amplifyframework.storage.s3.request.AWSS3StoragePathUploadRequest +import com.amplifyframework.storage.s3.service.StorageService +import com.google.common.util.concurrent.MoreExecutors +import io.mockk.coEvery +import io.mockk.mockk +import io.mockk.verify +import java.io.File +import org.junit.Before +import org.junit.Test + +class AWSS3StoragePathUploadFileOperationTest { + + private lateinit var awsS3StorageUploadFileOperation: AWSS3StoragePathUploadFileOperation + private lateinit var storageService: StorageService + private lateinit var authCredentialsProvider: AuthCredentialsProvider + + @Before + fun setup() { + storageService = mockk(relaxed = true) + authCredentialsProvider = mockk() + } + + @Test + fun `success string storage path`() { + // GIVEN + val path = StoragePath.fromString("public/123") + val tempFile = File.createTempFile("new", "file.tmp") + val expectedServiceKey = "public/123" + val request = AWSS3StoragePathUploadRequest( + path, + tempFile, + "/image", + ServerSideEncryption.NONE, + emptyMap(), + false + ) + val onError = mockk>(relaxed = true) + awsS3StorageUploadFileOperation = AWSS3StoragePathUploadFileOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + {}, + {}, + onError + ) + + // WHEN + awsS3StorageUploadFileOperation.start() + + // THEN + verify(exactly = 0) { onError.accept(any()) } + verify { + storageService.uploadFile( + awsS3StorageUploadFileOperation.transferId, + expectedServiceKey, + tempFile, + any(), + false + ) + } + } + + @Test + fun `success identityId storage path`() { + // GIVEN + coEvery { authCredentialsProvider.getIdentityId() } returns "123" + val path = StoragePath.fromIdentityId { "protected/$it/picture.jpg" } + val tempFile = File.createTempFile("new", "file.tmp") + val expectedServiceKey = "protected/123/picture.jpg" + val request = AWSS3StoragePathUploadRequest( + path, + tempFile, + "/image", + ServerSideEncryption.NONE, + emptyMap(), + false + ) + val onError = mockk>(relaxed = true) + awsS3StorageUploadFileOperation = AWSS3StoragePathUploadFileOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + {}, + {}, + onError + ) + + // WHEN + awsS3StorageUploadFileOperation.start() + + // THEN + verify(exactly = 0) { onError.accept(any()) } + verify { + storageService.uploadFile( + awsS3StorageUploadFileOperation.transferId, + expectedServiceKey, + tempFile, + any(), + false + ) + } + } + + @Test + fun `invalid storage path fails with invalid path`() { + // GIVEN + coEvery { authCredentialsProvider.getIdentityId() } returns "123" + val path = StoragePath.fromIdentityId { "/protected/$it/picture.jpg" } + val tempFile = File.createTempFile("new", "file.tmp") + val request = AWSS3StoragePathUploadRequest( + path, + tempFile, + "/image", + ServerSideEncryption.NONE, + emptyMap(), + false + ) + val onError = mockk>(relaxed = true) + awsS3StorageUploadFileOperation = AWSS3StoragePathUploadFileOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + {}, + {}, + onError + ) + + // WHEN + awsS3StorageUploadFileOperation.start() + + // THEN + verify { onError.accept(StoragePathValidationException.invalidStoragePathException()) } + verify(exactly = 0) { + storageService.uploadFile(any(), any(), any(), any(), any()) + } + } + + @Test + fun `invalid storage path fails with failed identityId resolution`() { + // GIVEN + val expectedException = Exception("test") + coEvery { authCredentialsProvider.getIdentityId() } throws expectedException + val path = StoragePath.fromIdentityId { "protected/$it/picture.jpg" } + val tempFile = File.createTempFile("new", "file.tmp") + val request = AWSS3StoragePathUploadRequest( + path, + tempFile, + "/image", + ServerSideEncryption.NONE, + emptyMap(), + false + ) + val onError = mockk>(relaxed = true) + awsS3StorageUploadFileOperation = AWSS3StoragePathUploadFileOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + {}, + {}, + onError + ) + + // WHEN + awsS3StorageUploadFileOperation.start() + + // THEN + verify { + onError.accept( + StorageException( + "Failed to fetch identity ID", + expectedException, + "See included exception for more details and suggestions to fix." + ) + ) + } + verify(exactly = 0) { + storageService.uploadFile(any(), any(), any(), any(), any()) + } + } + + @Test + fun `invalid storage path fails with unsupported storage path type`() { + // GIVEN + val path = UnsupportedStoragePath() + val tempFile = File.createTempFile("new", "file.tmp") + val request = AWSS3StoragePathUploadRequest( + path, + tempFile, + "/image", + ServerSideEncryption.NONE, + emptyMap(), + false + ) + val onError = mockk>(relaxed = true) + awsS3StorageUploadFileOperation = AWSS3StoragePathUploadFileOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + {}, + {}, + onError + ) + + // WHEN + awsS3StorageUploadFileOperation.start() + + // THEN + verify { onError.accept(StoragePathValidationException.unsupportedStoragePathException()) } + verify(exactly = 0) { + storageService.uploadFile(any(), any(), any(), any(), any()) + } + } + + class UnsupportedStoragePath : StoragePath() +} diff --git a/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathUploadInputStreamOperationTest.kt b/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathUploadInputStreamOperationTest.kt new file mode 100644 index 000000000..43eb85e44 --- /dev/null +++ b/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathUploadInputStreamOperationTest.kt @@ -0,0 +1,246 @@ +/* + * Copyright 2022 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.amplifyframework.storage.s3.operation + +import com.amplifyframework.auth.AuthCredentialsProvider +import com.amplifyframework.core.Consumer +import com.amplifyframework.storage.StorageException +import com.amplifyframework.storage.StoragePath +import com.amplifyframework.storage.StoragePathValidationException +import com.amplifyframework.storage.s3.ServerSideEncryption +import com.amplifyframework.storage.s3.extensions.invalidStoragePathException +import com.amplifyframework.storage.s3.extensions.unsupportedStoragePathException +import com.amplifyframework.storage.s3.request.AWSS3StoragePathUploadRequest +import com.amplifyframework.storage.s3.service.StorageService +import com.google.common.util.concurrent.MoreExecutors +import io.mockk.coEvery +import io.mockk.mockk +import io.mockk.verify +import java.io.File +import java.io.InputStream +import org.junit.Before +import org.junit.Test + +class AWSS3StoragePathUploadInputStreamOperationTest { + + private lateinit var awsS3StorageUploadInputStreamOperation: AWSS3StoragePathUploadInputStreamOperation + private lateinit var storageService: StorageService + private lateinit var authCredentialsProvider: AuthCredentialsProvider + + @Before + fun setup() { + storageService = mockk(relaxed = true) + authCredentialsProvider = mockk() + } + + @Test + fun `success string storage path`() { + // GIVEN + val path = StoragePath.fromString("public/123") + val inputStream = File.createTempFile("new", "file.tmp").inputStream() + val expectedServiceKey = "public/123" + val request = AWSS3StoragePathUploadRequest( + path, + inputStream, + "/image", + ServerSideEncryption.NONE, + emptyMap(), + false + ) + val onError = mockk>(relaxed = true) + awsS3StorageUploadInputStreamOperation = AWSS3StoragePathUploadInputStreamOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + {}, + {}, + onError + ) + + // WHEN + awsS3StorageUploadInputStreamOperation.start() + + // THEN + verify(exactly = 0) { onError.accept(any()) } + verify { + storageService.uploadInputStream( + awsS3StorageUploadInputStreamOperation.transferId, + expectedServiceKey, + inputStream, + any(), + false + ) + } + } + + @Test + fun `success identityId storage path`() { + // GIVEN + coEvery { authCredentialsProvider.getIdentityId() } returns "123" + val path = StoragePath.fromIdentityId { "protected/$it/picture.jpg" } + val inputStream = File.createTempFile("new", "file.tmp").inputStream() + val expectedServiceKey = "protected/123/picture.jpg" + val request = AWSS3StoragePathUploadRequest( + path, + inputStream, + "/image", + ServerSideEncryption.NONE, + emptyMap(), + false + ) + val onError = mockk>(relaxed = true) + awsS3StorageUploadInputStreamOperation = AWSS3StoragePathUploadInputStreamOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + {}, + {}, + onError + ) + + // WHEN + awsS3StorageUploadInputStreamOperation.start() + + // THEN + verify(exactly = 0) { onError.accept(any()) } + verify { + storageService.uploadInputStream( + awsS3StorageUploadInputStreamOperation.transferId, + expectedServiceKey, + inputStream, + any(), + false + ) + } + } + + @Test + fun `invalid storage path fails with invalid path`() { + // GIVEN + coEvery { authCredentialsProvider.getIdentityId() } returns "123" + val path = StoragePath.fromIdentityId { "/protected/$it/picture.jpg" } + val inputStream = File.createTempFile("new", "file.tmp").inputStream() + val request = AWSS3StoragePathUploadRequest( + path, + inputStream, + "/image", + ServerSideEncryption.NONE, + emptyMap(), + false + ) + val onError = mockk>(relaxed = true) + awsS3StorageUploadInputStreamOperation = AWSS3StoragePathUploadInputStreamOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + {}, + {}, + onError + ) + + // WHEN + awsS3StorageUploadInputStreamOperation.start() + + // THEN + verify { onError.accept(StoragePathValidationException.invalidStoragePathException()) } + verify(exactly = 0) { + storageService.uploadInputStream(any(), any(), any(), any(), any()) + } + } + + @Test + fun `invalid storage path fails with failed identityId resolution`() { + // GIVEN + val expectedException = Exception("test") + coEvery { authCredentialsProvider.getIdentityId() } throws expectedException + val path = StoragePath.fromIdentityId { "protected/$it/picture.jpg" } + val inputStream = File.createTempFile("new", "file.tmp").inputStream() + val request = AWSS3StoragePathUploadRequest( + path, + inputStream, + "/image", + ServerSideEncryption.NONE, + emptyMap(), + false + ) + val onError = mockk>(relaxed = true) + awsS3StorageUploadInputStreamOperation = AWSS3StoragePathUploadInputStreamOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + {}, + {}, + onError + ) + + // WHEN + awsS3StorageUploadInputStreamOperation.start() + + // THEN + verify { + onError.accept( + StorageException( + "Failed to fetch identity ID", + expectedException, + "See included exception for more details and suggestions to fix." + ) + ) + } + verify(exactly = 0) { + storageService.uploadInputStream(any(), any(), any(), any(), any()) + } + } + + @Test + fun `invalid storage path fails with unsupported storage path type`() { + // GIVEN + val path = UnsupportedStoragePath() + val inputStream = File.createTempFile("new", "file.tmp").inputStream() + val request = AWSS3StoragePathUploadRequest( + path, + inputStream, + "/image", + ServerSideEncryption.NONE, + emptyMap(), + false + ) + val onError = mockk>(relaxed = true) + awsS3StorageUploadInputStreamOperation = AWSS3StoragePathUploadInputStreamOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + {}, + {}, + onError + ) + + // WHEN + awsS3StorageUploadInputStreamOperation.start() + + // THEN + verify { onError.accept(StoragePathValidationException.unsupportedStoragePathException()) } + verify(exactly = 0) { + storageService.uploadInputStream(any(), any(), any(), any(), any()) + } + } + + class UnsupportedStoragePath : StoragePath() +} diff --git a/core-kotlin/src/test/java/com/amplifyframework/kotlin/storage/KotlinStorageFacadeTest.kt b/core-kotlin/src/test/java/com/amplifyframework/kotlin/storage/KotlinStorageFacadeTest.kt index 87e12c2f9..24cde54b7 100644 --- a/core-kotlin/src/test/java/com/amplifyframework/kotlin/storage/KotlinStorageFacadeTest.kt +++ b/core-kotlin/src/test/java/com/amplifyframework/kotlin/storage/KotlinStorageFacadeTest.kt @@ -82,7 +82,7 @@ class KotlinStorageFacadeTest { @Test fun getUrlStoragePathSucceeds() = runBlocking { - val forRemotePath = StoragePath.fromString("/delete_me.png") + val forRemotePath = StoragePath.fromString("delete_me.png") val result = StorageGetUrlResult.fromUrl(URL("https://s3.amazon.biz/file.png")) every { delegate.getUrl(eq(forRemotePath), any(), any(), any()) @@ -116,7 +116,7 @@ class KotlinStorageFacadeTest { @Test(expected = StorageException::class) fun getUrlStoragePathThrows(): Unit = runBlocking { - val forRemotePath = StoragePath.fromString("/delete_me.png") + val forRemotePath = StoragePath.fromString("delete_me.png") val error = StorageException("uh", "oh") every { delegate.getUrl(eq(forRemotePath), any(), any(), any()) @@ -172,7 +172,7 @@ class KotlinStorageFacadeTest { @Test fun downloadFileStoragePathSucceeds(): Unit = runBlocking { - val fromRemotePath = StoragePath.fromString("/kool-pic.png") + val fromRemotePath = StoragePath.fromString("kool-pic.png") val toLocalFile = File("/local/path/kool-pic.png") val transferId = UUID.randomUUID().toString() val progressEvents = (0L until 101 step 50) @@ -212,7 +212,7 @@ class KotlinStorageFacadeTest { */ @Test fun performActionsOnDownloadFile(): Unit = runBlocking { - val fromRemotePath = StoragePath.fromString("/kool-pic.png") + val fromRemotePath = StoragePath.fromString("kool-pic.png") val toLocalFile = File("/local/path/kool-pic.png") val transferId = UUID.randomUUID().toString() @@ -268,7 +268,7 @@ class KotlinStorageFacadeTest { @Test(expected = StorageException::class) fun downloadFileStoragePathThrows(): Unit = runBlocking { - val remotePath = StoragePath.fromString("/kool-pic.png") + val remotePath = StoragePath.fromString("kool-pic.png") val toLocalFile = File("/local/path/kool-pic.png") val error = StorageException("uh", "oh") val transferId = UUID.randomUUID().toString() @@ -396,7 +396,7 @@ class KotlinStorageFacadeTest { @Test(expected = StorageException::class) fun uploadFileStoragePathThrows(): Unit = runBlocking { - val remotePath = StoragePath.fromString("/kool-pic.png") + val remotePath = StoragePath.fromString("kool-pic.png") val fromLocalFile = File("/local/path/kool-pic.png") val error = StorageException("uh", "oh") val transferId = UUID.randomUUID().toString() @@ -422,7 +422,7 @@ class KotlinStorageFacadeTest { */ @Test fun performActionOnUploadFileSucceeds() = runBlocking { - val remotePath = StoragePath.fromString("/kool-pic.png") + val remotePath = StoragePath.fromString("kool-pic.png") val fromLocalFile = File("/local/path/kool-pic.png") val transferId = UUID.randomUUID().toString() val cancelable = mockk>() @@ -554,7 +554,7 @@ class KotlinStorageFacadeTest { @Test(expected = StorageException::class) fun uploadInputStreamStoragePathThrows(): Unit = runBlocking { - val remotePath = StoragePath.fromString("/kool-pic.png") + val remotePath = StoragePath.fromString("kool-pic.png") val fromStream = mockk() val error = StorageException("uh", "oh") val transferId = UUID.randomUUID().toString() @@ -581,7 +581,7 @@ class KotlinStorageFacadeTest { */ @Test fun performActionOnUploadInputStream() = runBlocking { - val remotePath = StoragePath.fromString("/kool-pic.png") + val remotePath = StoragePath.fromString("kool-pic.png") val fromStream = mockk() val transferId = UUID.randomUUID().toString() val cancelable = mockk>() @@ -667,7 +667,7 @@ class KotlinStorageFacadeTest { @Test(expected = StorageException::class) fun removeStoragePathThrows(): Unit = runBlocking { - val path = StoragePath.fromString("/delete_me.png") + val path = StoragePath.fromString("delete_me.png") val error = StorageException("uh", "oh") every { delegate.remove(eq(path), any(), any(), any()) @@ -704,7 +704,7 @@ class KotlinStorageFacadeTest { @Test fun listStoragePathSucceeds() = runBlocking { - val path = StoragePath.fromString("/beach/photos") + val path = StoragePath.fromString("beach/photos") val item = StorageItem("/me_at_beach.png", "/me_at_beach.png", 100L, Date(), "eTag", "props") val result = StorageListResult.fromItems(listOf(item), null) every { @@ -741,7 +741,7 @@ class KotlinStorageFacadeTest { @Test(expected = StorageException::class) fun listStoragePathThrows(): Unit = runBlocking { - val path = StoragePath.fromString("/beach/photos") + val path = StoragePath.fromString("beach/photos") val error = StorageException("uh", "oh") every { delegate.list(eq(path), any(), any(), any()) diff --git a/core/src/main/java/com/amplifyframework/storage/StoragePath.kt b/core/src/main/java/com/amplifyframework/storage/StoragePath.kt index 066fc4bec..a12d350d0 100644 --- a/core/src/main/java/com/amplifyframework/storage/StoragePath.kt +++ b/core/src/main/java/com/amplifyframework/storage/StoragePath.kt @@ -2,26 +2,53 @@ package com.amplifyframework.storage import com.amplifyframework.annotations.InternalAmplifyApi +/** + * Resolves identityId to be injected into String for StoragePath. + */ typealias IdentityIdPathResolver = (identityId: String) -> String +/** + + * StoragePath is a wrapper class that provides the ability to resolve transfer paths at the time of transfer. + */ abstract class StoragePath { companion object { + + /** + * Generate a StoragePath from String. + * + * @param path A full transfer path in String format + * @return path + */ @JvmStatic fun fromString(path: String): StoragePath = StringStoragePath(path) + /** + * Generate a StoragePath from IdentityIdPathResolver. + * + * @param identityIdPathResolver resolves the StoragePath with the ability to inject identityId's at the time + * of transfer. + * @return path + */ @JvmStatic fun fromIdentityId(identityIdPathResolver: IdentityIdPathResolver): StoragePath = IdentityIdProvidedStoragePath(identityIdPathResolver) } } +/** + * StoragePath that was created with the full String path. + */ data class StringStoragePath internal constructor(private val path: String) : StoragePath() { @InternalAmplifyApi fun resolvePath() = path } +/** + * StoragePath that is resolved by providing the identityId. + */ data class IdentityIdProvidedStoragePath internal constructor( private val identityIdPathResolver: IdentityIdPathResolver ) : StoragePath() { diff --git a/core/src/main/java/com/amplifyframework/storage/StoragePathValidationException.kt b/core/src/main/java/com/amplifyframework/storage/StoragePathValidationException.kt index cd05f93b4..2c2bcf951 100644 --- a/core/src/main/java/com/amplifyframework/storage/StoragePathValidationException.kt +++ b/core/src/main/java/com/amplifyframework/storage/StoragePathValidationException.kt @@ -3,6 +3,9 @@ package com.amplifyframework.storage import com.amplifyframework.AmplifyException import com.amplifyframework.annotations.InternalAmplifyApi +/** + * Exception thrown when the StoragePath is not valid. + */ class StoragePathValidationException @InternalAmplifyApi constructor( message: String, recoverySuggestion: String diff --git a/core/src/main/java/com/amplifyframework/storage/result/StorageUploadFileResult.java b/core/src/main/java/com/amplifyframework/storage/result/StorageUploadFileResult.java index 7e15993f7..9004a8abe 100644 --- a/core/src/main/java/com/amplifyframework/storage/result/StorageUploadFileResult.java +++ b/core/src/main/java/com/amplifyframework/storage/result/StorageUploadFileResult.java @@ -35,7 +35,7 @@ public final class StorageUploadFileResult extends StorageUploadResult { * @param key Key for an item that was uploaded successfully */ @InternalAmplifyApi - public StorageUploadFileResult(String path, String key) { + public StorageUploadFileResult(@NonNull String path, @NonNull String key) { super(path, key); } diff --git a/testutils/src/main/java/com/amplifyframework/testutils/sync/SynchronousStorage.java b/testutils/src/main/java/com/amplifyframework/testutils/sync/SynchronousStorage.java index a8bc4942a..d278c27f7 100644 --- a/testutils/src/main/java/com/amplifyframework/testutils/sync/SynchronousStorage.java +++ b/testutils/src/main/java/com/amplifyframework/testutils/sync/SynchronousStorage.java @@ -21,6 +21,7 @@ import com.amplifyframework.storage.StorageCategory; import com.amplifyframework.storage.StorageCategoryBehavior; import com.amplifyframework.storage.StorageException; +import com.amplifyframework.storage.StoragePath; import com.amplifyframework.storage.options.StorageDownloadFileOptions; import com.amplifyframework.storage.options.StorageListOptions; import com.amplifyframework.storage.options.StoragePagedListOptions; @@ -117,6 +118,47 @@ public StorageDownloadFileResult downloadFile( ); } + /** + * Download a file synchronously and return the result of operation. + * + * @param path Path of file to download + * @param local File to save downloaded object to + * @param options Download options + * @return Download operation result containing downloaded file + * @throws StorageException if download fails or times out + */ + @NonNull + public StorageDownloadFileResult downloadFile( + @NonNull StoragePath path, + @NonNull File local, + @NonNull StorageDownloadFileOptions options + ) throws StorageException { + return downloadFile(path, local, options, STORAGE_OPERATION_TIMEOUT_MS); + } + + /** + * Download a file synchronously and return the result of operation. + * + * @param path Path of file to download + * @param local File to save downloaded object to + * @param options Download options + * @param timeoutMs Custom time-out duration in milliseconds + * @return Download operation result containing downloaded file + * @throws StorageException if download fails or times out + */ + @NonNull + @SuppressWarnings("deprecation") + public StorageDownloadFileResult downloadFile( + @NonNull StoragePath path, + @NonNull File local, + @NonNull StorageDownloadFileOptions options, + long timeoutMs + ) throws StorageException { + return Await.result(timeoutMs, (onResult, onError) -> + asyncDelegate.downloadFile(path, local, options, onResult, onError) + ); + } + /** * Upload a file synchronously and return the result of operation. * @@ -159,6 +201,47 @@ public StorageUploadFileResult uploadFile( ); } + /** + * Upload a file synchronously and return the result of operation. + * + * @param path Path of file on service + * @param local File to upload + * @param options Upload options + * @return Upload operation result + * @throws StorageException if upload fails or times out + */ + @NonNull + @SuppressWarnings("deprecation") + public StorageUploadFileResult uploadFile( + @NonNull StoragePath path, + @NonNull File local, + @NonNull StorageUploadFileOptions options + ) throws StorageException { + return uploadFile(path, local, options, STORAGE_OPERATION_TIMEOUT_MS); + } + + /** + * Upload a file synchronously and return the result of operation. + * + * @param path Path of file on service + * @param local File to upload + * @param options Upload options + * @param timeoutMs Custom time-out duration in milliseconds + * @return Upload operation result + * @throws StorageException if upload fails or times out + */ + @NonNull + public StorageUploadFileResult uploadFile( + @NonNull StoragePath path, + @NonNull File local, + @NonNull StorageUploadFileOptions options, + long timeoutMs + ) throws StorageException { + return Await.result(timeoutMs, (onResult, onError) -> + asyncDelegate.uploadFile(path, local, options, onResult, onError) + ); + } + /** * Upload an InputStream synchronously and return the result of operation. *