Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(storage): add delimiter support #2871

Merged
merged 11 commits into from
Jul 24, 2024
4 changes: 4 additions & 0 deletions aws-storage-s3/api/aws-storage-s3.api
Original file line number Diff line number Diff line change
Expand Up @@ -297,10 +297,12 @@ public final class com/amplifyframework/storage/s3/request/AWSS3StorageGetPresig
public final class com/amplifyframework/storage/s3/request/AWSS3StorageListRequest {
public fun <init> (Ljava/lang/String;Lcom/amplifyframework/storage/StorageAccessLevel;Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Lcom/amplifyframework/storage/StorageAccessLevel;Ljava/lang/String;ILjava/lang/String;)V
public fun <init> (Ljava/lang/String;Lcom/amplifyframework/storage/StorageAccessLevel;Ljava/lang/String;ILjava/lang/String;Lcom/amplifyframework/storage/options/SubpathStrategy;)V
public fun getAccessLevel ()Lcom/amplifyframework/storage/StorageAccessLevel;
public fun getNextToken ()Ljava/lang/String;
public fun getPageSize ()I
public fun getPath ()Ljava/lang/String;
public fun getSubpathStrategy ()Lcom/amplifyframework/storage/options/SubpathStrategy;
public fun getTargetIdentityId ()Ljava/lang/String;
}

