Skip to content

Commit 219bb56

Browse files
Add AsyncPresignedUrlManager for S3 GetObject operations with presigned URLs (#6255)
* async presignedurlmanager api and implementation * Refactor DefaultAsyncPresignedUrlManager: move config to constructor, declare variables inline * change operation name
1 parent 3cd3dff commit 219bb56

File tree

3 files changed

+608
-0
lines changed

3 files changed

+608
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.services.s3.internal.presignedurl;
17+
18+
import static software.amazon.awssdk.core.client.config.SdkClientOption.SIGNER_OVERRIDDEN;
19+
import static software.amazon.awssdk.utils.FunctionalUtils.runAndLogError;
20+
21+
import java.util.Collections;
22+
import java.util.List;
23+
import java.util.Optional;
24+
import java.util.concurrent.CompletableFuture;
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
import software.amazon.awssdk.annotations.SdkInternalApi;
28+
import software.amazon.awssdk.awscore.exception.AwsServiceException;
29+
import software.amazon.awssdk.awscore.internal.AwsProtocolMetadata;
30+
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
31+
import software.amazon.awssdk.core.async.AsyncResponseTransformerUtils;
32+
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
33+
import software.amazon.awssdk.core.client.config.SdkClientConfiguration;
34+
import software.amazon.awssdk.core.client.config.SdkClientOption;
35+
import software.amazon.awssdk.core.client.handler.AsyncClientHandler;
36+
import software.amazon.awssdk.core.client.handler.ClientExecutionParams;
37+
import software.amazon.awssdk.core.exception.SdkClientException;
38+
import software.amazon.awssdk.core.http.HttpResponseHandler;
39+
import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute;
40+
import software.amazon.awssdk.core.metrics.CoreMetric;
41+
import software.amazon.awssdk.core.signer.NoOpSigner;
42+
import software.amazon.awssdk.metrics.MetricCollector;
43+
import software.amazon.awssdk.metrics.MetricPublisher;
44+
import software.amazon.awssdk.metrics.NoOpMetricCollector;
45+
import software.amazon.awssdk.protocols.xml.AwsS3ProtocolFactory;
46+
import software.amazon.awssdk.protocols.xml.XmlOperationMetadata;
47+
import software.amazon.awssdk.services.s3.internal.presignedurl.model.PresignedUrlGetObjectRequestWrapper;
48+
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
49+
import software.amazon.awssdk.services.s3.model.InvalidObjectStateException;
50+
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
51+
import software.amazon.awssdk.services.s3.model.S3Exception;
52+
import software.amazon.awssdk.services.s3.presignedurl.AsyncPresignedUrlManager;
53+
import software.amazon.awssdk.services.s3.presignedurl.model.PresignedUrlGetObjectRequest;
54+
import software.amazon.awssdk.utils.CompletableFutureUtils;
55+
import software.amazon.awssdk.utils.Pair;
56+
57+
/**
58+
* Default implementation of {@link AsyncPresignedUrlManager} for executing S3 operations asynchronously using presigned URLs.
59+
*/
60+
@SdkInternalApi
61+
public final class DefaultAsyncPresignedUrlManager implements AsyncPresignedUrlManager {
62+
private static final Logger log = LoggerFactory.getLogger(DefaultAsyncPresignedUrlManager.class);
63+
64+
private final AsyncClientHandler clientHandler;
65+
private final AwsS3ProtocolFactory protocolFactory;
66+
private final SdkClientConfiguration clientConfiguration;
67+
private final List<MetricPublisher> metricPublishers;
68+
private final AwsProtocolMetadata protocolMetadata;
69+
70+
public DefaultAsyncPresignedUrlManager(AsyncClientHandler clientHandler,
71+
AwsS3ProtocolFactory protocolFactory,
72+
SdkClientConfiguration clientConfiguration,
73+
AwsProtocolMetadata protocolMetadata) {
74+
this.clientHandler = clientHandler;
75+
this.protocolFactory = protocolFactory;
76+
this.protocolMetadata = protocolMetadata;
77+
this.clientConfiguration = updateSdkClientConfiguration(clientConfiguration);
78+
this.metricPublishers = Optional.ofNullable(
79+
this.clientConfiguration.option(SdkClientOption.METRIC_PUBLISHERS))
80+
.orElse(Collections.emptyList());
81+
}
82+
83+
@Override
84+
public <ReturnT> CompletableFuture<ReturnT> getObject(
85+
PresignedUrlGetObjectRequest presignedUrlGetObjectRequest,
86+
AsyncResponseTransformer<GetObjectResponse, ReturnT> asyncResponseTransformer)
87+
throws NoSuchKeyException, InvalidObjectStateException,
88+
AwsServiceException, SdkClientException, S3Exception {
89+
90+
PresignedUrlGetObjectRequestWrapper internalRequest = PresignedUrlGetObjectRequestWrapper.builder()
91+
.url(presignedUrlGetObjectRequest.presignedUrl())
92+
.range(presignedUrlGetObjectRequest.range())
93+
.build();
94+
95+
MetricCollector apiCallMetricCollector = metricPublishers.isEmpty() ?
96+
NoOpMetricCollector.create() : MetricCollector.create("ApiCall");
97+
98+
try {
99+
apiCallMetricCollector.reportMetric(CoreMetric.SERVICE_ID, "S3");
100+
//TODO: Discuss if we need to change OPERATION_NAME as part of Surface API Review
101+
apiCallMetricCollector.reportMetric(CoreMetric.OPERATION_NAME, "GetObject");
102+
103+
Pair<AsyncResponseTransformer<GetObjectResponse, ReturnT>, CompletableFuture<Void>> pair =
104+
AsyncResponseTransformerUtils.wrapWithEndOfStreamFuture(asyncResponseTransformer);
105+
AsyncResponseTransformer<GetObjectResponse, ReturnT> finalAsyncResponseTransformer = pair.left();
106+
asyncResponseTransformer = finalAsyncResponseTransformer;
107+
CompletableFuture<Void> endOfStreamFuture = pair.right();
108+
109+
HttpResponseHandler<GetObjectResponse> responseHandler = protocolFactory.createResponseHandler(
110+
GetObjectResponse::builder, new XmlOperationMetadata().withHasStreamingSuccessResponse(true));
111+
112+
HttpResponseHandler<AwsServiceException> errorResponseHandler = protocolFactory.createErrorResponseHandler();
113+
114+
CompletableFuture<ReturnT> executeFuture = clientHandler.execute(
115+
new ClientExecutionParams<PresignedUrlGetObjectRequestWrapper, GetObjectResponse>()
116+
.withOperationName("GetObject")
117+
.withProtocolMetadata(protocolMetadata)
118+
.withResponseHandler(responseHandler)
119+
.withErrorResponseHandler(errorResponseHandler)
120+
.withRequestConfiguration(clientConfiguration)
121+
.withInput(internalRequest)
122+
.withMetricCollector(apiCallMetricCollector)
123+
// TODO: Deprecate IS_DISCOVERED_ENDPOINT, use new SKIP_ENDPOINT_RESOLUTION for better semantics
124+
.putExecutionAttribute(SdkInternalExecutionAttribute.IS_DISCOVERED_ENDPOINT, true)
125+
.withMarshaller(new PresignedUrlGetObjectRequestMarshaller(protocolFactory)),
126+
asyncResponseTransformer);
127+
128+
CompletableFuture<ReturnT> whenCompleteFuture = executeFuture.whenComplete((r, e) -> {
129+
if (e != null) {
130+
runAndLogError(log, "Exception thrown in exceptionOccurred callback, ignoring",
131+
() -> finalAsyncResponseTransformer.exceptionOccurred(e));
132+
}
133+
endOfStreamFuture.whenComplete((r2, e2) -> {
134+
metricPublishers.forEach(p -> p.publish(apiCallMetricCollector.collect()));
135+
});
136+
});
137+
return CompletableFutureUtils.forwardExceptionTo(whenCompleteFuture, executeFuture);
138+
} catch (Throwable t) {
139+
AsyncResponseTransformer<GetObjectResponse, ReturnT> finalAsyncResponseTransformer = asyncResponseTransformer;
140+
runAndLogError(log, "Exception thrown in exceptionOccurred callback, ignoring",
141+
() -> finalAsyncResponseTransformer.exceptionOccurred(t));
142+
metricPublishers.forEach(p -> p.publish(apiCallMetricCollector.collect()));
143+
return CompletableFutureUtils.failedFuture(t);
144+
}
145+
}
146+
147+
private SdkClientConfiguration updateSdkClientConfiguration(SdkClientConfiguration clientConfiguration) {
148+
SdkClientConfiguration.Builder configBuilder = clientConfiguration.toBuilder();
149+
configBuilder.option(SdkAdvancedClientOption.SIGNER, new NoOpSigner());
150+
configBuilder.option(SIGNER_OVERRIDDEN, true);
151+
return configBuilder.build();
152+
}
153+
154+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.services.s3.presignedurl;
17+
18+
import java.util.concurrent.CompletableFuture;
19+
import software.amazon.awssdk.annotations.SdkPublicApi;
20+
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
21+
import software.amazon.awssdk.core.exception.SdkClientException;
22+
import software.amazon.awssdk.services.s3.S3AsyncClient;
23+
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
24+
import software.amazon.awssdk.services.s3.model.InvalidObjectStateException;
25+
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
26+
import software.amazon.awssdk.services.s3.model.S3Exception;
27+
import software.amazon.awssdk.services.s3.presignedurl.model.PresignedUrlGetObjectRequest;
28+
29+
/**
30+
* Interface for executing S3 operations asynchronously using presigned URLs. This can be accessed using
31+
* {@link S3AsyncClient#presignedUrlManager()}.
32+
*/
33+
@SdkPublicApi
34+
public interface AsyncPresignedUrlManager {
35+
/**
36+
* <p>
37+
* Downloads an S3 object asynchronously using a presigned URL.
38+
* </p>
39+
* <p>
40+
* This operation uses a presigned URL that contains all necessary authentication information, eliminating the
41+
* need for AWS credentials at request time. The presigned URL must be valid and not expired.
42+
* </p>
43+
* <dl>
44+
* <dt>Range Requests</dt>
45+
* <dd>
46+
* <p>
47+
* Supports partial object downloads using HTTP Range headers. Specify the range parameter
48+
* in the request to download only a portion of the object (e.g., "bytes=0-1023").
49+
* </p>
50+
* </dd>
51+
* </dl>
52+
*
53+
* @param request The presigned URL request containing the URL and optional range parameters
54+
* @param responseTransformer Transforms the response to the desired return type. See
55+
* {@link software.amazon.awssdk.core.async.AsyncResponseTransformer} for pre-built
56+
* implementations like downloading to a file or converting to bytes.
57+
* @param <ReturnT> The type of the transformed response
58+
* @return A {@link CompletableFuture} containing the transformed result of the AsyncResponseTransformer
59+
* @throws software.amazon.awssdk.services.s3.model.NoSuchKeyException The specified object does not exist
60+
* @throws software.amazon.awssdk.services.s3.model.InvalidObjectStateException Object is archived and must be restored before
61+
* retrieval
62+
* @throws software.amazon.awssdk.core.exception.SdkClientException If any client side error occurs such as
63+
* network failures or invalid presigned URL
64+
* @throws S3Exception Base class for all S3 service exceptions.
65+
* Unknown exceptions will be thrown as an
66+
* instance of this type.
67+
*/
68+
default <ReturnT> CompletableFuture<ReturnT> getObject(PresignedUrlGetObjectRequest request,
69+
AsyncResponseTransformer<GetObjectResponse,
70+
ReturnT> responseTransformer) throws NoSuchKeyException,
71+
InvalidObjectStateException,
72+
SdkClientException,
73+
S3Exception {
74+
throw new UnsupportedOperationException();
75+
}
76+
77+
// TODO: Add convenience methods :
78+
// - getObject(Consumer<Builder>, AsyncResponseTransformer) - consumer-based request building
79+
// - getObject(PresignedUrlGetObjectRequest, Path) - direct file download
80+
// - getObject(Consumer<Builder>, Path) - consumer + file download
81+
}

0 commit comments

Comments
 (0)