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 2c6bd21c5..3a13fb94e 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 @@ -18,6 +18,8 @@ import com.google.api.core.BetaApi; 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.CreateMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; import com.google.cloud.storage.multipartupload.model.ListPartsRequest; @@ -59,6 +61,16 @@ public abstract CreateMultipartUploadResponse createMultipartUpload( @BetaApi public abstract ListPartsResponse listParts(ListPartsRequest listPartsRequest); + /** + * Aborts a multipart upload. + * + * @param request The request object containing the details for aborting the multipart upload. + * @return An {@link AbortMultipartUploadResponse} object. + */ + @BetaApi + public abstract AbortMultipartUploadResponse abortMultipartUpload( + AbortMultipartUploadRequest request); + /** * Creates a new instance of {@link MultipartUploadClient}. * 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 664126a41..6721b3a81 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 @@ -18,6 +18,8 @@ import com.google.api.core.BetaApi; import com.google.cloud.storage.Conversions.Decoder; 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.CreateMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; import com.google.cloud.storage.multipartupload.model.ListPartsRequest; @@ -64,4 +66,13 @@ public ListPartsResponse listParts(ListPartsRequest request) { () -> httpRequestManager.sendListPartsRequest(uri, request), Decoder.identity()); } + + @Override + @BetaApi + public AbortMultipartUploadResponse abortMultipartUpload(AbortMultipartUploadRequest request) { + return retrier.run( + retryAlgorithmManager.idempotent(), + () -> httpRequestManager.sendAbortMultipartUploadRequest(uri, request), + Decoder.identity()); + } } 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 a8f23a1b0..8827f49ca 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 @@ -28,6 +28,8 @@ import com.google.api.gax.rpc.FixedHeaderProvider; import com.google.api.gax.rpc.HeaderProvider; import com.google.api.services.storage.Storage; +import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadRequest; +import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadResponse; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; import com.google.cloud.storage.multipartupload.model.ListPartsRequest; @@ -95,7 +97,21 @@ ListPartsResponse sendListPartsRequest(URI uri, ListPartsRequest request) throws return httpRequest.execute().parseAs(ListPartsResponse.class); } - @SuppressWarnings("DataFlowIssue") + AbortMultipartUploadResponse sendAbortMultipartUploadRequest( + URI uri, AbortMultipartUploadRequest request) throws IOException { + + String encodedBucket = urlEncode(request.bucket()); + String encodedKey = urlEncode(request.key()); + String resourcePath = "/" + encodedBucket + "/" + encodedKey; + String queryString = "?uploadId=" + urlEncode(request.uploadId()); + String abortUri = uri.toString() + resourcePath + queryString; + + HttpRequest httpRequest = requestFactory.buildDeleteRequest(new GenericUrl(abortUri)); + httpRequest.setParser(objectParser); + httpRequest.setThrowExceptionOnExecuteError(true); + return httpRequest.execute().parseAs(AbortMultipartUploadResponse.class); + } + static MultipartUploadHttpRequestManager createFrom(HttpStorageOptions options) { Storage storage = options.getStorageRpcV1().getStorage(); ImmutableMap.Builder stableHeaders = diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/AbortMultipartUploadRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/AbortMultipartUploadRequest.java new file mode 100644 index 000000000..e33b1b386 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/AbortMultipartUploadRequest.java @@ -0,0 +1,123 @@ +/* + * 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.api.core.BetaApi; + +/** + * Represents a request to abort a multipart upload. This request is used to stop an in-progress + * multipart upload, deleting any previously uploaded parts. + */ +@BetaApi +public final class AbortMultipartUploadRequest { + private final String bucket; + private final String key; + private final String uploadId; + + private AbortMultipartUploadRequest(Builder builder) { + this.bucket = builder.bucket; + this.key = builder.key; + this.uploadId = builder.uploadId; + } + + /** + * Returns the name of the bucket in which the multipart upload is stored. + * + * @return The bucket name. + */ + public String bucket() { + return bucket; + } + + /** + * Returns the name of the object that is being uploaded. + * + * @return The object name. + */ + public String key() { + return key; + } + + /** + * Returns the upload ID of the multipart upload to abort. + * + * @return The upload ID. + */ + public String uploadId() { + return uploadId; + } + + /** + * Returns a new builder for creating {@link AbortMultipartUploadRequest} instances. + * + * @return A new {@link Builder}. + */ + public static Builder builder() { + return new Builder(); + } + + /** A builder for creating {@link AbortMultipartUploadRequest} instances. */ + @BetaApi + public static class Builder { + private String bucket; + private String key; + private String uploadId; + + private Builder() {} + + /** + * Sets the name of the bucket in which the multipart upload is stored. + * + * @param bucket The bucket name. + * @return This builder. + */ + public Builder bucket(String bucket) { + this.bucket = bucket; + return this; + } + + /** + * Sets the name of the object that is being uploaded. + * + * @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 to abort. + * + * @param uploadId The upload ID. + * @return This builder. + */ + public Builder uploadId(String uploadId) { + this.uploadId = uploadId; + return this; + } + + /** + * Builds a new {@link AbortMultipartUploadRequest} instance. + * + * @return A new {@link AbortMultipartUploadRequest}. + */ + public AbortMultipartUploadRequest build() { + return new AbortMultipartUploadRequest(this); + } + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/AbortMultipartUploadResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/AbortMultipartUploadResponse.java new file mode 100644 index 000000000..e5cfc5e47 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/AbortMultipartUploadResponse.java @@ -0,0 +1,25 @@ +/* + * 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.api.core.BetaApi; + +/** + * Represents a response to an abort multipart upload request. This class is currently empty as the + * abort operation does not return any specific data in its response body. + */ +@BetaApi +public final class AbortMultipartUploadResponse {} 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 8d3455a4e..7aa35b9f1 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 @@ -30,6 +30,8 @@ import com.google.cloud.storage.it.runner.annotations.Backend; import com.google.cloud.storage.it.runner.annotations.ParallelFriendly; 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.CreateMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; import com.google.cloud.storage.multipartupload.model.ListPartsRequest; @@ -541,4 +543,60 @@ public void sendListPartsRequest_errorResponse() throws Exception { () -> multipartUploadHttpRequestManager.sendListPartsRequest(endpoint, request)); } } + + @Test + public void sendAbortMultipartUploadRequest_success() throws Exception { + HttpRequestHandler handler = + req -> { + assertThat(req.uri()).contains("?uploadId=test-upload-id"); + AbortMultipartUploadResponse response = new AbortMultipartUploadResponse(); + 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(); + AbortMultipartUploadRequest request = + AbortMultipartUploadRequest.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .build(); + + AbortMultipartUploadResponse response = + multipartUploadHttpRequestManager.sendAbortMultipartUploadRequest(endpoint, request); + + assertThat(response).isNotNull(); + } + } + + @Test + public void sendAbortMultipartUploadRequest_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(); + AbortMultipartUploadRequest request = + AbortMultipartUploadRequest.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .build(); + + assertThrows( + HttpResponseException.class, + () -> + multipartUploadHttpRequestManager.sendAbortMultipartUploadRequest(endpoint, request)); + } + } }