Skip to content

Commit 6dbb771

Browse files
authored
feat(storage): add object existence validation option to get presigned url (#2848)
1 parent c0e5110 commit 6dbb771

File tree

12 files changed

+377
-36
lines changed

12 files changed

+377
-36
lines changed

aws-storage-s3/api/aws-storage-s3.api

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ public final class com/amplifyframework/storage/s3/options/AWSS3StorageGetPresig
176176
public static fun defaultInstance ()Lcom/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions;
177177
public fun equals (Ljava/lang/Object;)Z
178178
public static fun from (Lcom/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions;)Lcom/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions$Builder;
179+
public fun getValidateObjectExistence ()Z
179180
public fun hashCode ()I
180181
public fun toString ()Ljava/lang/String;
181182
public fun useAccelerateEndpoint ()Z
@@ -187,6 +188,7 @@ public final class com/amplifyframework/storage/s3/options/AWSS3StorageGetPresig
187188
public synthetic fun build ()Lcom/amplifyframework/storage/options/StorageOptions;
188189
public fun build ()Lcom/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions;
189190
public fun setUseAccelerateEndpoint (Z)Lcom/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions$Builder;
191+
public fun setValidateObjectExistence (Z)Lcom/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions$Builder;
190192
}
191193

192194
public final class com/amplifyframework/storage/s3/options/AWSS3StorageListOptions : com/amplifyframework/storage/options/StorageListOptions {
@@ -283,11 +285,13 @@ public final class com/amplifyframework/storage/s3/request/AWSS3StorageDownloadF
283285

284286
public final class com/amplifyframework/storage/s3/request/AWSS3StorageGetPresignedUrlRequest {
285287
public fun <init> (Ljava/lang/String;Lcom/amplifyframework/storage/StorageAccessLevel;Ljava/lang/String;IZ)V
288+
public fun <init> (Ljava/lang/String;Lcom/amplifyframework/storage/StorageAccessLevel;Ljava/lang/String;IZZ)V
286289
public fun getAccessLevel ()Lcom/amplifyframework/storage/StorageAccessLevel;
287290
public fun getExpires ()I
288291
public fun getKey ()Ljava/lang/String;
289292
public fun getTargetIdentityId ()Ljava/lang/String;
290293
public fun useAccelerateEndpoint ()Z
294+
public fun validateObjectExistence ()Z
291295
}
292296

293297
public final class com/amplifyframework/storage/s3/request/AWSS3StorageListRequest {
@@ -331,6 +335,7 @@ public abstract interface class com/amplifyframework/storage/s3/service/StorageS
331335
public abstract fun resumeTransfer (Lcom/amplifyframework/storage/s3/transfer/TransferObserver;)V
332336
public abstract fun uploadFile (Ljava/lang/String;Ljava/lang/String;Ljava/io/File;Lcom/amplifyframework/storage/ObjectMetadata;Z)Lcom/amplifyframework/storage/s3/transfer/TransferObserver;
333337
public abstract fun uploadInputStream (Ljava/lang/String;Ljava/lang/String;Ljava/io/InputStream;Lcom/amplifyframework/storage/ObjectMetadata;Z)Lcom/amplifyframework/storage/s3/transfer/TransferObserver;
338+
public abstract fun validateObjectExists (Ljava/lang/String;)V
334339
}
335340

336341
public abstract interface class com/amplifyframework/storage/s3/service/StorageService$Factory {

aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/AWSS3StoragePathGetUrlTest.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,22 @@ package com.amplifyframework.storage.s3
1616

1717
import android.content.Context
1818
import androidx.test.core.app.ApplicationProvider
19+
import aws.sdk.kotlin.services.s3.model.NotFound
1920
import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin
2021
import com.amplifyframework.storage.StorageCategory
22+
import com.amplifyframework.storage.StorageException
2123
import com.amplifyframework.storage.StoragePath
2224
import com.amplifyframework.storage.options.StorageGetUrlOptions
2325
import com.amplifyframework.storage.options.StorageUploadFileOptions
26+
import com.amplifyframework.storage.s3.options.AWSS3StorageGetPresignedUrlOptions
2427
import com.amplifyframework.storage.s3.test.R
2528
import com.amplifyframework.storage.s3.util.WorkmanagerTestUtils.initializeWorkmanagerTestUtil
2629
import com.amplifyframework.testutils.random.RandomTempFile
2730
import com.amplifyframework.testutils.sync.SynchronousAuth
2831
import com.amplifyframework.testutils.sync.SynchronousStorage
2932
import java.io.File
3033
import org.junit.Assert.assertEquals
34+
import org.junit.Assert.assertThrows
3135
import org.junit.Assert.assertTrue
3236
import org.junit.BeforeClass
3337
import org.junit.Test
@@ -79,4 +83,42 @@ class AWSS3StoragePathGetUrlTest {
7983
assertEquals("/public/$SMALL_FILE_NAME", result.url.path)
8084
assertTrue(result.url.query.contains("X-Amz-Expires=30"))
8185
}
86+
87+
@Test
88+
fun testGetUrlWithObjectExistenceValidationEnabled() {
89+
val result = synchronousStorage.getUrl(
90+
SMALL_FILE_PATH,
91+
AWSS3StorageGetPresignedUrlOptions.builder().setValidateObjectExistence(true).expires(30).build()
92+
)
93+
94+
assertEquals("/public/$SMALL_FILE_NAME", result.url.path)
95+
assertTrue(result.url.query.contains("X-Amz-Expires=30"))
96+
}
97+
98+
@Test
99+
fun testGetUrlWithStorageExceptionObjectNotFoundThrown() {
100+
val exception = assertThrows(StorageException::class.java) {
101+
synchronousStorage.getUrl(
102+
StoragePath.fromString("public/SOME_UNKNOWN_FILE"),
103+
AWSS3StorageGetPresignedUrlOptions.builder().setValidateObjectExistence(true).expires(30).build()
104+
)
105+
}
106+
107+
assertTrue(exception.cause is NotFound)
108+
}
109+
110+
@Test
111+
fun testGetUrlWithObjectExistenceValidationDisabledForNonExistentObject() {
112+
val result = synchronousStorage.getUrl(
113+
StoragePath.fromString("public/SOME_UNKNOWN_FILE"),
114+
AWSS3StorageGetPresignedUrlOptions.builder().setValidateObjectExistence(false).expires(30).build()
115+
)
116+
117+
assertEquals("/public/SOME_UNKNOWN_FILE", result.url.path)
118+
assertTrue(result.url.query.contains("X-Amz-Expires=30"))
119+
120+
assertThrows(java.io.FileNotFoundException::class.java) {
121+
result.url.readBytes()
122+
}
123+
}
82124
}

aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/AWSS3StoragePlugin.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,8 @@ public StorageGetUrlOperation<?> getUrl(
319319
@NonNull Consumer<StorageException> onError) {
320320
boolean useAccelerateEndpoint = options instanceof AWSS3StorageGetPresignedUrlOptions &&
321321
((AWSS3StorageGetPresignedUrlOptions) options).useAccelerateEndpoint();
322+
boolean validateObjectExistence = options instanceof AWSS3StorageGetPresignedUrlOptions &&
323+
((AWSS3StorageGetPresignedUrlOptions) options).getValidateObjectExistence();
322324
AWSS3StorageGetPresignedUrlRequest request = new AWSS3StorageGetPresignedUrlRequest(
323325
key,
324326
options.getAccessLevel() != null
@@ -328,7 +330,8 @@ public StorageGetUrlOperation<?> getUrl(
328330
options.getExpires() != 0
329331
? options.getExpires()
330332
: defaultUrlExpiration,
331-
useAccelerateEndpoint
333+
useAccelerateEndpoint,
334+
validateObjectExistence
332335
);
333336

334337
AWSS3StorageGetPresignedUrlOperation operation =
@@ -355,10 +358,15 @@ public StorageGetUrlOperation<?> getUrl(
355358
) {
356359
boolean useAccelerateEndpoint = options instanceof AWSS3StorageGetPresignedUrlOptions &&
357360
((AWSS3StorageGetPresignedUrlOptions) options).useAccelerateEndpoint();
361+
362+
boolean validateObjectExistence = options instanceof AWSS3StorageGetPresignedUrlOptions &&
363+
((AWSS3StorageGetPresignedUrlOptions) options).getValidateObjectExistence();
364+
358365
AWSS3StoragePathGetPresignedUrlRequest request = new AWSS3StoragePathGetPresignedUrlRequest(
359366
path,
360367
options.getExpires() != 0 ? options.getExpires() : defaultUrlExpiration,
361-
useAccelerateEndpoint
368+
useAccelerateEndpoint,
369+
validateObjectExistence
362370
);
363371

364372
AWSS3StoragePathGetPresignedUrlOperation operation =

aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageGetPresignedUrlOperation.java

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,14 @@
3131
import java.util.concurrent.ExecutorService;
3232

3333
/**
34-
* An operation to retrieve pre-signed object URL from AWS S3.
35-
* @deprecated Class should not be public and explicitly cast to. Cast to StorageGetUrlOperation.
36-
* Internal usages are moving to AWSS3StoragePathGetPresignedUrlOperation
34+
* An operation to retrieve pre-signed object URL from AWS S3.
35+
*
36+
* @deprecated Class should not be public and explicitly cast to. Cast to StorageGetUrlOperation.
37+
* Internal usages are moving to AWSS3StoragePathGetPresignedUrlOperation
3738
*/
3839
@Deprecated
3940
public final class AWSS3StorageGetPresignedUrlOperation
40-
extends StorageGetUrlOperation<AWSS3StorageGetPresignedUrlRequest> {
41+
extends StorageGetUrlOperation<AWSS3StorageGetPresignedUrlRequest> {
4142
private final StorageService storageService;
4243
private final ExecutorService executorService;
4344
private final AuthCredentialsProvider authCredentialsProvider;
@@ -58,13 +59,13 @@ public final class AWSS3StorageGetPresignedUrlOperation
5859
* @param onError Notified upon URL generation error
5960
*/
6061
public AWSS3StorageGetPresignedUrlOperation(
61-
@NonNull StorageService storageService,
62-
@NonNull ExecutorService executorService,
63-
@NonNull AuthCredentialsProvider authCredentialsProvider,
64-
@NonNull AWSS3StorageGetPresignedUrlRequest request,
65-
@NonNull AWSS3StoragePluginConfiguration awss3StoragePluginConfiguration,
66-
@NonNull Consumer<StorageGetUrlResult> onSuccess,
67-
@NonNull Consumer<StorageException> onError
62+
@NonNull StorageService storageService,
63+
@NonNull ExecutorService executorService,
64+
@NonNull AuthCredentialsProvider authCredentialsProvider,
65+
@NonNull AWSS3StorageGetPresignedUrlRequest request,
66+
@NonNull AWSS3StoragePluginConfiguration awss3StoragePluginConfiguration,
67+
@NonNull Consumer<StorageGetUrlResult> onSuccess,
68+
@NonNull Consumer<StorageException> onError
6869
) {
6970
super(request);
7071
this.storageService = storageService;
@@ -79,16 +80,26 @@ public AWSS3StorageGetPresignedUrlOperation(
7980
@Override
8081
public void start() {
8182
executorService.submit(() -> {
82-
awsS3StoragePluginConfiguration.getAWSS3PluginPrefixResolver(authCredentialsProvider).
83+
awsS3StoragePluginConfiguration.getAWSS3PluginPrefixResolver(authCredentialsProvider).
8384
resolvePrefix(getRequest().getAccessLevel(),
8485
getRequest().getTargetIdentityId(),
8586
prefix -> {
8687
try {
8788
String serviceKey = prefix.concat(getRequest().getKey());
89+
90+
if (getRequest().validateObjectExistence()) {
91+
try {
92+
storageService.validateObjectExists(serviceKey);
93+
} catch (StorageException exception) {
94+
onError.accept(exception);
95+
return;
96+
}
97+
}
98+
8899
URL url = storageService.getPresignedUrl(
89-
serviceKey,
90-
getRequest().getExpires(),
91-
getRequest().useAccelerateEndpoint());
100+
serviceKey,
101+
getRequest().getExpires(),
102+
getRequest().useAccelerateEndpoint());
92103
onSuccess.accept(StorageGetUrlResult.fromUrl(url));
93104
} catch (Exception exception) {
94105
onError.accept(new StorageException(

aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathGetPresignedUrlOperation.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,24 @@ internal class AWSS3StoragePathGetPresignedUrlOperation(
4848
return@submit
4949
}
5050

51+
if (request.validateObjectExistence) {
52+
try {
53+
storageService.validateObjectExists(serviceKey)
54+
} catch (se: StorageException) {
55+
onError.accept(se)
56+
return@submit
57+
} catch (exception: Exception) {
58+
onError.accept(
59+
StorageException(
60+
"Encountered an issue while validating the existence of object",
61+
exception,
62+
"See included exception for more details and suggestions to fix."
63+
)
64+
)
65+
return@submit
66+
}
67+
}
68+
5169
try {
5270
val url = storageService.getPresignedUrl(
5371
serviceKey,

aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626
*/
2727
public final class AWSS3StorageGetPresignedUrlOptions extends StorageGetUrlOptions {
2828
private final boolean useAccelerationMode;
29+
private final boolean validateObjectExistence;
2930

3031
private AWSS3StorageGetPresignedUrlOptions(final Builder builder) {
3132
super(builder);
3233
this.useAccelerationMode = builder.useAccelerateEndpoint;
34+
this.validateObjectExistence = builder.validateObjectExistence;
3335
}
3436

3537
/**
@@ -59,6 +61,8 @@ public static Builder from(@NonNull AWSS3StorageGetPresignedUrlOptions options)
5961
return builder()
6062
.accessLevel(options.getAccessLevel())
6163
.targetIdentityId(options.getTargetIdentityId())
64+
.expires(options.getExpires())
65+
.setValidateObjectExistence(options.getValidateObjectExistence())
6266
.expires(options.getExpires());
6367
}
6468

@@ -80,6 +84,16 @@ public boolean useAccelerateEndpoint() {
8084
return useAccelerationMode;
8185
}
8286

87+
/**
88+
* Gets the flag to determine whether to validate whether an S3 object exists.
89+
* Note: Setting this to `true` will result in a latency cost since confirming the existence
90+
* of the underlying S3 object will likely require a round-trip network call.
91+
* @return boolean flag
92+
*/
93+
public boolean getValidateObjectExistence() {
94+
return validateObjectExistence;
95+
}
96+
8397
@Override
8498
@SuppressWarnings("deprecation")
8599
public boolean equals(Object obj) {
@@ -91,7 +105,8 @@ public boolean equals(Object obj) {
91105
AWSS3StorageGetPresignedUrlOptions that = (AWSS3StorageGetPresignedUrlOptions) obj;
92106
return ObjectsCompat.equals(getAccessLevel(), that.getAccessLevel()) &&
93107
ObjectsCompat.equals(getTargetIdentityId(), that.getTargetIdentityId()) &&
94-
ObjectsCompat.equals(getExpires(), that.getExpires());
108+
ObjectsCompat.equals(getExpires(), that.getExpires()) &&
109+
ObjectsCompat.equals(getValidateObjectExistence(), that.getValidateObjectExistence());
95110
}
96111
}
97112

@@ -101,7 +116,8 @@ public int hashCode() {
101116
return ObjectsCompat.hash(
102117
getAccessLevel(),
103118
getTargetIdentityId(),
104-
getExpires()
119+
getExpires(),
120+
getValidateObjectExistence()
105121
);
106122
}
107123

@@ -113,6 +129,7 @@ public String toString() {
113129
"accessLevel=" + getAccessLevel() +
114130
", targetIdentityId=" + getTargetIdentityId() +
115131
", expires=" + getExpires() +
132+
", validateObjectExistence=" + getValidateObjectExistence() +
116133
'}';
117134
}
118135

@@ -123,6 +140,7 @@ public String toString() {
123140
*/
124141
public static final class Builder extends StorageGetUrlOptions.Builder<Builder> {
125142
private boolean useAccelerateEndpoint;
143+
private boolean validateObjectExistence;
126144

127145
/**
128146
* Configure to use acceleration mode on new StorageGetPresignedUrlOptions instances.
@@ -134,6 +152,16 @@ public Builder setUseAccelerateEndpoint(boolean useAccelerateEndpoint) {
134152
return this;
135153
}
136154

155+
/**
156+
* Configure to validate object existence flag on new StorageGetPresignedUrlOptions instances.
157+
* @param validateObjectExistence boolean flag to represent flag to validate object existence.
158+
* @return Current Builder instance for fluent chaining
159+
*/
160+
public Builder setValidateObjectExistence(boolean validateObjectExistence) {
161+
this.validateObjectExistence = validateObjectExistence;
162+
return this;
163+
}
164+
137165
@Override
138166
@NonNull
139167
public AWSS3StorageGetPresignedUrlOptions build() {

aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/request/AWSS3StorageGetPresignedUrlRequest.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public final class AWSS3StorageGetPresignedUrlRequest {
3333
private final String targetIdentityId;
3434
private final int expires;
3535
private final boolean useAccelerateEndpoint;
36+
private final boolean validateObjectExistence;
3637

3738
/**
3839
* Constructs a new AWSS3StorageGetUrlRequest.
@@ -58,6 +59,37 @@ public AWSS3StorageGetPresignedUrlRequest(
5859
this.targetIdentityId = targetIdentityId;
5960
this.expires = expires;
6061
this.useAccelerateEndpoint = useAccelerateEndpoint;
62+
this.validateObjectExistence = false;
63+
}
64+
65+
/**
66+
* Constructs a new AWSS3StorageGetUrlRequest.
67+
* Although this has public access, it is intended for internal use and should not be used directly by host
68+
* applications. The behavior of this may change without warning.
69+
*
70+
* @param key key for item to obtain URL for
71+
* @param accessLevel Storage access level
72+
* @param targetIdentityId If set, this should override the current user's identity ID.
73+
* If null, the operation will fetch the current identity ID.
74+
* @param expires The number of seconds before the URL expires
75+
* @param useAccelerateEndpoint Flag to enable acceleration mode
76+
* @param validateObjectExistence Flag to validate if object exists in storage
77+
*/
78+
@SuppressWarnings("deprecation")
79+
public AWSS3StorageGetPresignedUrlRequest(
80+
@NonNull String key,
81+
@NonNull StorageAccessLevel accessLevel,
82+
@Nullable String targetIdentityId,
83+
int expires,
84+
boolean useAccelerateEndpoint,
85+
boolean validateObjectExistence
86+
) {
87+
this.key = key;
88+
this.accessLevel = accessLevel;
89+
this.targetIdentityId = targetIdentityId;
90+
this.expires = expires;
91+
this.useAccelerateEndpoint = useAccelerateEndpoint;
92+
this.validateObjectExistence = validateObjectExistence;
6193
}
6294

6395
/**
@@ -104,5 +136,14 @@ public int getExpires() {
104136
public boolean useAccelerateEndpoint() {
105137
return useAccelerateEndpoint;
106138
}
139+
140+
/**
141+
* Gets the flag to determine whether to validate for object existence.
142+
*
143+
* @return boolean flag
144+
*/
145+
public boolean validateObjectExistence() {
146+
return validateObjectExistence;
147+
}
107148
}
108149

aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/request/AWSS3StoragePathGetPresignedUrlRequest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ import com.amplifyframework.storage.StoragePath
2222
internal data class AWSS3StoragePathGetPresignedUrlRequest(
2323
val path: StoragePath,
2424
val expires: Int,
25-
val useAccelerateEndpoint: Boolean
25+
val useAccelerateEndpoint: Boolean,
26+
val validateObjectExistence: Boolean,
2627
)

0 commit comments

Comments
 (0)