diff --git a/src/main/java/software/amazon/awssdk/crt/s3/S3Client.java b/src/main/java/software/amazon/awssdk/crt/s3/S3Client.java index 7833b710e..fa2c5a83d 100644 --- a/src/main/java/software/amazon/awssdk/crt/s3/S3Client.java +++ b/src/main/java/software/amazon/awssdk/crt/s3/S3Client.java @@ -168,7 +168,7 @@ public S3MetaRequest makeMetaRequest(S3MetaRequestOptions options) { ChecksumAlgorithm.marshallAlgorithmsForJNI(checksumConfig.getValidateChecksumAlgorithmList()), httpRequestBytes, options.getHttpRequest().getBodyStream(), requestFilePath, signingConfig, responseHandlerNativeAdapter, endpoint == null ? null : endpoint.toString().getBytes(UTF8), - options.getResumeToken()); + options.getResumeToken(), options.getObjectSizeHint()); metaRequest.setMetaRequestNativeHandle(metaRequestNativeHandle); @@ -236,5 +236,5 @@ private static native long s3ClientMakeMetaRequest(long clientId, S3MetaRequest int[] validateAlgorithms, byte[] httpRequestBytes, HttpRequestBodyStream httpRequestBodyStream, byte[] requestFilePath, AwsSigningConfig signingConfig, S3MetaRequestResponseHandlerNativeAdapter responseHandlerNativeAdapter, - byte[] endpoint, ResumeToken resumeToken); + byte[] endpoint, ResumeToken resumeToken, Long objectSizeHint); } diff --git a/src/main/java/software/amazon/awssdk/crt/s3/S3MetaRequestOptions.java b/src/main/java/software/amazon/awssdk/crt/s3/S3MetaRequestOptions.java index a7bc644a9..b942ec58f 100644 --- a/src/main/java/software/amazon/awssdk/crt/s3/S3MetaRequestOptions.java +++ b/src/main/java/software/amazon/awssdk/crt/s3/S3MetaRequestOptions.java @@ -88,6 +88,7 @@ private static Map buildEnumMapping() { private AwsSigningConfig signingConfig; private URI endpoint; private ResumeToken resumeToken; + private Long objectSizeHint; public S3MetaRequestOptions withMetaRequestType(MetaRequestType metaRequestType) { this.metaRequestType = metaRequestType; @@ -263,4 +264,20 @@ public S3MetaRequestOptions withResumeToken(ResumeToken resumeToken) { public ResumeToken getResumeToken() { return resumeToken; } + + /* + * (Optional) + * Total object size hint, in bytes. + * The optimal strategy for downloading a file depends on its size. + * Set this hint to help the S3 client choose the best strategy for this particular file. + * This is just used as an estimate, so it's okay to provide an approximate value if the exact size is unknown. + */ + public S3MetaRequestOptions withObjectSizeHint(Long objectSizeHint) { + this.objectSizeHint = objectSizeHint; + return this; + } + + public Long getObjectSizeHint() { + return objectSizeHint; + } } diff --git a/src/native/crt.c b/src/native/crt.c index 880e5bd9d..6a62f45d3 100644 --- a/src/native/crt.c +++ b/src/native/crt.c @@ -320,6 +320,30 @@ int aws_size_t_from_java(JNIEnv *env, size_t *out_size, jlong java_long, const c return AWS_OP_ERR; } +int aws_uint64_t_from_java_Long(JNIEnv *env, uint64_t *out_long, jobject java_Long, const char *errmsg_prefix) { + if (java_Long == NULL) { + aws_jni_throw_null_pointer_exception(env, "%s can't be null"); + goto error; + } + + jlong jlong_value = (*env)->CallLongMethod(env, java_Long, boxed_long_properties.long_value_method_id); + if ((*env)->ExceptionCheck(env)) { + goto error; + } + + int64_t jlong_value_int64 = (int64_t)jlong_value; + if (jlong_value_int64 < 0) { + aws_jni_throw_illegal_argument_exception(env, "%s cannot be negative", errmsg_prefix); + goto error; + } + *out_long = (uint64_t)jlong_value_int64; + return AWS_OP_SUCCESS; + +error: + *out_long = 0; + return AWS_OP_ERR; +} + jbyteArray aws_java_byte_array_new(JNIEnv *env, size_t size) { jbyteArray jArray = (*env)->NewByteArray(env, (jsize)size); return jArray; diff --git a/src/native/crt.h b/src/native/crt.h index 67c740c05..0f653b38e 100644 --- a/src/native/crt.h +++ b/src/native/crt.h @@ -82,6 +82,13 @@ bool aws_jni_get_and_clear_exception(JNIEnv *env, jthrowable *out); ******************************************************************************/ int aws_size_t_from_java(JNIEnv *env, size_t *out_size, jlong java_long, const char *errmsg_prefix); +/******************************************************************************* + * Set a uint64_t based on a Long (jobject). + * If conversion fails, a java IllegalArgumentException is thrown like + * "{errmsg_prefix} cannot be negative" and AWS_OP_ERR is returned. + ******************************************************************************/ +int aws_uint64_t_from_java_Long(JNIEnv *env, uint64_t *out_long, jobject java_Long, const char *errmsg_prefix); + /******************************************************************************* * aws_java_byte_array_new - Creates a new Java byte[] ******************************************************************************/ diff --git a/src/native/s3_client.c b/src/native/s3_client.c index 21e46d58f..2891d8c8e 100644 --- a/src/native/s3_client.c +++ b/src/native/s3_client.c @@ -953,7 +953,8 @@ JNIEXPORT jlong JNICALL Java_software_amazon_awssdk_crt_s3_S3Client_s3ClientMake jobject java_signing_config, jobject java_response_handler_jobject, jbyteArray jni_endpoint, - jobject java_resume_token_jobject) { + jobject java_resume_token_jobject, + jobject jni_object_size_hint) { (void)jni_class; aws_cache_jni_ids(env); @@ -1039,6 +1040,13 @@ JNIEXPORT jlong JNICALL Java_software_amazon_awssdk_crt_s3_S3Client_s3ClientMake checksum_config.validate_checksum_algorithms = &response_checksum_list; } + uint64_t object_size_hint = 0; + if (jni_object_size_hint != NULL) { + if (aws_uint64_t_from_java_Long(env, &object_size_hint, jni_object_size_hint, "objectSizeHint")) { + goto done; + } + } + struct aws_s3_meta_request_options meta_request_options = { .type = meta_request_type, .checksum_config = &checksum_config, @@ -1053,6 +1061,7 @@ JNIEXPORT jlong JNICALL Java_software_amazon_awssdk_crt_s3_S3Client_s3ClientMake .shutdown_callback = s_on_s3_meta_request_shutdown_complete_callback, .endpoint = jni_endpoint != NULL ? &endpoint : NULL, .resume_token = resume_token, + .object_size_hint = jni_object_size_hint != NULL ? &object_size_hint : NULL, }; meta_request = aws_s3_client_make_meta_request(client, &meta_request_options); diff --git a/src/test/java/software/amazon/awssdk/crt/test/S3ClientTest.java b/src/test/java/software/amazon/awssdk/crt/test/S3ClientTest.java index a0aa916b0..9e9dbd2f8 100644 --- a/src/test/java/software/amazon/awssdk/crt/test/S3ClientTest.java +++ b/src/test/java/software/amazon/awssdk/crt/test/S3ClientTest.java @@ -330,6 +330,54 @@ public void onFinished(S3FinishedResponseContext context) { Assert.fail(ex.getMessage()); } } + + @Test + public void testS3GetWithSizeHint() { + skipIfAndroid(); + skipIfNetworkUnavailable(); + Assume.assumeTrue(hasAwsCredentials()); + S3ClientOptions clientOptions = new S3ClientOptions().withRegion(REGION); + try (S3Client client = createS3Client(clientOptions)) { + CompletableFuture onFinishedFuture = new CompletableFuture<>(); + S3MetaRequestResponseHandler responseHandler = new S3MetaRequestResponseHandler() { + + @Override + public int onResponseBody(ByteBuffer bodyBytesIn, long objectRangeStart, long objectRangeEnd) { + byte[] bytes = new byte[bodyBytesIn.remaining()]; + bodyBytesIn.get(bytes); + Log.log(Log.LogLevel.Info, Log.LogSubject.JavaCrtS3, "Body Response: " + Arrays.toString(bytes)); + return 0; + } + + @Override + public void onFinished(S3FinishedResponseContext context) { + Log.log(Log.LogLevel.Info, Log.LogSubject.JavaCrtS3, + "Meta request finished with error code " + context.getErrorCode()); + if (context.getErrorCode() != 0) { + onFinishedFuture.completeExceptionally(makeExceptionFromFinishedResponseContext(context)); + return; + } + onFinishedFuture.complete(Integer.valueOf(context.getErrorCode())); + } + }; + + HttpHeader[] headers = { new HttpHeader("Host", ENDPOINT) }; + HttpRequest httpRequest = new HttpRequest("GET", PRE_EXIST_1MB_PATH, headers, null); + + S3MetaRequestOptions metaRequestOptions = new S3MetaRequestOptions() + .withMetaRequestType(MetaRequestType.GET_OBJECT).withHttpRequest(httpRequest) + .withResponseHandler(responseHandler) + // Passing a 5GB size hint intentionally to verify + // that an incorrect size_hint and size_hint > UINT32.MAX work fine. + .withObjectSizeHint(5L * 1024 * 1024 * 1024); + + try (S3MetaRequest metaRequest = client.makeMetaRequest(metaRequestOptions)) { + Assert.assertEquals(Integer.valueOf(0), onFinishedFuture.get()); + } + } catch (InterruptedException | ExecutionException ex) { + Assert.fail(ex.getMessage()); + } + } @Test public void testS3GetErrorHeadersAreReported() {