diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java index 3b524f143c..8a709c0b6e 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java @@ -20,6 +20,8 @@ import com.google.api.core.InternalExtensionOnly; import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadResponse; +import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadRequest; +import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadResponse; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; import com.google.cloud.storage.multipartupload.model.ListPartsRequest; @@ -73,6 +75,17 @@ public abstract CreateMultipartUploadResponse createMultipartUpload( public abstract AbortMultipartUploadResponse abortMultipartUpload( AbortMultipartUploadRequest request); + /** + * Completes a multipart upload. + * + * @param request The request object containing the details for completing the multipart upload. + * @return A {@link CompleteMultipartUploadResponse} object containing information about the + * completed upload. + */ + @BetaApi + public abstract CompleteMultipartUploadResponse completeMultipartUpload( + CompleteMultipartUploadRequest request); + /** * Uploads a part in a multipart upload. * diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java index 818ed545cc..428c5038fb 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java @@ -20,6 +20,8 @@ import com.google.cloud.storage.Retrying.Retrier; import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadResponse; +import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadRequest; +import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadResponse; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; import com.google.cloud.storage.multipartupload.model.ListPartsRequest; @@ -79,6 +81,16 @@ public AbortMultipartUploadResponse abortMultipartUpload(AbortMultipartUploadReq Decoder.identity()); } + @Override + @BetaApi + public CompleteMultipartUploadResponse completeMultipartUpload( + CompleteMultipartUploadRequest request) { + return retrier.run( + retryAlgorithmManager.idempotent(), + () -> httpRequestManager.sendCompleteMultipartUploadRequest(uri, request), + Decoder.identity()); + } + @Override @BetaApi public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requestBody) { diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java index afc185486e..96e15e2547 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java @@ -31,6 +31,8 @@ import com.google.cloud.storage.Crc32cValue.Crc32cLengthKnown; import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadResponse; +import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadRequest; +import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadResponse; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; import com.google.cloud.storage.multipartupload.model.ListPartsRequest; @@ -44,6 +46,7 @@ import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLEncoder; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.regex.Matcher; @@ -111,11 +114,33 @@ AbortMultipartUploadResponse sendAbortMultipartUploadRequest( String abortUri = uri.toString() + resourcePath + queryString; HttpRequest httpRequest = requestFactory.buildDeleteRequest(new GenericUrl(abortUri)); + httpRequest.getHeaders().putAll(headerProvider.getHeaders()); httpRequest.setParser(objectParser); httpRequest.setThrowExceptionOnExecuteError(true); return httpRequest.execute().parseAs(AbortMultipartUploadResponse.class); } + CompleteMultipartUploadResponse sendCompleteMultipartUploadRequest( + URI uri, CompleteMultipartUploadRequest request) throws IOException { + String encodedBucket = urlEncode(request.bucket()); + String encodedKey = urlEncode(request.key()); + String resourcePath = "/" + encodedBucket + "/" + encodedKey; + String queryString = "?uploadId=" + request.uploadId(); + String completeUri = uri.toString() + resourcePath + queryString; + byte[] bytes = new XmlMapper().writeValueAsBytes(request.multipartUpload()); + HttpRequest httpRequest = + requestFactory.buildPostRequest( + new GenericUrl(completeUri), new ByteArrayContent("application/xml", bytes)); + httpRequest.getHeaders().putAll(headerProvider.getHeaders()); + @Nullable Crc32cLengthKnown crc32cValue = Hasher.defaultHasher().hash(ByteBuffer.wrap(bytes)); + if (crc32cValue != null) { + addChecksumHeader(crc32cValue, httpRequest.getHeaders()); + } + httpRequest.setParser(objectParser); + httpRequest.setThrowExceptionOnExecuteError(true); + return httpRequest.execute().parseAs(CompleteMultipartUploadResponse.class); + } + UploadPartResponse sendUploadPartRequest( URI uri, UploadPartRequest request, RewindableContent rewindableContent) throws IOException { String encodedBucket = urlEncode(request.bucket()); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CompleteMultipartUploadRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CompleteMultipartUploadRequest.java new file mode 100644 index 0000000000..8d67fda5bd --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CompleteMultipartUploadRequest.java @@ -0,0 +1,174 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.google.cloud.storage.multipartupload.model; + +import com.google.common.base.MoreObjects; +import java.util.Objects; + +/** Represents a request to complete a multipart upload. */ +public final class CompleteMultipartUploadRequest { + + private final String bucket; + private final String key; + private final String uploadId; + private final CompletedMultipartUpload multipartUpload; + + private CompleteMultipartUploadRequest(Builder builder) { + this.bucket = builder.bucket; + this.key = builder.key; + this.uploadId = builder.uploadId; + this.multipartUpload = builder.multipartUpload; + } + + /** + * Returns the bucket name. + * + * @return The bucket name. + */ + public String bucket() { + return bucket; + } + + /** + * Returns the object name. + * + * @return The object name. + */ + public String key() { + return key; + } + + /** + * Returns the upload ID of the multipart upload. + * + * @return The upload ID. + */ + public String uploadId() { + return uploadId; + } + + /** + * Returns the {@link CompletedMultipartUpload} payload for this request. + * + * @return The {@link CompletedMultipartUpload} payload. + */ + public CompletedMultipartUpload multipartUpload() { + return multipartUpload; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CompleteMultipartUploadRequest)) { + return false; + } + CompleteMultipartUploadRequest that = (CompleteMultipartUploadRequest) o; + return Objects.equals(bucket, that.bucket) + && Objects.equals(key, that.key) + && Objects.equals(uploadId, that.uploadId) + && Objects.equals(multipartUpload, that.multipartUpload); + } + + @Override + public int hashCode() { + return Objects.hash(bucket, key, uploadId, multipartUpload); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("bucket", bucket) + .add("key", key) + .add("uploadId", uploadId) + .add("completedMultipartUpload", multipartUpload) + .toString(); + } + + /** + * Creates a new builder for {@link CompleteMultipartUploadRequest}. + * + * @return A new builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** Builder for {@link CompleteMultipartUploadRequest}. */ + public static class Builder { + private String bucket; + private String key; + private String uploadId; + private CompletedMultipartUpload multipartUpload; + + private Builder() {} + + /** + * Sets the bucket name. + * + * @param bucket The bucket name. + * @return This builder. + */ + public Builder bucket(String bucket) { + this.bucket = bucket; + return this; + } + + /** + * Sets the object name. + * + * @param key The object name. + * @return This builder. + */ + public Builder key(String key) { + this.key = key; + return this; + } + + /** + * Sets the upload ID of the multipart upload. + * + * @param uploadId The upload ID. + * @return This builder. + */ + public Builder uploadId(String uploadId) { + this.uploadId = uploadId; + return this; + } + + /** + * Sets the {@link CompletedMultipartUpload} payload for this request. + * + * @param completedMultipartUpload The {@link CompletedMultipartUpload} payload. + * @return This builder. + */ + public Builder multipartUpload(CompletedMultipartUpload completedMultipartUpload) { + this.multipartUpload = completedMultipartUpload; + return this; + } + + /** + * Builds the {@link CompleteMultipartUploadRequest} object. + * + * @return The new {@link CompleteMultipartUploadRequest} object. + */ + public CompleteMultipartUploadRequest build() { + return new CompleteMultipartUploadRequest(this); + } + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CompleteMultipartUploadResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CompleteMultipartUploadResponse.java new file mode 100644 index 0000000000..ac63ec1acc --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CompleteMultipartUploadResponse.java @@ -0,0 +1,187 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.google.cloud.storage.multipartupload.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.base.MoreObjects; +import java.util.Objects; + +/** Represents the response from a completed multipart upload. */ +@JsonDeserialize(builder = CompleteMultipartUploadResponse.Builder.class) +public final class CompleteMultipartUploadResponse { + + private final String location; + private final String bucket; + private final String key; + private final String etag; + + private CompleteMultipartUploadResponse(Builder builder) { + this.location = builder.location; + this.bucket = builder.bucket; + this.key = builder.key; + this.etag = builder.etag; + } + + /** + * Returns the URL of the completed object. + * + * @return The URL of the completed object. + */ + @JsonProperty("Location") + public String location() { + return location; + } + + /** + * Returns the bucket name. + * + * @return The bucket name. + */ + @JsonProperty("Bucket") + public String bucket() { + return bucket; + } + + /** + * Returns the object name. + * + * @return The object name. + */ + @JsonProperty("Key") + public String key() { + return key; + } + + /** + * Returns the ETag of the completed object. + * + * @return The ETag of the completed object. + */ + @JsonProperty("ETag") + public String etag() { + return etag; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CompleteMultipartUploadResponse)) { + return false; + } + CompleteMultipartUploadResponse that = (CompleteMultipartUploadResponse) o; + return Objects.equals(location, that.location) + && Objects.equals(bucket, that.bucket) + && Objects.equals(key, that.key) + && Objects.equals(etag, that.etag); + } + + @Override + public int hashCode() { + return Objects.hash(location, bucket, key, etag); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("location", location) + .add("bucket", bucket) + .add("key", key) + .add("etag", etag) + .toString(); + } + + /** + * Creates a new builder for {@link CompleteMultipartUploadResponse}. + * + * @return A new builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** Builder for {@link CompleteMultipartUploadResponse}. */ + @JsonPOJOBuilder(buildMethodName = "build") + public static class Builder { + private String location; + private String bucket; + private String key; + private String etag; + + private Builder() {} + + /** + * Sets the URL of the completed object. + * + * @param location The URL of the completed object. + * @return This builder. + */ + @JsonProperty("Location") + public Builder location(String location) { + this.location = location; + return this; + } + + /** + * Sets the bucket name. + * + * @param bucket The bucket name. + * @return This builder. + */ + @JsonProperty("Bucket") + public Builder bucket(String bucket) { + this.bucket = bucket; + return this; + } + + /** + * Sets the object name. + * + * @param key The object name. + * @return This builder. + */ + @JsonProperty("Key") + public Builder key(String key) { + this.key = key; + return this; + } + + /** + * Sets the ETag of the completed object. + * + * @param etag The ETag of the completed object. + * @return This builder. + */ + @JsonProperty("ETag") + public Builder etag(String etag) { + this.etag = etag; + return this; + } + + /** + * Builds the {@link CompleteMultipartUploadResponse} object. + * + * @return The new {@link CompleteMultipartUploadResponse} object. + */ + public CompleteMultipartUploadResponse build() { + return new CompleteMultipartUploadResponse(this); + } + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CompletedMultipartUpload.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CompletedMultipartUpload.java new file mode 100644 index 0000000000..8929167249 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CompletedMultipartUpload.java @@ -0,0 +1,106 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.google.cloud.storage.multipartupload.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.google.common.base.MoreObjects; +import java.util.List; +import java.util.Objects; + +/** + * Represents the XML payload for a completed multipart upload. This is used in the request body + * when completing a multipart upload. + */ +@JacksonXmlRootElement(localName = "CompleteMultipartUpload") +public class CompletedMultipartUpload { + + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "Part") + private final List completedPartList; + + private CompletedMultipartUpload(Builder builder) { + this.completedPartList = builder.parts; + } + + /** + * Returns the list of completed parts for this multipart upload. + * + * @return The list of completed parts. + */ + public List parts() { + return completedPartList; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CompletedMultipartUpload)) { + return false; + } + CompletedMultipartUpload that = (CompletedMultipartUpload) o; + return Objects.equals(completedPartList, that.completedPartList); + } + + @Override + public int hashCode() { + return Objects.hash(completedPartList); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("completedPartList", completedPartList).toString(); + } + + /** + * Creates a new builder for {@link CompletedMultipartUpload}. + * + * @return A new builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** Builder for {@link CompletedMultipartUpload}. */ + public static class Builder { + private List parts; + + private Builder() {} + + /** + * Sets the list of completed parts for the multipart upload. + * + * @param completedPartList The list of completed parts. + * @return This builder. + */ + public Builder parts(List completedPartList) { + this.parts = completedPartList; + return this; + } + + /** + * Builds the {@link CompletedMultipartUpload} object. + * + * @return The new {@link CompletedMultipartUpload} object. + */ + public CompletedMultipartUpload build() { + return new CompletedMultipartUpload(this); + } + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CompletedPart.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CompletedPart.java new file mode 100644 index 0000000000..c86925fbd0 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CompletedPart.java @@ -0,0 +1,97 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.google.cloud.storage.multipartupload.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; + +/** Represents a completed part of a multipart upload. */ +public final class CompletedPart { + + @JacksonXmlProperty(localName = "PartNumber") + private final int partNumber; + + @JacksonXmlProperty(localName = "ETag") + private final String eTag; + + private CompletedPart(int partNumber, String eTag) { + this.partNumber = partNumber; + this.eTag = eTag; + } + + /** + * Creates a new builder for {@link CompletedPart}. + * + * @return A new builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the part number of this completed part. + * + * @return The part number. + */ + public int partNumber() { + return partNumber; + } + + /** + * Returns the ETag of this completed part. + * + * @return The ETag. + */ + public String eTag() { + return eTag; + } + + /** Builder for {@link CompletedPart}. */ + public static class Builder { + private int partNumber; + private String etag; + + /** + * Sets the part number of the completed part. + * + * @param partNumber The part number. + * @return This builder. + */ + public Builder partNumber(int partNumber) { + this.partNumber = partNumber; + return this; + } + + /** + * Sets the ETag of the completed part. + * + * @param etag The ETag. + * @return This builder. + */ + public Builder eTag(String etag) { + this.etag = etag; + return this; + } + + /** + * Builds the {@link CompletedPart} object. + * + * @return The new {@link CompletedPart} object. + */ + public CompletedPart build() { + return new CompletedPart(partNumber, etag); + } + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java index d5c0df0d56..1412e27e45 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java @@ -32,6 +32,10 @@ import com.google.cloud.storage.it.runner.annotations.SingleBackend; import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadResponse; +import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadRequest; +import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadResponse; +import com.google.cloud.storage.multipartupload.model.CompletedMultipartUpload; +import com.google.cloud.storage.multipartupload.model.CompletedPart; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; import com.google.cloud.storage.multipartupload.model.ListPartsRequest; @@ -40,6 +44,7 @@ import com.google.cloud.storage.multipartupload.model.Part; import com.google.cloud.storage.multipartupload.model.UploadPartRequest; import com.google.cloud.storage.multipartupload.model.UploadPartResponse; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.hash.Hashing; import io.grpc.netty.shaded.io.netty.buffer.ByteBuf; @@ -605,6 +610,128 @@ public void sendAbortMultipartUploadRequest_error() throws Exception { } } + @Test + public void sendCompleteMultipartUploadRequest_success() throws Exception { + HttpRequestHandler handler = + req -> { + CompleteMultipartUploadResponse response = + CompleteMultipartUploadResponse.builder() + .bucket("test-bucket") + .key("test-key") + .etag("\"test-etag\"") + .build(); + ByteBuf buf = Unpooled.wrappedBuffer(xmlMapper.writeValueAsBytes(response)); + + DefaultFullHttpResponse resp = + new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); + resp.headers().set(CONTENT_TYPE, "application/xml; charset=utf-8"); + return resp; + }; + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + CompleteMultipartUploadRequest request = + CompleteMultipartUploadRequest.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .multipartUpload( + CompletedMultipartUpload.builder() + .parts( + ImmutableList.of( + CompletedPart.builder().partNumber(1).eTag("\"etag1\"").build(), + CompletedPart.builder().partNumber(2).eTag("\"etag2\"").build())) + .build()) + .build(); + + CompleteMultipartUploadResponse response = + multipartUploadHttpRequestManager.sendCompleteMultipartUploadRequest(endpoint, request); + + assertThat(response).isNotNull(); + assertThat(response.bucket()).isEqualTo("test-bucket"); + assertThat(response.key()).isEqualTo("test-key"); + assertThat(response.etag()).isEqualTo("\"test-etag\""); + } + } + + @Test + public void sendCompleteMultipartUploadRequest_error() throws Exception { + HttpRequestHandler handler = + req -> { + FullHttpResponse resp = + new DefaultFullHttpResponse(req.protocolVersion(), HttpResponseStatus.BAD_REQUEST); + resp.headers().set(CONTENT_TYPE, "text/plain; charset=utf-8"); + return resp; + }; + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + CompleteMultipartUploadRequest request = + CompleteMultipartUploadRequest.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .multipartUpload( + CompletedMultipartUpload.builder() + .parts( + ImmutableList.of( + CompletedPart.builder().partNumber(1).eTag("\"etag1\"").build(), + CompletedPart.builder().partNumber(2).eTag("\"etag2\"").build())) + .build()) + .build(); + + assertThrows( + HttpResponseException.class, + () -> + multipartUploadHttpRequestManager.sendCompleteMultipartUploadRequest( + endpoint, request)); + } + } + + @Test + public void sendCompleteMultipartUploadRequest_body() throws Exception { + HttpRequestHandler handler = + req -> { + FullHttpRequest fullHttpRequest = (FullHttpRequest) req; + ByteBuf content = fullHttpRequest.content(); + String body = content.toString(StandardCharsets.UTF_8); + assertThat(body) + .isEqualTo( + "1\"etag1\"2\"etag2\""); + CompleteMultipartUploadResponse response = + CompleteMultipartUploadResponse.builder() + .bucket("test-bucket") + .key("test-key") + .etag("\"test-etag\"") + .build(); + ByteBuf buf = Unpooled.wrappedBuffer(xmlMapper.writeValueAsBytes(response)); + + DefaultFullHttpResponse resp = + new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); + resp.headers().set(CONTENT_TYPE, "application/xml; charset=utf-8"); + return resp; + }; + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + CompleteMultipartUploadRequest request = + CompleteMultipartUploadRequest.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .multipartUpload( + CompletedMultipartUpload.builder() + .parts( + ImmutableList.of( + CompletedPart.builder().partNumber(1).eTag("\"etag1\"").build(), + CompletedPart.builder().partNumber(2).eTag("\"etag2\"").build())) + .build()) + .build(); + + multipartUploadHttpRequestManager.sendCompleteMultipartUploadRequest(endpoint, request); + } + } + @Test public void sendUploadPartRequest_success() throws Exception { String etag = "\"af1ed31420542285653c803a34aa839a\"";