Skip to content

Commit 70b0c39

Browse files
authored
Recv file path binding (#825)
1 parent c66582f commit 70b0c39

File tree

5 files changed

+207
-5
lines changed

5 files changed

+207
-5
lines changed

src/main/java/software/amazon/awssdk/crt/s3/S3Client.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ public S3MetaRequest makeMetaRequest(S3MetaRequestOptions options) {
157157
if (options.getRequestFilePath() != null) {
158158
requestFilePath = options.getRequestFilePath().toString().getBytes(UTF8);
159159
}
160+
byte[] responseFilePath = null;
161+
if (options.getResponseFilePath() != null) {
162+
responseFilePath = options.getResponseFilePath().toString().getBytes(UTF8);
163+
}
160164

161165
AwsSigningConfig signingConfig = options.getSigningConfig();
162166
boolean didCreateSigningConfig = false;
@@ -177,7 +181,9 @@ public S3MetaRequest makeMetaRequest(S3MetaRequestOptions options) {
177181
ChecksumAlgorithm.marshallAlgorithmsForJNI(checksumConfig.getValidateChecksumAlgorithmList()),
178182
httpRequestBytes, options.getHttpRequest().getBodyStream(), requestFilePath, signingConfig,
179183
responseHandlerNativeAdapter, endpoint == null ? null : endpoint.toString().getBytes(UTF8),
180-
options.getResumeToken(), options.getObjectSizeHint());
184+
options.getResumeToken(), options.getObjectSizeHint(), responseFilePath,
185+
options.getResponseFileOption().getNativeValue(), options.getResponseFilePosition(),
186+
options.getResponseFileDeleteOnFailure());
181187

182188
metaRequest.setMetaRequestNativeHandle(metaRequestNativeHandle);
183189

@@ -246,5 +252,6 @@ private static native long s3ClientMakeMetaRequest(long clientId, S3MetaRequest
246252
int[] validateAlgorithms, byte[] httpRequestBytes,
247253
HttpRequestBodyStream httpRequestBodyStream, byte[] requestFilePath,
248254
AwsSigningConfig signingConfig, S3MetaRequestResponseHandlerNativeAdapter responseHandlerNativeAdapter,
249-
byte[] endpoint, ResumeToken resumeToken, Long objectSizeHint);
255+
byte[] endpoint, ResumeToken resumeToken, Long objectSizeHint, byte[] responseFilePath,
256+
int responseFileOption, long responseFilePosition, boolean responseFileDeleteOnFailure);
250257
}

src/main/java/software/amazon/awssdk/crt/s3/S3MetaRequestOptions.java

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package software.amazon.awssdk.crt.s3;
66

77
import software.amazon.awssdk.crt.http.HttpRequest;
8+
import software.amazon.awssdk.crt.http.HttpStreamResponseHandler;
89
import software.amazon.awssdk.crt.auth.credentials.CredentialsProvider;
910
import software.amazon.awssdk.crt.auth.signing.AwsSigningConfig;
1011

