Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, String> stableHeaders =
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
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 @@ -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));
}
}
}
Loading