Expand Down Expand Up @@ -331,6 +333,8 @@ public abstract interface class com/amplifyframework/storage/s3/service/StorageS
public abstract fun getTransfer (Ljava/lang/String;)Lcom/amplifyframework/storage/s3/transfer/TransferRecord;
public abstract fun listFiles (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List;
public abstract fun listFiles (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;)Lcom/amplifyframework/storage/result/StorageListResult;
public abstract fun listFiles (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Lcom/amplifyframework/storage/options/SubpathStrategy;)Lcom/amplifyframework/storage/result/StorageListResult;
mattcreaser marked this conversation as resolved.
Show resolved Hide resolved
public abstract fun listFiles (Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/storage/options/SubpathStrategy;)Lcom/amplifyframework/storage/result/StorageListResult;
public abstract fun pauseTransfer (Lcom/amplifyframework/storage/s3/transfer/TransferObserver;)V
public abstract fun resumeTransfer (Lcom/amplifyframework/storage/s3/transfer/TransferObserver;)V
public abstract fun uploadFile (Ljava/lang/String;Ljava/lang/String;Ljava/io/File;Lcom/amplifyframework/storage/ObjectMetadata;Z)Lcom/amplifyframework/storage/s3/transfer/TransferObserver;
Expand Down
1 change: 1 addition & 0 deletions aws-storage-s3/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ dependencies {
androidTestImplementation(libs.test.androidx.runner)
androidTestImplementation(libs.test.androidx.junit)
androidTestImplementation(libs.test.androidx.workmanager)
androidTestImplementation(libs.test.kotest.assertions)
androidTestImplementation(project(":aws-storage-s3"))

androidTestUtil(libs.test.androidx.orchestrator)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*
* Copyright 2023 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.cognito.AWSCognitoAuthPlugin
import com.amplifyframework.storage.StorageCategory
import com.amplifyframework.storage.StoragePath
import com.amplifyframework.storage.options.StorageRemoveOptions
import com.amplifyframework.storage.options.StorageUploadFileOptions
import com.amplifyframework.storage.options.SubpathStrategy
import com.amplifyframework.storage.s3.options.AWSS3StoragePagedListOptions
import com.amplifyframework.storage.s3.test.R
import com.amplifyframework.storage.s3.util.WorkmanagerTestUtils
import com.amplifyframework.testutils.random.RandomTempFile
import com.amplifyframework.testutils.sync.SynchronousAuth
import com.amplifyframework.testutils.sync.SynchronousStorage
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.ints.shouldBeExactly
import io.kotest.matchers.nulls.shouldBeNull
import java.io.File
import org.junit.After
import org.junit.BeforeClass
import org.junit.Test

/**
* Integration tests for using SubpathStrategy with Storage List API
*/
class AWSS3StorageSubPathStrategyListTest {
companion object {
private const val SMALL_FILE_SIZE = 100L
private const val FIRST_FILE_NAME = "01"
private const val SECOND_FILE_NAME = "02"
private const val THIRD_FILE_NAME = "03"
private const val FOURTH_FILE_NAME = "04"
private const val FIFTH_FILE_NAME = "05"
private const val CUSTOM_FILE_NAME = "custom"
private const val FIRST_FILE_STRING_PATH = "public/photos/2023/$FIRST_FILE_NAME"
private val FIRST_FILE_PATH = StoragePath.fromString(FIRST_FILE_STRING_PATH)
private const val SECOND_FILE_STRING_PATH = "public/photos/2023/$SECOND_FILE_NAME"
private val SECOND_FILE_PATH = StoragePath.fromString(SECOND_FILE_STRING_PATH)
private const val THIRD_FILE_STRING_PATH = "public/photos/2024/$THIRD_FILE_NAME"
private val THIRD_FILE_PATH = StoragePath.fromString(THIRD_FILE_STRING_PATH)
private const val FOURTH_FILE_STRING_PATH = "public/photos/2024/$FOURTH_FILE_NAME"
private val FOURTH_FILE_PATH = StoragePath.fromString(FOURTH_FILE_STRING_PATH)
private const val FIFTH_FILE_STRING_PATH = "public/photos/$FIFTH_FILE_NAME"
private val FIFTH_FILE_PATH = StoragePath.fromString(FIFTH_FILE_STRING_PATH)
private const val CUSTOM_FILE_STRING_PATH = "public/photos/202$/$CUSTOM_FILE_NAME"
private val CUSTOM_FILE_PATH = StoragePath.fromString(CUSTOM_FILE_STRING_PATH)

lateinit var storageCategory: StorageCategory
lateinit var synchronousStorage: SynchronousStorage
lateinit var synchronousAuth: SynchronousAuth
private lateinit var first: File
private lateinit var second: File
private lateinit var third: File
private lateinit var fourth: File
private lateinit var fifth: File
private lateinit var customFile: File

/**
* Initialize mobile client and configure the storage.
* Upload the test files ahead of time.
*/
@JvmStatic
@BeforeClass
fun setUpOnce() {
val context = ApplicationProvider.getApplicationContext<Context>()
WorkmanagerTestUtils.initializeWorkmanagerTestUtil(context)

synchronousAuth = SynchronousAuth.delegatingToCognito(context, AWSCognitoAuthPlugin())

// Get a handle to storage
storageCategory = TestStorageCategory.create(context, R.raw.amplifyconfiguration)
synchronousStorage = SynchronousStorage.delegatingTo(storageCategory)

// Upload test files
first = RandomTempFile(FIRST_FILE_NAME, SMALL_FILE_SIZE)
synchronousStorage.uploadFile(FIRST_FILE_PATH, first, StorageUploadFileOptions.defaultInstance())
second = RandomTempFile(SECOND_FILE_NAME, SMALL_FILE_SIZE)
synchronousStorage.uploadFile(SECOND_FILE_PATH, second, StorageUploadFileOptions.defaultInstance())
third = RandomTempFile(THIRD_FILE_NAME, SMALL_FILE_SIZE)
synchronousStorage.uploadFile(THIRD_FILE_PATH, third, StorageUploadFileOptions.defaultInstance())
fourth = RandomTempFile(FOURTH_FILE_NAME, SMALL_FILE_SIZE)
synchronousStorage.uploadFile(FOURTH_FILE_PATH, fourth, StorageUploadFileOptions.defaultInstance())
fifth = RandomTempFile(FIFTH_FILE_NAME, SMALL_FILE_SIZE)
synchronousStorage.uploadFile(FIFTH_FILE_PATH, fifth, StorageUploadFileOptions.defaultInstance())

customFile = RandomTempFile(CUSTOM_FILE_NAME, SMALL_FILE_SIZE)
synchronousStorage.uploadFile(CUSTOM_FILE_PATH, customFile, StorageUploadFileOptions.defaultInstance())
}
}

@After
fun tearDown() {
synchronousStorage.remove("photos/2023/$FIRST_FILE_NAME", StorageRemoveOptions.defaultInstance())
synchronousStorage.remove("photos/2023/$SECOND_FILE_NAME", StorageRemoveOptions.defaultInstance())
synchronousStorage.remove("photos/2024/$THIRD_FILE_NAME", StorageRemoveOptions.defaultInstance())
synchronousStorage.remove("photos/2024/$FOURTH_FILE_NAME", StorageRemoveOptions.defaultInstance())
synchronousStorage.remove("photos/$FIFTH_FILE_NAME", StorageRemoveOptions.defaultInstance())
synchronousStorage.remove("photos/$CUSTOM_FILE_NAME", StorageRemoveOptions.defaultInstance())
}

@Test
fun testListWithIncludeStrategyAndStoragePath() {
val path = StoragePath.fromString("public/photos/")
val options = AWSS3StoragePagedListOptions
.builder()
.setPageSize(10)
.setSubpathStrategy(SubpathStrategy.Include)
.build()

val result = synchronousStorage.list(path, options)

result.items.size shouldBeExactly(6)
result.items.mapNotNull { it.path } shouldContainExactly listOf(
"public/photos/05",
"public/photos/202$/custom",
"public/photos/2023/01",
"public/photos/2023/02",
"public/photos/2024/03",
"public/photos/2024/04"
)
}

@Test
fun testListWithExcludeStrategyAndStoragePath() {
val options = AWSS3StoragePagedListOptions
.builder()
.setPageSize(10)
.setSubpathStrategy(SubpathStrategy.Exclude())
.build()

var result = synchronousStorage.list(StoragePath.fromString("public/photos/"), options)

result.items.size shouldBeExactly(1)
result.items.mapNotNull { it.path } shouldContainExactly listOf("public/photos/05")

result.excludedSubpaths.size shouldBeExactly(3)
result.excludedSubpaths shouldContainExactly listOf(
"public/photos/202$/",
"public/photos/2023/",
"public/photos/2024/"
)

result = synchronousStorage.list(StoragePath.fromString("public/photos/2023/"), options)

result.items.size shouldBeExactly(2)
result.items.mapNotNull { it.path } shouldContainExactly listOf(
"public/photos/2023/01",
"public/photos/2023/02"
)

result.excludedSubpaths.shouldBeNull()
}

@Test
fun testListWithExcludeCustomDelimiterStrategyAndStoragePath() {
val options = AWSS3StoragePagedListOptions
.builder()
.setPageSize(10)
.setSubpathStrategy(SubpathStrategy.Exclude("$"))
.build()

var result = synchronousStorage.list(StoragePath.fromString("public/photos/"), options)

result.items.size shouldBeExactly(5)
result.items.mapNotNull { it.path } shouldContainExactly listOf(
"public/photos/05",
"public/photos/2023/01",
"public/photos/2023/02",
"public/photos/2024/03",
"public/photos/2024/04"
)

result.excludedSubpaths.size shouldBeExactly(1)
result.excludedSubpaths shouldContainExactly listOf("public/photos/202$")

result = synchronousStorage.list(StoragePath.fromString("public/photos/2023/"), options)

result.items.size shouldBeExactly(2)
result.items.mapNotNull { it.path } shouldContainExactly listOf(
"public/photos/2023/01",
"public/photos/2023/02",
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -955,7 +955,8 @@ public StorageListOperation<?> list(@NonNull String path,
options.getAccessLevel() != null ? options.getAccessLevel() : defaultAccessLevel,
options.getTargetIdentityId(),
options.getPageSize(),
options.getNextToken());
options.getNextToken(),
options.getSubpathStrategy());

AWSS3StorageListOperation operation =
new AWSS3StorageListOperation(
Expand Down Expand Up @@ -983,7 +984,8 @@ public StorageListOperation<?> list(
AWSS3StoragePathListRequest request = new AWSS3StoragePathListRequest(
path,
options.getPageSize(),
options.getNextToken());
options.getNextToken(),
options.getSubpathStrategy());

AWSS3StoragePathListOperation operation =
new AWSS3StoragePathListOperation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@
import com.amplifyframework.auth.AuthCredentialsProvider;
import com.amplifyframework.core.Consumer;
import com.amplifyframework.storage.StorageException;
import com.amplifyframework.storage.StorageItem;
import com.amplifyframework.storage.operation.StorageListOperation;
import com.amplifyframework.storage.options.SubpathStrategy;
import com.amplifyframework.storage.result.StorageListResult;
import com.amplifyframework.storage.s3.configuration.AWSS3StoragePluginConfiguration;
import com.amplifyframework.storage.s3.options.AWSS3StoragePagedListOptions;
import com.amplifyframework.storage.s3.request.AWSS3StorageListRequest;
import com.amplifyframework.storage.s3.service.StorageService;

import java.util.List;
import java.util.concurrent.ExecutorService;

/**
Expand Down Expand Up @@ -86,14 +85,14 @@ public void start() {
prefix -> {
try {
String serviceKey = prefix.concat(getRequest().getPath());
SubpathStrategy subpathStrategy = getRequest().getSubpathStrategy();
if (getRequest().getPageSize() == AWSS3StoragePagedListOptions.ALL_PAGE_SIZE) {
// fetch all the keys
List<StorageItem> listedItems = storageService.listFiles(serviceKey, prefix);
onSuccess.accept(StorageListResult.fromItems(listedItems, null));
onSuccess.accept(storageService.listFiles(serviceKey, prefix, subpathStrategy));
} else {
onSuccess.accept(
storageService.listFiles(serviceKey, prefix, getRequest().getPageSize(),
getRequest().getNextToken()));
getRequest().getNextToken(), subpathStrategy));
}
} catch (Exception exception) {
onError.accept(new StorageException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ internal class AWSS3StoragePathListOperation(

try {
onSuccess.accept(
storageService.listFiles(serviceKey, request.pageSize, request.nextToken)
storageService.listFiles(serviceKey, request.pageSize, request.nextToken, request.subpathStrategy)
)
} catch (exception: Exception) {
onError.accept(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import androidx.annotation.Nullable;

import com.amplifyframework.storage.StorageAccessLevel;
import com.amplifyframework.storage.options.SubpathStrategy;
import com.amplifyframework.storage.s3.options.AWSS3StoragePagedListOptions;

/**
Expand All @@ -33,6 +34,7 @@ public final class AWSS3StorageListRequest {
private final String targetIdentityId;
private final int pageSize;
private final String nextToken;
private final SubpathStrategy subpathStrategy;

/**
* Constructs a new AWSS3StorageListRequest.
Expand All @@ -55,6 +57,7 @@ public AWSS3StorageListRequest(
this.targetIdentityId = targetIdentityId;
this.pageSize = AWSS3StoragePagedListOptions.ALL_PAGE_SIZE;
this.nextToken = null;
this.subpathStrategy = null;
}

/**
Expand Down Expand Up @@ -82,6 +85,37 @@ public AWSS3StorageListRequest(
this.targetIdentityId = targetIdentityId;
this.pageSize = pageSize;
this.nextToken = nextToken;
this.subpathStrategy = null;
}

/**
* Constructs a new AWSS3StorageListRequest.
* Although this has public access, it is intended for internal use and should not be used directly by host
* applications. The behavior of this may change without warning.
*
* @param path the path in S3 to list items from
* @param accessLevel Storage access level
* @param targetIdentityId If set, this should override the current user's identity ID.
* If null, the operation will fetch the current identity ID.
* @param pageSize number of keys to be retrieved from s3
* @param nextToken next continuation token to be passed to s3
* @param subpathStrategy strategy to include or exclude sub-paths in s3 path
*/
@SuppressWarnings("deprecation")
public AWSS3StorageListRequest(
@NonNull String path,
@NonNull StorageAccessLevel accessLevel,
@Nullable String targetIdentityId,
int pageSize,
@Nullable String nextToken,
@Nullable SubpathStrategy subpathStrategy
) {
this.path = path;
this.accessLevel = accessLevel;
this.targetIdentityId = targetIdentityId;
this.pageSize = pageSize;
this.nextToken = nextToken;
this.subpathStrategy = subpathStrategy;
}

/**
Expand Down Expand Up @@ -128,5 +162,14 @@ public int getPageSize() {
public String getNextToken() {
return nextToken;
}

/**
* Get SubpathStrategy to include/exclude sub-paths.
* @return SubpathStrategy to include/exclude sub-paths.
* */
@Nullable
public SubpathStrategy getSubpathStrategy() {
return subpathStrategy;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
package com.amplifyframework.storage.s3.request

import com.amplifyframework.storage.StoragePath
import com.amplifyframework.storage.options.SubpathStrategy

/**
* Parameters to provide to S3 that describe a request to list files.
*/
internal data class AWSS3StoragePathListRequest(
val path: StoragePath,
val pageSize: Int,
val nextToken: String?
val nextToken: String?,
val subpathStrategy: SubpathStrategy?
)
Loading
Loading