@@ -84,6 +85,10 @@ private static Map<Integer, MetaRequestType> buildEnumMapping() {
8485
private ChecksumConfig checksumConfig;
8586
private HttpRequest httpRequest;
8687
private Path requestFilePath;
88+
private Path responseFilePath;
89+
private ResponseFileOption responseFileOption = ResponseFileOption.CREATE_OR_REPLACE;
90+
private long responseFilePosition = 0;
91+
private boolean responseFileDeleteOnFailure = false;
8792
private S3MetaRequestResponseHandler responseHandler;
8893
private CredentialsProvider credentialsProvider;
8994
private AwsSigningConfig signingConfig;
@@ -313,4 +318,134 @@ public S3MetaRequestOptions withObjectSizeHint(Long objectSizeHint) {
313318
public Long getObjectSizeHint() {
314319
return objectSizeHint;
315320
}
321+
322+
public enum ResponseFileOption {
323+
/**
324+
* Create a new file if it doesn't exist, otherwise replace the existing file.
325+
*/
326+
CREATE_OR_REPLACE(0),
327+
328+
/**
329+
* Always create a new file. If the file already exists,
330+
* AWS_ERROR_S3_RECV_FILE_EXISTS will be raised.
331+
*/
332+
CREATE_NEW(1),
333+
334+
/**
335+
* Create a new file if it doesn't exist, otherwise append to the existing file.
336+
*/
337+
CREATE_OR_APPEND(2),
338+
339+
/**
340+
* Write to an existing file at the specified position, defined by the
341+
* {@link withHttpRequest}.
342+
* If the file does not exist, AWS_ERROR_S3_RECV_FILE_NOT_EXISTS will be raised.
343+
* If {@link withHttpRequest} is not configured, start overwriting data at the
344+
* beginning of the file (byte 0).
345+
*/
346+
WRITE_TO_POSITION(3);
347+
348+
ResponseFileOption(int nativeValue) {
349+
this.nativeValue = nativeValue;
350+
}
351+
352+
public int getNativeValue() {
353+
return nativeValue;
354+
}
355+
356+
public static ResponseFileOption getEnumValueFromInteger(int value) {
357+
ResponseFileOption enumValue = enumMapping.get(value);
358+
if (enumValue != null) {
359+
return enumValue;
360+
}
361+
362+
throw new RuntimeException("Invalid S3 ResponseFileOption");
363+
}
364+
365+
private static Map<Integer, ResponseFileOption> buildEnumMapping() {
366+
Map<Integer, ResponseFileOption> enumMapping = new HashMap<Integer, ResponseFileOption>();
367+
enumMapping.put(CREATE_OR_REPLACE.getNativeValue(), CREATE_OR_REPLACE);
368+
enumMapping.put(CREATE_NEW.getNativeValue(), CREATE_NEW);
369+
enumMapping.put(CREATE_OR_APPEND.getNativeValue(), CREATE_OR_APPEND);
370+
enumMapping.put(WRITE_TO_POSITION.getNativeValue(), WRITE_TO_POSITION);
371+
return enumMapping;
372+
}
373+
374+
private int nativeValue;
375+
376+
private static Map<Integer, ResponseFileOption> enumMapping = buildEnumMapping();
377+
}
378+
379+
/**
380+
* If set, this file will be used to write the response body to a file.
381+
* And the {@link HttpStreamResponseHandler#onResponseBody} will not be invoked.
382+
* {@link withResponseFileOption} configures the write behavior.
383+
*
384+
* @param responseFilePath path to file to write response body to.
385+
* @return this
386+
*/
387+
public S3MetaRequestOptions withResponseFilePath(Path responseFilePath) {
388+
this.responseFilePath = responseFilePath;
389+
return this;
390+
}
391+
392+
public Path getResponseFilePath() {
393+
return responseFilePath;
394+
}
395+
396+
/**
397+
* Sets the option for how to handle the response file when downloading an
398+
* object from S3.
399+
* This option is only applicable when {@link withResponseFilePath} is set.
400+
*
401+
* By default, the option is set to
402+
* {@link ResponseFileOption#CREATE_OR_REPLACE}.
403+
*
404+
* @param responseFileOption The option for handling the response file.
405+
* @return this
406+
*/
407+
public S3MetaRequestOptions withResponseFileOption(ResponseFileOption responseFileOption) {
408+
this.responseFileOption = responseFileOption;
409+
return this;
410+
}
411+
412+
public ResponseFileOption getResponseFileOption() {
413+
return responseFileOption;
414+
}
415+
416+
/**
417+
* Sets the position to start writing to the response file.
418+
* This option is only applicable when {@link withResponseFileOption} is set
419+
* to {@link ResponseFileOption#WRITE_TO_POSITION}.
420+
*
421+
* @param responseFilePosition The position to start writing to the response
422+
* file.
423+
* @return this
424+
*/
425+
public S3MetaRequestOptions withResponseFilePosition(long responseFilePosition) {
426+
this.responseFilePosition = responseFilePosition;
427+
return this;
428+
}
429+
430+
public long getResponseFilePosition() {
431+
return responseFilePosition;
432+
}
433+
434+
/**
435+
* Sets whether to delete the response file on failure when downloading an
436+
* object from S3.
437+
* This option is only applicable when a response file path is set.
438+
*
439+
* @param responseFileDeleteOnFailure True to delete the response file on
440+
* failure,
441+
* False to leave it as-is.
442+
* @return this
443+
*/
444+
public S3MetaRequestOptions withResponseFileDeleteOnFailure(boolean responseFileDeleteOnFailure) {
445+
this.responseFileDeleteOnFailure = responseFileDeleteOnFailure;
446+
return this;
447+
}
448+
public boolean getResponseFileDeleteOnFailure() {
449+
return responseFileDeleteOnFailure;
450+
}
316451
}

src/native/s3_client.c

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -975,7 +975,11 @@ JNIEXPORT jlong JNICALL Java_software_amazon_awssdk_crt_s3_S3Client_s3ClientMake
975975
jobject java_response_handler_jobject,
976976
jbyteArray jni_endpoint,
977977
jobject java_resume_token_jobject,
978-
jobject jni_object_size_hint) {
978+
jobject jni_object_size_hint,
979+
jbyteArray jni_response_filepath,
980+
jint jni_response_file_option,
981+
jlong jni_response_file_position,
982+
jboolean jni_response_file_delete_on_failure) {
979983
(void)jni_class;
980984
aws_cache_jni_ids(env);
981985

@@ -985,6 +989,8 @@ JNIEXPORT jlong JNICALL Java_software_amazon_awssdk_crt_s3_S3Client_s3ClientMake
985989
AWS_ZERO_STRUCT(operation_name);
986990
struct aws_byte_cursor request_filepath;
987991
AWS_ZERO_STRUCT(request_filepath);
992+
struct aws_byte_cursor response_filepath;
993+
AWS_ZERO_STRUCT(response_filepath);
988994
struct aws_s3_meta_request_resume_token *resume_token =
989995
s_native_resume_token_from_java_new(env, java_resume_token_jobject);
990996
struct aws_s3_meta_request *meta_request = NULL;
@@ -1043,6 +1049,17 @@ JNIEXPORT jlong JNICALL Java_software_amazon_awssdk_crt_s3_S3Client_s3ClientMake
10431049
}
10441050
}
10451051

