Skip to content

Commit

Permalink
feat(storage): add delimiter support (#2871)
Browse files Browse the repository at this point in the history
  • Loading branch information
phantumcode authored Jul 24, 2024
1 parent 45a2451 commit cf64793
Show file tree
Hide file tree
Showing 18 changed files with 684 additions and 29 deletions.
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;
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

0 comments on commit cf64793

Please sign in to comment.