Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@

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;
import com.google.cloud.storage.multipartupload.model.ListPartsResponse;
import java.io.IOException;
import java.net.URI;
import java.security.NoSuchAlgorithmException;

/**
* A client for interacting with Google Cloud Storage's Multipart Upload API.
Expand Down Expand Up @@ -60,6 +63,18 @@ public abstract CreateMultipartUploadResponse createMultipartUpload(
@BetaApi
public abstract ListPartsResponse listParts(ListPartsRequest listPartsRequest) throws IOException;

/**
* Aborts a multipart upload.
*
* @param request The request object containing the details for aborting the multipart upload.
* @return An {@link AbortMultipartUploadResponse} object.
* @throws IOException if an I/O error occurs.
* @throws NoSuchAlgorithmException if the specified algorithm is not available.
*/
@BetaApi
public abstract AbortMultipartUploadResponse abortMultipartUpload(
AbortMultipartUploadRequest request) throws IOException, NoSuchAlgorithmException;
Copy link
Collaborator

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?


/**
* Creates a new instance of {@link MultipartUploadClient}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as list, this should be options.getRetryAlgorithmManager().idempotent()

() -> httpRequestManager.sendAbortMultipartUploadRequest(uri, request, options),
Decoder.identity());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DELETEs don't have bodies, do we need to set the content-type for GCS? If not, please remove.

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);
Expand Down
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public static class Builder {
@BetaApi
public static final 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);
}
}
}
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
Expand Up @@ -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;
Expand Down Expand Up @@ -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"
Copy link
Collaborator

Choose a reason for hiding this comment

The 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"
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -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));
}
}
}
Loading