-
Couldn't load subscription status.
- Fork 85
feat: Adding API for AbortMultipartUpload. #3361
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: mpu-feature-3
Are you sure you want to change the base?
Changes from all commits
e0cd719
a8e1aed
c9164f7
18cd58a
55f09ee
d257a51
53ff734
9569a20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,12 +20,15 @@ | |
| 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; | ||
| import com.google.cloud.storage.multipartupload.model.ListPartsResponse; | ||
| import java.io.IOException; | ||
| import java.net.URI; | ||
| import java.security.NoSuchAlgorithmException; | ||
|
|
||
| /** | ||
| * This class is an implementation of {@link MultipartUploadClient} that uses the Google Cloud | ||
|
|
@@ -64,4 +67,15 @@ public ListPartsResponse listParts(ListPartsRequest request) throws IOException | |
| () -> httpRequestManager.sendListPartsRequest(uri, request, options), | ||
| Decoder.identity()); | ||
| } | ||
|
|
||
| @Override | ||
| @BetaApi | ||
| public AbortMultipartUploadResponse abortMultipartUpload(AbortMultipartUploadRequest request) | ||
| throws IOException, NoSuchAlgorithmException { | ||
|
|
||
| return retrier.run( | ||
| Retrying.alwaysRetry(), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as list, this should be |
||
| () -> httpRequestManager.sendAbortMultipartUploadRequest(uri, request, options), | ||
| Decoder.identity()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,8 @@ | |
| import com.google.api.client.http.HttpRequest; | ||
| import com.google.api.client.http.HttpRequestFactory; | ||
| import com.google.api.client.util.ObjectParser; | ||
| 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; | ||
|
|
@@ -91,6 +93,27 @@ ListPartsResponse sendListPartsRequest( | |
| return httpRequest.execute().parseAs(ListPartsResponse.class); | ||
| } | ||
|
|
||
| AbortMultipartUploadResponse sendAbortMultipartUploadRequest( | ||
| URI uri, AbortMultipartUploadRequest request, HttpStorageOptions options) throws IOException { | ||
|
|
||
| String encodedBucket = encode(request.bucket()); | ||
| String encodedKey = encode(request.key()); | ||
| String resourcePath = "/" + encodedBucket + "/" + encodedKey; | ||
| String queryString = "?uploadId=" + encode(request.uploadId()); | ||
| String abortUri = uri.toString() + resourcePath + queryString; | ||
| String contentType = "application/x-www-form-urlencoded"; | ||
| Map<String, String> extensionHeaders = getGenericExtensionHeader(options); | ||
|
|
||
| HttpRequest httpRequest = requestFactory.buildDeleteRequest(new GenericUrl(abortUri)); | ||
| httpRequest.getHeaders().setContentType(contentType); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| for (Map.Entry<String, String> entry : extensionHeaders.entrySet()) { | ||
| httpRequest.getHeaders().set(entry.getKey(), entry.getValue()); | ||
| } | ||
| httpRequest.setParser(objectParser); | ||
| httpRequest.setThrowExceptionOnExecuteError(true); | ||
| return httpRequest.execute().parseAs(AbortMultipartUploadResponse.class); | ||
| } | ||
|
|
||
| private Map<String, String> getExtensionHeadersForCreateMultipartUpload( | ||
| CreateMultipartUploadRequest request, HttpStorageOptions options) { | ||
| Map<String, String> extensionHeaders = getGenericExtensionHeader(options); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,122 @@ | ||||||||
| /* | ||||||||
| * 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. */ | ||||||||
| public static class Builder { | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| 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); | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -396,7 +398,7 @@ public void sendListPartsRequest_success() throws Exception { | |
| + " <IsTruncated>false</IsTruncated>\n" | ||
| + " <Part>\n" | ||
| + " <PartNumber>1</PartNumber>\n" | ||
| + " <ETag>\"etag\"</ETag>\n" | ||
| + " <ETag>etag</ETag>\n" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can these two changes be moved back to the list PR since they're related to that? |
||
| + " <Size>123</Size>\n" | ||
| + " <LastModified>2024-05-08T17:50:00.000Z</LastModified>\n" | ||
| + " </Part>\n" | ||
|
|
@@ -435,7 +437,7 @@ public void sendListPartsRequest_success() throws Exception { | |
| assertThat(response.getParts()).hasSize(1); | ||
| Part part = response.getParts().get(0); | ||
| assertThat(part.partNumber()).isEqualTo(1); | ||
| assertThat(part.eTag()).isEqualTo("\"etag\""); | ||
| assertThat(part.eTag()).isEqualTo("etag"); | ||
| assertThat(part.size()).isEqualTo(123); | ||
| assertThat(part.lastModified()).isEqualTo("2024-05-08T17:50:00.000Z"); | ||
| } | ||
|
|
@@ -467,4 +469,62 @@ public void sendListPartsRequest_error() throws Exception { | |
| endpoint, request, httpStorageOptions)); | ||
| } | ||
| } | ||
|
|
||
| @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, httpStorageOptions); | ||
|
|
||
| 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, httpStorageOptions)); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should never throw a
NoSuchAlgorithmException. What necessitates this being declared here?