1052+
if (jni_response_filepath) {
1053+
response_filepath = aws_jni_byte_cursor_from_jbyteArray_acquire(env, jni_response_filepath);
1054+
if (response_filepath.ptr == NULL) {
1055+
goto done;
1056+
}
1057+
if (response_filepath.len == 0) {
1058+
aws_jni_throw_illegal_argument_exception(env, "Response file path cannot be empty");
1059+
goto done;
1060+
}
1061+
}
1062+
10461063
struct aws_uri endpoint;
10471064
AWS_ZERO_STRUCT(endpoint);
10481065
if (jni_endpoint != NULL) {
@@ -1097,6 +1114,10 @@ JNIEXPORT jlong JNICALL Java_software_amazon_awssdk_crt_s3_S3Client_s3ClientMake
10971114
.endpoint = jni_endpoint != NULL ? &endpoint : NULL,
10981115
.resume_token = resume_token,
10991116
.object_size_hint = jni_object_size_hint != NULL ? &object_size_hint : NULL,
1117+
.recv_filepath = response_filepath,
1118+
.recv_file_option = jni_response_file_option,
1119+
.recv_file_position = jni_response_file_position,
1120+
.recv_file_delete_on_failure = jni_response_file_delete_on_failure,
11001121
};
11011122

11021123
meta_request = aws_s3_client_make_meta_request(client, &meta_request_options);

src/test/java/software/amazon/awssdk/crt/test/S3ClientTest.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ public void testS3ClientCreateDestroy() {
126126

127127
}
128128
}
129-
129+
130130
@Test
131131
public void testS3ClientCreateDestroyWithTLS() {
132132
skipIfAndroid();
@@ -351,6 +351,45 @@ public void onFinished(S3FinishedResponseContext context) {
351351
}
352352
}
353353

354+
@Test
355+
public void testS3GetWithResponseFilePath() {
356+
skipIfAndroid();
357+
skipIfNetworkUnavailable();
358+
Assume.assumeTrue(hasAwsCredentials());
359+
S3ClientOptions clientOptions = new S3ClientOptions().withRegion(REGION);
360+
try (S3Client client = createS3Client(clientOptions)) {
361+
CompletableFuture<Integer> onFinishedFuture = new CompletableFuture<>();
362+
Path responsePath = Files.createTempFile("testS3GetFilePath", ".txt");
363+
S3MetaRequestResponseHandler responseHandler = new S3MetaRequestResponseHandler() {
364+
@Override
365+
public void onFinished(S3FinishedResponseContext context) {
366+
Log.log(Log.LogLevel.Info, Log.LogSubject.JavaCrtS3,
367+
"Meta request finished with error code " + context.getErrorCode());
368+
if (context.getErrorCode() != 0) {
369+
onFinishedFuture.completeExceptionally(makeExceptionFromFinishedResponseContext(context));
370+
return;
371+
}
372+
onFinishedFuture.complete(Integer.valueOf(context.getErrorCode()));
373+
}
374+
};
375+
376+
HttpHeader[] headers = { new HttpHeader("Host", ENDPOINT) };
377+
HttpRequest httpRequest = new HttpRequest("GET", PRE_EXIST_1MB_PATH, headers, null);
378+
379+
S3MetaRequestOptions metaRequestOptions = new S3MetaRequestOptions()
380+
.withMetaRequestType(MetaRequestType.GET_OBJECT).withHttpRequest(httpRequest)
381+
.withResponseFilePath(responsePath)
382+
.withResponseHandler(responseHandler);
383+
384+
try (S3MetaRequest metaRequest = client.makeMetaRequest(metaRequestOptions)) {
385+
Assert.assertEquals(Integer.valueOf(0), onFinishedFuture.get());
386+
}
387+
Files.deleteIfExists(responsePath);
388+
} catch (Exception ex) {
389+
Assert.fail(ex.getMessage());
390+
}
391+
}
392+
354393
@Test
355394
public void testS3GetWithSizeHint() {
356395
skipIfAndroid();

0 commit comments

Comments
 (0)