com.google.guava
guava
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java
index b1400bcb62..a73b5b140a 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java
@@ -32,8 +32,6 @@
import com.google.cloud.http.HttpTransportOptions;
import com.google.cloud.spi.ServiceRpcFactory;
import com.google.cloud.storage.BlobWriteSessionConfig.WriterFactory;
-import com.google.cloud.storage.Retrying.DefaultRetrier;
-import com.google.cloud.storage.Retrying.HttpRetrier;
import com.google.cloud.storage.Retrying.RetryingDependencies;
import com.google.cloud.storage.Storage.BlobWriteOption;
import com.google.cloud.storage.TransportCompatibility.Transport;
@@ -408,14 +406,7 @@ public Storage create(StorageOptions options) {
}
WriterFactory factory = blobWriteSessionConfig.createFactory(clock);
StorageImpl storage =
- new StorageImpl(
- httpStorageOptions,
- factory,
- new HttpRetrier(
- new DefaultRetrier(
- OtelStorageDecorator.retryContextDecorator(otel),
- RetryingDependencies.simple(
- options.getClock(), options.getRetrySettings()))));
+ new StorageImpl(httpStorageOptions, factory, options.createRetrier());
return OtelStorageDecorator.decorate(storage, otel, Transport.HTTP);
} catch (IOException e) {
throw new IllegalStateException(
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
new file mode 100644
index 0000000000..2c6bd21c59
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java
@@ -0,0 +1,77 @@
+/*
+ * 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;
+
+import com.google.api.core.BetaApi;
+import com.google.api.core.InternalExtensionOnly;
+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;
+
+/**
+ * A client for interacting with Google Cloud Storage's Multipart Upload API.
+ *
+ * This class is for internal use only and is not intended for public consumption. It provides a
+ * low-level interface for creating and managing multipart uploads.
+ *
+ * @see Multipart Uploads
+ */
+@BetaApi
+@InternalExtensionOnly
+public abstract class MultipartUploadClient {
+
+ MultipartUploadClient() {}
+
+ /**
+ * Creates a new multipart upload.
+ *
+ * @param request The request object containing the details for creating the multipart upload.
+ * @return A {@link CreateMultipartUploadResponse} object containing the upload ID.
+ * @throws IOException if an I/O error occurs.
+ */
+ @BetaApi
+ public abstract CreateMultipartUploadResponse createMultipartUpload(
+ CreateMultipartUploadRequest request) throws IOException;
+
+ /**
+ * Lists the parts that have been uploaded for a specific multipart upload.
+ *
+ * @param listPartsRequest The request object containing the details for listing the parts.
+ * @return A {@link ListPartsResponse} object containing the list of parts.
+ */
+ @BetaApi
+ public abstract ListPartsResponse listParts(ListPartsRequest listPartsRequest);
+
+ /**
+ * Creates a new instance of {@link MultipartUploadClient}.
+ *
+ * @param config The configuration for the client.
+ * @return A new {@link MultipartUploadClient} instance.
+ */
+ @BetaApi
+ public static MultipartUploadClient create(MultipartUploadSettings config) {
+ HttpStorageOptions options = config.getOptions();
+ return new MultipartUploadClientImpl(
+ URI.create(options.getHost()),
+ options.createRetrier(),
+ MultipartUploadHttpRequestManager.createFrom(options),
+ options.getRetryAlgorithmManager());
+ }
+}
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
new file mode 100644
index 0000000000..664126a416
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java
@@ -0,0 +1,67 @@
+/*
+ * 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;
+
+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.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;
+
+/**
+ * This class is an implementation of {@link MultipartUploadClient} that uses the Google Cloud
+ * Storage XML API to perform multipart uploads.
+ */
+@BetaApi
+final class MultipartUploadClientImpl extends MultipartUploadClient {
+
+ private final MultipartUploadHttpRequestManager httpRequestManager;
+ private final Retrier retrier;
+ private final URI uri;
+ private final HttpRetryAlgorithmManager retryAlgorithmManager;
+
+ MultipartUploadClientImpl(
+ URI uri,
+ Retrier retrier,
+ MultipartUploadHttpRequestManager multipartUploadHttpRequestManager,
+ HttpRetryAlgorithmManager retryAlgorithmManager) {
+ this.httpRequestManager = multipartUploadHttpRequestManager;
+ this.retrier = retrier;
+ this.uri = uri;
+ this.retryAlgorithmManager = retryAlgorithmManager;
+ }
+
+ @Override
+ @BetaApi
+ public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUploadRequest request)
+ throws IOException {
+ return httpRequestManager.sendCreateMultipartUploadRequest(uri, request);
+ }
+
+ @Override
+ @BetaApi
+ public ListPartsResponse listParts(ListPartsRequest request) {
+
+ return retrier.run(
+ retryAlgorithmManager.idempotent(),
+ () -> httpRequestManager.sendListPartsRequest(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
new file mode 100644
index 0000000000..a8f23a1b04
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java
@@ -0,0 +1,181 @@
+/*
+ * 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;
+
+import static com.google.cloud.storage.Utils.ifNonNull;
+
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import com.google.api.client.http.ByteArrayContent;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpHeaders;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.util.ObjectParser;
+import com.google.api.gax.core.GaxProperties;
+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.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 com.google.common.base.StandardSystemProperty;
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+final class MultipartUploadHttpRequestManager {
+
+ private final HttpRequestFactory requestFactory;
+ private final ObjectParser objectParser;
+ private final HeaderProvider headerProvider;
+
+ MultipartUploadHttpRequestManager(
+ HttpRequestFactory requestFactory, ObjectParser objectParser, HeaderProvider headerProvider) {
+ this.requestFactory = requestFactory;
+ this.objectParser = objectParser;
+ this.headerProvider = headerProvider;
+ }
+
+ CreateMultipartUploadResponse sendCreateMultipartUploadRequest(
+ URI uri, CreateMultipartUploadRequest request) throws IOException {
+
+ String encodedBucket = urlEncode(request.bucket());
+ String encodedKey = urlEncode(request.key());
+ String resourcePath = "/" + encodedBucket + "/" + encodedKey;
+ String createUri = uri.toString() + resourcePath + "?uploads";
+
+ HttpRequest httpRequest =
+ requestFactory.buildPostRequest(
+ new GenericUrl(createUri), new ByteArrayContent(request.getContentType(), new byte[0]));
+ httpRequest.getHeaders().putAll(headerProvider.getHeaders());
+ addHeadersForCreateMultipartUpload(request, httpRequest.getHeaders());
+ httpRequest.setParser(objectParser);
+ httpRequest.setThrowExceptionOnExecuteError(true);
+ return httpRequest.execute().parseAs(CreateMultipartUploadResponse.class);
+ }
+
+ ListPartsResponse sendListPartsRequest(URI uri, ListPartsRequest request) throws IOException {
+
+ String encodedBucket = urlEncode(request.bucket());
+ String encodedKey = urlEncode(request.key());
+ String resourcePath = "/" + encodedBucket + "/" + encodedKey;
+ String queryString = "?uploadId=" + urlEncode(request.uploadId());
+
+ if (request.getMaxParts() != null) {
+ queryString += "&max-parts=" + request.getMaxParts();
+ }
+ if (request.getPartNumberMarker() != null) {
+ queryString += "&part-number-marker=" + request.getPartNumberMarker();
+ }
+ String listUri = uri.toString() + resourcePath + queryString;
+ HttpRequest httpRequest = requestFactory.buildGetRequest(new GenericUrl(listUri));
+ httpRequest.getHeaders().putAll(headerProvider.getHeaders());
+ httpRequest.setParser(objectParser);
+ httpRequest.setThrowExceptionOnExecuteError(true);
+ return httpRequest.execute().parseAs(ListPartsResponse.class);
+ }
+
+ @SuppressWarnings("DataFlowIssue")
+ static MultipartUploadHttpRequestManager createFrom(HttpStorageOptions options) {
+ Storage storage = options.getStorageRpcV1().getStorage();
+ ImmutableMap.Builder stableHeaders =
+ ImmutableMap.builder()
+ // http-java-client will automatically append its own version to the user-agent
+ .put("User-Agent", "gcloud-java/" + options.getLibraryVersion())
+ .put(
+ "x-goog-api-client",
+ String.format(
+ "gl-java/%s gccl/%s %s/%s",
+ GaxProperties.getJavaVersion(),
+ options.getLibraryVersion(),
+ formatName(StandardSystemProperty.OS_NAME.value()),
+ formatSemver(StandardSystemProperty.OS_VERSION.value())));
+ ifNonNull(options.getProjectId(), pid -> stableHeaders.put("x-goog-user-project", pid));
+ return new MultipartUploadHttpRequestManager(
+ storage.getRequestFactory(),
+ new XmlObjectParser(new XmlMapper()),
+ options.getMergedHeaderProvider(FixedHeaderProvider.create(stableHeaders.build())));
+ }
+
+ private void addHeadersForCreateMultipartUpload(
+ CreateMultipartUploadRequest request, HttpHeaders headers) {
+ // TODO(shreyassinha): add a PredefinedAcl::getXmlEntry with the corresponding value from
+ // https://cloud.google.com/storage/docs/xml-api/reference-headers#xgoogacl
+ if (request.getCannedAcl() != null) {
+ headers.put("x-goog-acl", request.getCannedAcl().toString());
+ }
+ // TODO(shreyassinha) Add encoding for x-goog-meta-* headers
+ if (request.getMetadata() != null) {
+ for (Map.Entry entry : request.getMetadata().entrySet()) {
+ if (entry.getKey() != null || entry.getValue() != null) {
+ headers.put("x-goog-meta-" + entry.getKey(), entry.getValue());
+ }
+ }
+ }
+ if (request.getStorageClass() != null) {
+ headers.put("x-goog-storage-class", request.getStorageClass().toString());
+ }
+ if (request.getKmsKeyName() != null && !request.getKmsKeyName().isEmpty()) {
+ headers.put("x-goog-encryption-kms-key-name", request.getKmsKeyName());
+ }
+ if (request.getObjectLockMode() != null) {
+ headers.put("x-goog-object-lock-mode", request.getObjectLockMode().toString());
+ }
+ if (request.getObjectLockRetainUntilDate() != null) {
+ headers.put(
+ "x-goog-object-lock-retain-until-date",
+ Utils.offsetDateTimeRfc3339Codec.encode(request.getObjectLockRetainUntilDate()));
+ }
+ if (request.getCustomTime() != null) {
+ headers.put(
+ "x-goog-custom-time", Utils.offsetDateTimeRfc3339Codec.encode(request.getCustomTime()));
+ }
+ }
+
+ private static String urlEncode(String value) throws UnsupportedEncodingException {
+ return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
+ }
+
+ private static String formatName(String name) {
+ // Only lowercase letters, digits, and "-" are allowed
+ return name.toLowerCase().replaceAll("[^\\w\\d\\-]", "-");
+ }
+
+ private static String formatSemver(String version) {
+ return formatSemver(version, version);
+ }
+
+ private static String formatSemver(String version, String defaultValue) {
+ if (version == null) {
+ return null;
+ }
+
+ // Take only the semver version: x.y.z-a_b_c -> x.y.z
+ Matcher m = Pattern.compile("(\\d+\\.\\d+\\.\\d+).*").matcher(version);
+ if (m.find()) {
+ return m.group(1);
+ } else {
+ return defaultValue;
+ }
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadSettings.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadSettings.java
new file mode 100644
index 0000000000..fbf55b3bfd
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadSettings.java
@@ -0,0 +1,59 @@
+/*
+ * 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;
+
+import com.google.api.core.BetaApi;
+
+/**
+ * Settings for configuring the {@link MultipartUploadClient}.
+ *
+ * This class is for internal use only and is not intended for public consumption.
+ */
+@BetaApi
+public final class MultipartUploadSettings {
+ private final HttpStorageOptions options;
+
+ /**
+ * Constructs a {@code MultipartUploadSettings} instance.
+ *
+ * @param options The {@link HttpStorageOptions} to use for multipart uploads.
+ */
+ private MultipartUploadSettings(HttpStorageOptions options) {
+ this.options = options;
+ }
+
+ /**
+ * Returns the {@link HttpStorageOptions} configured for multipart uploads.
+ *
+ * @return The {@link HttpStorageOptions}.
+ */
+ @BetaApi
+ public HttpStorageOptions getOptions() {
+ return options;
+ }
+
+ /**
+ * Creates a new {@code MultipartUploadSettings} instance with the specified {@link
+ * HttpStorageOptions}.
+ *
+ * @param options The {@link HttpStorageOptions} to use.
+ * @return A new {@code MultipartUploadSettings} instance.
+ */
+ @BetaApi
+ public static MultipartUploadSettings of(HttpStorageOptions options) {
+ return new MultipartUploadSettings(options);
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClass.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClass.java
index 07efcbf842..1986b524c4 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClass.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClass.java
@@ -15,6 +15,7 @@
*/
package com.google.cloud.storage;
+import com.fasterxml.jackson.annotation.JsonCreator;
import com.google.api.core.ApiFunction;
import com.google.cloud.StringEnumType;
import com.google.cloud.StringEnumValue;
@@ -111,7 +112,11 @@ public static StorageClass valueOfStrict(String constant) {
}
/** Get the StorageClass for the given String constant, and allow unrecognized values. */
+ @JsonCreator
public static StorageClass valueOf(String constant) {
+ if (constant == null || constant.isEmpty()) {
+ return null;
+ }
return type.valueOf(constant);
}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java
index 4dac2b43ef..27306b6d6f 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java
@@ -26,6 +26,10 @@
import com.google.cloud.storage.HttpStorageOptions.HttpStorageDefaults;
import com.google.cloud.storage.HttpStorageOptions.HttpStorageFactory;
import com.google.cloud.storage.HttpStorageOptions.HttpStorageRpcFactory;
+import com.google.cloud.storage.Retrying.DefaultRetrier;
+import com.google.cloud.storage.Retrying.HttpRetrier;
+import com.google.cloud.storage.Retrying.Retrier;
+import com.google.cloud.storage.Retrying.RetryingDependencies;
import com.google.cloud.storage.Storage.BlobWriteOption;
import com.google.cloud.storage.TransportCompatibility.Transport;
import com.google.cloud.storage.spi.StorageRpcFactory;
@@ -68,6 +72,13 @@ public abstract class StorageOptions extends ServiceOptions durationSecondsCodec =
Codec.of(Duration::getSeconds, Duration::ofSeconds);
+ static final Codec offsetDateTimeRfc3339Codec =
+ Codec.of(
+ RFC_3339_DATE_TIME_FORMATTER::format,
+ s -> OffsetDateTime.parse(s, RFC_3339_DATE_TIME_FORMATTER));
+
@VisibleForTesting
static final Codec dateTimeCodec =
Codec.of(
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java
new file mode 100644
index 0000000000..7b56832c76
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java
@@ -0,0 +1,65 @@
+/*
+ * 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;
+
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.google.api.client.util.ObjectParser;
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.lang.reflect.Type;
+import java.nio.charset.Charset;
+
+final class XmlObjectParser implements ObjectParser {
+ private final XmlMapper xmlMapper;
+
+ @VisibleForTesting
+ public XmlObjectParser(XmlMapper xmlMapper) {
+ this.xmlMapper = xmlMapper;
+ this.xmlMapper.registerModule(new JavaTimeModule());
+ }
+
+ @Override
+ public T parseAndClose(InputStream in, Charset charset, Class dataClass)
+ throws IOException {
+ return parseAndClose(new InputStreamReader(in, charset), dataClass);
+ }
+
+ @Override
+ public Object parseAndClose(InputStream in, Charset charset, Type dataType) throws IOException {
+ throw new UnsupportedOperationException(
+ "XmlObjectParse#"
+ + CrossTransportUtils.fmtMethodName(
+ "parseAndClose", InputStream.class, Charset.class, Type.class));
+ }
+
+ @Override
+ public T parseAndClose(Reader reader, Class dataClass) throws IOException {
+ try (Reader r = reader) {
+ return xmlMapper.readValue(r, dataClass);
+ }
+ }
+
+ @Override
+ public Object parseAndClose(Reader reader, Type dataType) throws IOException {
+ throw new UnsupportedOperationException(
+ "XmlObjectParse#"
+ + CrossTransportUtils.fmtMethodName("parseAndClose", Reader.class, Type.class));
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java
new file mode 100644
index 0000000000..44161ad160
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java
@@ -0,0 +1,348 @@
+/*
+ * 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;
+import com.google.cloud.storage.Storage.PredefinedAcl;
+import com.google.cloud.storage.StorageClass;
+import com.google.common.base.MoreObjects;
+import java.time.OffsetDateTime;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Represents a request to initiate a multipart upload. This class holds all the necessary
+ * information to create a new multipart upload session.
+ */
+@BetaApi
+public final class CreateMultipartUploadRequest {
+ private final String bucket;
+ private final String key;
+ private final PredefinedAcl cannedAcl;
+ private final String contentType;
+ private final Map metadata;
+ private final StorageClass storageClass;
+ private final OffsetDateTime customTime;
+ private final String kmsKeyName;
+ private final ObjectLockMode objectLockMode;
+ private final OffsetDateTime objectLockRetainUntilDate;
+
+ private CreateMultipartUploadRequest(Builder builder) {
+ this.bucket = builder.bucket;
+ this.key = builder.key;
+ this.cannedAcl = builder.cannedAcl;
+ this.contentType = builder.contentType;
+ this.metadata = builder.metadata;
+ this.storageClass = builder.storageClass;
+ this.customTime = builder.customTime;
+ this.kmsKeyName = builder.kmsKeyName;
+ this.objectLockMode = builder.objectLockMode;
+ this.objectLockRetainUntilDate = builder.objectLockRetainUntilDate;
+ }
+
+ /**
+ * Returns the name of the bucket to which the object is being uploaded.
+ *
+ * @return The bucket name
+ */
+ public String bucket() {
+ return bucket;
+ }
+
+ /**
+ * Returns the name of the object.
+ *
+ * @see Object Naming
+ * @return The object name
+ */
+ public String key() {
+ return key;
+ }
+
+ /**
+ * Returns a canned ACL to apply to the object.
+ *
+ * @return The canned ACL
+ */
+ public PredefinedAcl getCannedAcl() {
+ return cannedAcl;
+ }
+
+ /**
+ * Returns the MIME type of the data you are uploading.
+ *
+ * @return The Content-Type
+ */
+ public String getContentType() {
+ return contentType;
+ }
+
+ /**
+ * Returns the custom metadata of the object.
+ *
+ * @return The custom metadata
+ */
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * Returns the storage class for the object.
+ *
+ * @return The Storage-Class
+ */
+ public StorageClass getStorageClass() {
+ return storageClass;
+ }
+
+ /**
+ * Returns a user-specified date and time.
+ *
+ * @return The custom time
+ */
+ public OffsetDateTime getCustomTime() {
+ return customTime;
+ }
+
+ /**
+ * Returns the customer-managed encryption key to use to encrypt the object.
+ *
+ * @return The Cloud KMS key
+ */
+ public String getKmsKeyName() {
+ return kmsKeyName;
+ }
+
+ /**
+ * Returns the mode of the object's retention configuration.
+ *
+ * @return The object lock mode
+ */
+ public ObjectLockMode getObjectLockMode() {
+ return objectLockMode;
+ }
+
+ /**
+ * Returns the date that determines the time until which the object is retained as immutable.
+ *
+ * @return The object lock retention until date
+ */
+ public OffsetDateTime getObjectLockRetainUntilDate() {
+ return objectLockRetainUntilDate;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof CreateMultipartUploadRequest)) {
+ return false;
+ }
+ CreateMultipartUploadRequest that = (CreateMultipartUploadRequest) o;
+ return Objects.equals(bucket, that.bucket)
+ && Objects.equals(key, that.key)
+ && cannedAcl == that.cannedAcl
+ && Objects.equals(contentType, that.contentType)
+ && Objects.equals(metadata, that.metadata)
+ && Objects.equals(storageClass, that.storageClass)
+ && Objects.equals(customTime, that.customTime)
+ && Objects.equals(kmsKeyName, that.kmsKeyName)
+ && objectLockMode == that.objectLockMode
+ && Objects.equals(objectLockRetainUntilDate, that.objectLockRetainUntilDate);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ bucket,
+ key,
+ cannedAcl,
+ contentType,
+ metadata,
+ storageClass,
+ customTime,
+ kmsKeyName,
+ objectLockMode,
+ objectLockRetainUntilDate);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("bucket", bucket)
+ .add("key", key)
+ .add("cannedAcl", cannedAcl)
+ .add("contentType", contentType)
+ .add("metadata", metadata)
+ .add("storageClass", storageClass)
+ .add("customTime", customTime)
+ .add("kmsKeyName", kmsKeyName)
+ .add("objectLockMode", objectLockMode)
+ .add("objectLockRetainUntilDate", objectLockRetainUntilDate)
+ .toString();
+ }
+
+ /**
+ * Returns a new {@link Builder} for creating a {@link CreateMultipartUploadRequest}.
+ *
+ * @return a new builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @BetaApi
+ public static final class Builder {
+ private String bucket;
+ private String key;
+ private PredefinedAcl cannedAcl;
+ private String contentType;
+ private Map metadata;
+ private StorageClass storageClass;
+ private OffsetDateTime customTime;
+ private String kmsKeyName;
+ private ObjectLockMode objectLockMode;
+ private OffsetDateTime objectLockRetainUntilDate;
+
+ private Builder() {}
+
+ /**
+ * The bucket to which the object is being uploaded.
+ *
+ * @param bucket The bucket name
+ * @return this builder
+ */
+ public Builder bucket(String bucket) {
+ this.bucket = bucket;
+ return this;
+ }
+
+ /**
+ * The name of the object.
+ *
+ * @see Object Naming
+ * @param key The object name
+ * @return this builder
+ */
+ public Builder key(String key) {
+ this.key = key;
+ return this;
+ }
+
+ /**
+ * A canned ACL to apply to the object.
+ *
+ * @param cannedAcl The canned ACL
+ * @return this builder
+ */
+ public Builder cannedAcl(PredefinedAcl cannedAcl) {
+ this.cannedAcl = cannedAcl;
+ return this;
+ }
+
+ /**
+ * The MIME type of the data you are uploading.
+ *
+ * @param contentType The Content-Type
+ * @return this builder
+ */
+ public Builder contentType(String contentType) {
+ this.contentType = contentType;
+ return this;
+ }
+
+ /**
+ * The custom metadata of the object.
+ *
+ * @param metadata The custom metadata
+ * @return this builder
+ */
+ public Builder metadata(Map metadata) {
+ this.metadata = metadata;
+ return this;
+ }
+
+ /**
+ * Gives each part of the upload and the resulting object a storage class besides the default
+ * storage class of the associated bucket.
+ *
+ * @param storageClass The Storage-Class
+ * @return this builder
+ */
+ public Builder storageClass(StorageClass storageClass) {
+ this.storageClass = storageClass;
+ return this;
+ }
+
+ /**
+ * A user-specified date and time.
+ *
+ * @param customTime The custom time
+ * @return this builder
+ */
+ public Builder customTime(OffsetDateTime customTime) {
+ this.customTime = customTime;
+ return this;
+ }
+
+ /**
+ * The customer-managed encryption key to use to encrypt the object. Refer: Customer
+ * Managed Keys
+ *
+ * @param kmsKeyName The Cloud KMS key
+ * @return this builder
+ */
+ public Builder kmsKeyName(String kmsKeyName) {
+ this.kmsKeyName = kmsKeyName;
+ return this;
+ }
+
+ /**
+ * Mode of the object's retention configuration. GOVERNANCE corresponds to unlocked mode, and
+ * COMPLIANCE corresponds to locked mode.
+ *
+ * @param objectLockMode The object lock mode
+ * @return this builder
+ */
+ public Builder objectLockMode(ObjectLockMode objectLockMode) {
+ this.objectLockMode = objectLockMode;
+ return this;
+ }
+
+ /**
+ * Date that determines the time until which the object is retained as immutable.
+ *
+ * @param objectLockRetainUntilDate The object lock retention until date
+ * @return this builder
+ */
+ public Builder objectLockRetainUntilDate(OffsetDateTime objectLockRetainUntilDate) {
+ this.objectLockRetainUntilDate = objectLockRetainUntilDate;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link CreateMultipartUploadRequest} object.
+ *
+ * @return a new {@link CreateMultipartUploadRequest} object
+ */
+ public CreateMultipartUploadRequest build() {
+ return new CreateMultipartUploadRequest(this);
+ }
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java
new file mode 100644
index 0000000000..f9a003ce67
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java
@@ -0,0 +1,166 @@
+/*
+ * 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;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
+import com.google.api.core.BetaApi;
+import com.google.common.base.MoreObjects;
+import java.util.Objects;
+
+/**
+ * Represents the response from a CreateMultipartUpload request. This class encapsulates the details
+ * of the initiated multipart upload, including the bucket, key, and the unique upload ID.
+ */
+@JacksonXmlRootElement(localName = "InitiateMultipartUploadResult")
+@BetaApi
+public final class CreateMultipartUploadResponse {
+
+ @JacksonXmlProperty(localName = "Bucket")
+ private String bucket;
+
+ @JacksonXmlProperty(localName = "Key")
+ private String key;
+
+ @JacksonXmlProperty(localName = "UploadId")
+ private String uploadId;
+
+ private CreateMultipartUploadResponse() {}
+
+ private CreateMultipartUploadResponse(Builder builder) {
+ this.bucket = builder.bucket;
+ this.key = builder.key;
+ this.uploadId = builder.uploadId;
+ }
+
+ /**
+ * Returns the name of the bucket where the multipart upload was initiated.
+ *
+ * @return The bucket name.
+ */
+ public String bucket() {
+ return bucket;
+ }
+
+ /**
+ * Returns the key (object name) for which the multipart upload was initiated.
+ *
+ * @return The object key.
+ */
+ public String key() {
+ return key;
+ }
+
+ /**
+ * Returns the unique identifier for this multipart upload. This ID must be included in all
+ * subsequent requests related to this upload (e.g., uploading parts, completing the upload).
+ *
+ * @return The upload ID.
+ */
+ public String uploadId() {
+ return uploadId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof CreateMultipartUploadResponse)) {
+ return false;
+ }
+ CreateMultipartUploadResponse that = (CreateMultipartUploadResponse) o;
+ return Objects.equals(bucket, that.bucket)
+ && Objects.equals(key, that.key)
+ && Objects.equals(uploadId, that.uploadId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(bucket, key, uploadId);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("bucket", bucket)
+ .add("key", key)
+ .add("uploadId", uploadId)
+ .toString();
+ }
+
+ /**
+ * Creates a new builder for {@link CreateMultipartUploadResponse}.
+ *
+ * @return A new builder.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** A builder for {@link CreateMultipartUploadResponse} objects. */
+ @BetaApi
+ public static final class Builder {
+ private String bucket;
+ private String key;
+ private String uploadId;
+
+ private Builder() {}
+
+ /**
+ * Sets the bucket name for the multipart upload.
+ *
+ * @param bucket The bucket name.
+ * @return This builder.
+ */
+ public Builder bucket(String bucket) {
+ this.bucket = bucket;
+ return this;
+ }
+
+ /**
+ * Sets the key (object name) for the multipart upload.
+ *
+ * @param key The object key.
+ * @return This builder.
+ */
+ public Builder key(String key) {
+ this.key = key;
+ return this;
+ }
+
+ /**
+ * Sets the upload ID for the multipart upload.
+ *
+ * @param uploadId The upload ID.
+ * @return This builder.
+ */
+ public Builder uploadId(String uploadId) {
+ this.uploadId = uploadId;
+ return this;
+ }
+
+ /**
+ * Builds a new {@link CreateMultipartUploadResponse} object.
+ *
+ * @return A new {@link CreateMultipartUploadResponse} object.
+ */
+ public CreateMultipartUploadResponse build() {
+ return new CreateMultipartUploadResponse(this);
+ }
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java
new file mode 100644
index 0000000000..51d23daf16
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java
@@ -0,0 +1,205 @@
+/*
+ * 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;
+import com.google.common.base.MoreObjects;
+import java.util.Objects;
+
+/** Represents a request to list the parts of a multipart upload. */
+@BetaApi
+public final class ListPartsRequest {
+ private final String bucket;
+
+ private final String key;
+
+ private final String uploadId;
+
+ private final Integer maxParts;
+
+ private final Integer partNumberMarker;
+
+ private ListPartsRequest(Builder builder) {
+ this.bucket = builder.bucket;
+ this.key = builder.key;
+ this.uploadId = builder.uploadId;
+ this.maxParts = builder.maxParts;
+ this.partNumberMarker = builder.partNumberMarker;
+ }
+
+ /**
+ * 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.
+ *
+ * @return the upload ID.
+ */
+ public String uploadId() {
+ return uploadId;
+ }
+
+ /**
+ * Returns the maximum number of parts to return.
+ *
+ * @return the maximum number of parts to return.
+ */
+ public Integer getMaxParts() {
+ return maxParts;
+ }
+
+ /**
+ * Returns the part number marker.
+ *
+ * @return the part number marker.
+ */
+ public Integer getPartNumberMarker() {
+ return partNumberMarker;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ListPartsRequest)) {
+ return false;
+ }
+ ListPartsRequest that = (ListPartsRequest) o;
+ return Objects.equals(bucket, that.bucket)
+ && Objects.equals(key, that.key)
+ && Objects.equals(uploadId, that.uploadId)
+ && Objects.equals(maxParts, that.maxParts)
+ && Objects.equals(partNumberMarker, that.partNumberMarker);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(bucket, key, uploadId, maxParts, partNumberMarker);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("bucket", bucket)
+ .add("key", key)
+ .add("uploadId", uploadId)
+ .add("maxParts", maxParts)
+ .add("partNumberMarker", partNumberMarker)
+ .toString();
+ }
+
+ /**
+ * Returns a new builder for this class.
+ *
+ * @return a new builder for this class.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** A builder for {@link ListPartsRequest}. */
+ @BetaApi
+ public static class Builder {
+ private String bucket;
+ private String key;
+ private String uploadId;
+ private Integer maxParts;
+ private Integer partNumberMarker;
+
+ 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.
+ *
+ * @param uploadId the upload ID.
+ * @return this builder.
+ */
+ public Builder uploadId(String uploadId) {
+ this.uploadId = uploadId;
+ return this;
+ }
+
+ /**
+ * Sets the maximum number of parts to return.
+ *
+ * @param maxParts the maximum number of parts to return.
+ * @return this builder.
+ */
+ public Builder maxParts(Integer maxParts) {
+ this.maxParts = maxParts;
+ return this;
+ }
+
+ /**
+ * Sets the part number marker.
+ *
+ * @param partNumberMarker the part number marker.
+ * @return this builder.
+ */
+ public Builder partNumberMarker(Integer partNumberMarker) {
+ this.partNumberMarker = partNumberMarker;
+ return this;
+ }
+
+ /**
+ * Builds a new {@link ListPartsRequest} object.
+ *
+ * @return a new {@link ListPartsRequest} object.
+ */
+ public ListPartsRequest build() {
+ return new ListPartsRequest(this);
+ }
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java
new file mode 100644
index 0000000000..2311190ff3
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java
@@ -0,0 +1,337 @@
+/*
+ * 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.JsonAlias;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import com.google.api.core.BetaApi;
+import com.google.cloud.storage.StorageClass;
+import com.google.common.base.MoreObjects;
+import java.util.List;
+import java.util.Objects;
+
+/** Represents a response to a list parts request. */
+@BetaApi
+public final class ListPartsResponse {
+
+ @JacksonXmlProperty(localName = "Bucket")
+ private String bucket;
+
+ @JacksonXmlProperty(localName = "Key")
+ private String key;
+
+ @JacksonXmlProperty(localName = "UploadId")
+ private String uploadId;
+
+ @JacksonXmlProperty(localName = "PartNumberMarker")
+ private int partNumberMarker;
+
+ @JacksonXmlProperty(localName = "NextPartNumberMarker")
+ private int nextPartNumberMarker;
+
+ @JacksonXmlProperty(localName = "MaxParts")
+ private int maxParts;
+
+ @JsonAlias("truncated") // S3 returns "truncated", GCS returns "IsTruncated"
+ @JacksonXmlProperty(localName = "IsTruncated")
+ private boolean isTruncated;
+
+ @JacksonXmlProperty(localName = "StorageClass")
+ private StorageClass storageClass;
+
+ @JacksonXmlElementWrapper(useWrapping = false)
+ @JacksonXmlProperty(localName = "Part")
+ private List parts;
+
+ private ListPartsResponse() {}
+
+ private ListPartsResponse(Builder builder) {
+ this.bucket = builder.bucket;
+ this.key = builder.key;
+ this.uploadId = builder.uploadId;
+ this.partNumberMarker = builder.partNumberMarker;
+ this.nextPartNumberMarker = builder.nextPartNumberMarker;
+ this.maxParts = builder.maxParts;
+ this.isTruncated = builder.isTruncated;
+ this.storageClass = builder.storageClass;
+ this.parts = builder.parts;
+ }
+
+ /**
+ * Creates a new {@code Builder} for {@code ListPartsResponse} objects.
+ *
+ * @return A new {@code Builder} instance.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Returns the bucket name.
+ *
+ * @return the bucket name.
+ */
+ public String getBucket() {
+ return bucket;
+ }
+
+ /**
+ * Returns the object name.
+ *
+ * @return the object name.
+ */
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * Returns the upload ID.
+ *
+ * @return the upload ID.
+ */
+ public String getUploadId() {
+ return uploadId;
+ }
+
+ /**
+ * Returns the part number marker.
+ *
+ * @return the part number marker.
+ */
+ public int getPartNumberMarker() {
+ return partNumberMarker;
+ }
+
+ /**
+ * Returns the next part number marker.
+ *
+ * @return the next part number marker.
+ */
+ public int getNextPartNumberMarker() {
+ return nextPartNumberMarker;
+ }
+
+ /**
+ * Returns the maximum number of parts to return.
+ *
+ * @return the maximum number of parts to return.
+ */
+ public int getMaxParts() {
+ return maxParts;
+ }
+
+ /**
+ * Returns true if the response is truncated.
+ *
+ * @return true if the response is truncated.
+ */
+ public boolean isTruncated() {
+ return isTruncated;
+ }
+
+ /**
+ * Returns the storage class of the object.
+ *
+ * @return the storage class of the object.
+ */
+ public StorageClass getStorageClass() {
+ return storageClass;
+ }
+
+ /**
+ * Returns the list of parts.
+ *
+ * @return the list of parts.
+ */
+ public List getParts() {
+ return parts;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ListPartsResponse)) {
+ return false;
+ }
+ ListPartsResponse that = (ListPartsResponse) o;
+ return Objects.equals(bucket, that.bucket)
+ && Objects.equals(key, that.key)
+ && Objects.equals(uploadId, that.uploadId)
+ && Objects.equals(partNumberMarker, that.partNumberMarker)
+ && Objects.equals(nextPartNumberMarker, that.nextPartNumberMarker)
+ && Objects.equals(maxParts, that.maxParts)
+ && Objects.equals(isTruncated, that.isTruncated)
+ && Objects.equals(storageClass, that.storageClass)
+ && Objects.equals(parts, that.parts);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ bucket,
+ key,
+ uploadId,
+ partNumberMarker,
+ nextPartNumberMarker,
+ maxParts,
+ isTruncated,
+ storageClass,
+ parts);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("bucket", bucket)
+ .add("key", key)
+ .add("uploadId", uploadId)
+ .add("partNumberMarker", partNumberMarker)
+ .add("nextPartNumberMarker", nextPartNumberMarker)
+ .add("maxParts", maxParts)
+ .add("isTruncated", isTruncated)
+ .add("storageClass", storageClass)
+ .add("parts", parts)
+ .toString();
+ }
+
+ /** Builder for {@code ListPartsResponse}. */
+ @BetaApi
+ public static final class Builder {
+ private String bucket;
+ private String key;
+ private String uploadId;
+ private int partNumberMarker;
+ private int nextPartNumberMarker;
+ private int maxParts;
+ private boolean isTruncated;
+ private StorageClass storageClass;
+ private List parts;
+
+ private Builder() {}
+
+ /**
+ * Sets the bucket name.
+ *
+ * @param bucket The bucket name.
+ * @return The builder instance.
+ */
+ public Builder setBucket(String bucket) {
+ this.bucket = bucket;
+ return this;
+ }
+
+ /**
+ * Sets the object name.
+ *
+ * @param key The object name.
+ * @return The builder instance.
+ */
+ public Builder setKey(String key) {
+ this.key = key;
+ return this;
+ }
+
+ /**
+ * Sets the upload ID.
+ *
+ * @param uploadId The upload ID.
+ * @return The builder instance.
+ */
+ public Builder setUploadId(String uploadId) {
+ this.uploadId = uploadId;
+ return this;
+ }
+
+ /**
+ * Sets the part number marker.
+ *
+ * @param partNumberMarker The part number marker.
+ * @return The builder instance.
+ */
+ public Builder setPartNumberMarker(int partNumberMarker) {
+ this.partNumberMarker = partNumberMarker;
+ return this;
+ }
+
+ /**
+ * Sets the next part number marker.
+ *
+ * @param nextPartNumberMarker The next part number marker.
+ * @return The builder instance.
+ */
+ public Builder setNextPartNumberMarker(int nextPartNumberMarker) {
+ this.nextPartNumberMarker = nextPartNumberMarker;
+ return this;
+ }
+
+ /**
+ * Sets the maximum number of parts to return.
+ *
+ * @param maxParts The maximum number of parts to return.
+ * @return The builder instance.
+ */
+ public Builder setMaxParts(int maxParts) {
+ this.maxParts = maxParts;
+ return this;
+ }
+
+ /**
+ * Sets whether the response is truncated.
+ *
+ * @param isTruncated True if the response is truncated, false otherwise.
+ * @return The builder instance.
+ */
+ public Builder setIsTruncated(boolean isTruncated) {
+ this.isTruncated = isTruncated;
+ return this;
+ }
+
+ /**
+ * Sets the storage class of the object.
+ *
+ * @param storageClass The storage class of the object.
+ * @return The builder instance.
+ */
+ public Builder setStorageClass(StorageClass storageClass) {
+ this.storageClass = storageClass;
+ return this;
+ }
+
+ /**
+ * Sets the list of parts.
+ *
+ * @param parts The list of parts.
+ * @return The builder instance.
+ */
+ public Builder setParts(List parts) {
+ this.parts = parts;
+ return this;
+ }
+
+ /**
+ * Builds a {@code ListPartsResponse} object.
+ *
+ * @return A new {@code ListPartsResponse} instance.
+ */
+ public ListPartsResponse build() {
+ return new ListPartsResponse(this);
+ }
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ObjectLockMode.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ObjectLockMode.java
new file mode 100644
index 0000000000..a058719e1c
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ObjectLockMode.java
@@ -0,0 +1,78 @@
+/*
+ * 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.ApiFunction;
+import com.google.api.core.BetaApi;
+import com.google.cloud.StringEnumType;
+import com.google.cloud.StringEnumValue;
+
+/**
+ * Represents the object lock mode. See https://cloud.google.com/storage/docs/object-lock
+ * for details.
+ */
+@BetaApi
+public final class ObjectLockMode extends StringEnumValue {
+ private static final long serialVersionUID = -1882734434792102329L;
+
+ private ObjectLockMode(String constant) {
+ super(constant);
+ }
+
+ private static final ApiFunction CONSTRUCTOR =
+ new ApiFunction() {
+ @Override
+ public ObjectLockMode apply(String constant) {
+ return new ObjectLockMode(constant);
+ }
+ };
+
+ private static final StringEnumType type =
+ new StringEnumType(ObjectLockMode.class, CONSTRUCTOR);
+
+ /**
+ * Governance mode. See https://cloud.google.com/storage/docs/object-lock
+ * for details.
+ */
+ public static final ObjectLockMode GOVERNANCE = type.createAndRegister("GOVERNANCE");
+
+ /**
+ * Compliance mode. See https://cloud.google.com/storage/docs/object-lock
+ * for details.
+ */
+ public static final ObjectLockMode COMPLIANCE = type.createAndRegister("COMPLIANCE");
+
+ /**
+ * Get the ObjectLockMode for the given String constant, and throw an exception if the constant is
+ * not recognized.
+ */
+ public static ObjectLockMode valueOfStrict(String constant) {
+ return type.valueOfStrict(constant);
+ }
+
+ /** Get the ObjectLockMode for the given String constant, and allow unrecognized values. */
+ public static ObjectLockMode valueOf(String constant) {
+ return type.valueOf(constant);
+ }
+
+ /** Return the known values for ObjectLockMode. */
+ public static ObjectLockMode[] values() {
+ return type.values();
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java
new file mode 100644
index 0000000000..61e639b823
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java
@@ -0,0 +1,188 @@
+/*
+ * 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;
+import com.google.api.core.BetaApi;
+import com.google.common.base.MoreObjects;
+import java.time.OffsetDateTime;
+import java.util.Objects;
+
+/** Represents a part of a multipart upload. */
+public final class Part {
+
+ @JacksonXmlProperty(localName = "PartNumber")
+ private int partNumber;
+
+ @JacksonXmlProperty(localName = "ETag")
+ private String eTag;
+
+ @JacksonXmlProperty(localName = "Size")
+ private long size;
+
+ @JacksonXmlProperty(localName = "LastModified")
+ private OffsetDateTime lastModified;
+
+ // for jackson
+ private Part() {}
+
+ private Part(Builder builder) {
+ this.partNumber = builder.partNumber;
+ this.eTag = builder.eTag;
+ this.size = builder.size;
+ this.lastModified = builder.lastModified;
+ }
+
+ /**
+ * Returns the part number.
+ *
+ * @return the part number.
+ */
+ public int partNumber() {
+ return partNumber;
+ }
+
+ /**
+ * Returns the ETag of the part.
+ *
+ * @return the ETag of the part.
+ */
+ public String eTag() {
+ return eTag;
+ }
+
+ /**
+ * Returns the size of the part.
+ *
+ * @return the size of the part.
+ */
+ public long size() {
+ return size;
+ }
+
+ /**
+ * Returns the last modified time of the part.
+ *
+ * @return the last modified time of the part.
+ */
+ public OffsetDateTime lastModified() {
+ return lastModified;
+ }
+
+ /**
+ * Returns a new builder for this class.
+ *
+ * @return a new builder for this class.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Part)) {
+ return false;
+ }
+ Part that = (Part) o;
+ return Objects.equals(partNumber, that.partNumber)
+ && Objects.equals(eTag, that.eTag)
+ && Objects.equals(size, that.size)
+ && Objects.equals(lastModified, that.lastModified);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(partNumber, eTag, size, lastModified);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("partNumber", partNumber)
+ .add("eTag", eTag)
+ .add("size", size)
+ .add("lastModified", lastModified)
+ .toString();
+ }
+
+ /** A builder for {@link Part}. */
+ @BetaApi
+ public static final class Builder {
+ private int partNumber;
+ private String eTag;
+ private long size;
+ private OffsetDateTime lastModified;
+
+ private Builder() {}
+
+ /**
+ * Sets the part number.
+ *
+ * @param partNumber the part number.
+ * @return this builder.
+ */
+ public Builder partNumber(int partNumber) {
+ this.partNumber = partNumber;
+ return this;
+ }
+
+ /**
+ * Sets the ETag of the part.
+ *
+ * @param eTag the ETag of the part.
+ * @return this builder.
+ */
+ public Builder eTag(String eTag) {
+ this.eTag = eTag;
+ return this;
+ }
+
+ /**
+ * Sets the size of the part.
+ *
+ * @param size the size of the part.
+ * @return this builder.
+ */
+ public Builder size(long size) {
+ this.size = size;
+ return this;
+ }
+
+ /**
+ * Sets the last modified time of the part.
+ *
+ * @param lastModified the last modified time of the part.
+ * @return this builder.
+ */
+ public Builder lastModified(OffsetDateTime lastModified) {
+ this.lastModified = lastModified;
+ return this;
+ }
+
+ /**
+ * Builds a new {@link Part} object.
+ *
+ * @return a new {@link Part} object.
+ */
+ public Part build() {
+ return new Part(this);
+ }
+ }
+}
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
new file mode 100644
index 0000000000..8d3455a4e3
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java
@@ -0,0 +1,544 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.grpc.netty.shaded.io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
+import static io.grpc.netty.shaded.io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static org.junit.Assert.assertThrows;
+
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.google.api.client.http.HttpResponseException;
+import com.google.cloud.NoCredentials;
+import com.google.cloud.storage.FakeHttpServer.HttpRequestHandler;
+import com.google.cloud.storage.it.runner.StorageITRunner;
+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.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 com.google.cloud.storage.multipartupload.model.ObjectLockMode;
+import com.google.cloud.storage.multipartupload.model.Part;
+import com.google.common.collect.ImmutableMap;
+import io.grpc.netty.shaded.io.netty.buffer.ByteBuf;
+import io.grpc.netty.shaded.io.netty.buffer.Unpooled;
+import io.grpc.netty.shaded.io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.grpc.netty.shaded.io.netty.handler.codec.http.FullHttpResponse;
+import io.grpc.netty.shaded.io.netty.handler.codec.http.HttpResponseStatus;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.Collections;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+
+@RunWith(StorageITRunner.class)
+@SingleBackend(Backend.PROD)
+@ParallelFriendly
+public final class ITMultipartUploadHttpRequestManagerTest {
+ private static final XmlMapper xmlMapper;
+
+ static {
+ xmlMapper = new XmlMapper();
+ xmlMapper.registerModule(new JavaTimeModule());
+ }
+
+ private MultipartUploadHttpRequestManager multipartUploadHttpRequestManager;
+ @Rule public final TemporaryFolder temp = new TemporaryFolder();
+
+ @Before
+ public void setUp() throws Exception {
+ HttpStorageOptions httpStorageOptions =
+ HttpStorageOptions.newBuilder()
+ .setProjectId("test-project")
+ .setCredentials(NoCredentials.getInstance())
+ .build();
+ multipartUploadHttpRequestManager =
+ MultipartUploadHttpRequestManager.createFrom(httpStorageOptions);
+ }
+
+ @Test
+ public void sendCreateMultipartUploadRequest_success() throws Exception {
+ HttpRequestHandler handler =
+ req -> {
+ CreateMultipartUploadResponse response =
+ CreateMultipartUploadResponse.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .uploadId("test-upload-id")
+ .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();
+ CreateMultipartUploadRequest request =
+ CreateMultipartUploadRequest.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .contentType("application/octet-stream")
+ .build();
+
+ CreateMultipartUploadResponse response =
+ multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(endpoint, request);
+
+ assertThat(response).isNotNull();
+ assertThat(response.bucket()).isEqualTo("test-bucket");
+ assertThat(response.key()).isEqualTo("test-key");
+ assertThat(response.uploadId()).isEqualTo("test-upload-id");
+ }
+ }
+
+ @Test
+ public void sendCreateMultipartUploadRequest_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();
+ CreateMultipartUploadRequest request =
+ CreateMultipartUploadRequest.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .contentType("application/octet-stream")
+ .build();
+
+ assertThrows(
+ HttpResponseException.class,
+ () ->
+ multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(
+ endpoint, request));
+ }
+ }
+
+ @Test
+ public void sendCreateMultipartUploadRequest_withCannedAcl() throws Exception {
+ HttpRequestHandler handler =
+ req -> {
+ assertThat(req.headers().get("x-goog-acl")).isEqualTo("AUTHENTICATED_READ");
+ CreateMultipartUploadResponse response =
+ CreateMultipartUploadResponse.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .uploadId("test-upload-id")
+ .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();
+ CreateMultipartUploadRequest request =
+ CreateMultipartUploadRequest.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .contentType("application/octet-stream")
+ .cannedAcl(Storage.PredefinedAcl.AUTHENTICATED_READ)
+ .build();
+
+ multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(endpoint, request);
+ }
+ }
+
+ @Test
+ public void sendCreateMultipartUploadRequest_withMetadata() throws Exception {
+ HttpRequestHandler handler =
+ req -> {
+ assertThat(req.headers().get("x-goog-meta-key1")).isEqualTo("value1");
+ assertThat(req.headers().get("x-goog-meta-key2")).isEqualTo("value2");
+ CreateMultipartUploadResponse response =
+ CreateMultipartUploadResponse.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .uploadId("test-upload-id")
+ .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();
+ CreateMultipartUploadRequest request =
+ CreateMultipartUploadRequest.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .contentType("application/octet-stream")
+ .metadata(ImmutableMap.of("key1", "value1", "key2", "value2"))
+ .build();
+
+ multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(endpoint, request);
+ }
+ }
+
+ @Test
+ public void sendCreateMultipartUploadRequest_withStorageClass() throws Exception {
+ HttpRequestHandler handler =
+ req -> {
+ assertThat(req.headers().get("x-goog-storage-class")).isEqualTo("ARCHIVE");
+ CreateMultipartUploadResponse response =
+ CreateMultipartUploadResponse.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .uploadId("test-upload-id")
+ .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();
+ CreateMultipartUploadRequest request =
+ CreateMultipartUploadRequest.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .contentType("application/octet-stream")
+ .storageClass(StorageClass.ARCHIVE)
+ .build();
+
+ multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(endpoint, request);
+ }
+ }
+
+ @Test
+ public void sendCreateMultipartUploadRequest_withKmsKeyName() throws Exception {
+ HttpRequestHandler handler =
+ req -> {
+ assertThat(req.headers().get("x-goog-encryption-kms-key-name"))
+ .isEqualTo("projects/p/locations/l/keyRings/r/cryptoKeys/k");
+ CreateMultipartUploadResponse response =
+ CreateMultipartUploadResponse.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .uploadId("test-upload-id")
+ .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();
+ CreateMultipartUploadRequest request =
+ CreateMultipartUploadRequest.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .contentType("application/octet-stream")
+ .kmsKeyName("projects/p/locations/l/keyRings/r/cryptoKeys/k")
+ .build();
+
+ multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(endpoint, request);
+ }
+ }
+
+ @Test
+ public void sendCreateMultipartUploadRequest_withObjectLockMode() throws Exception {
+ HttpRequestHandler handler =
+ req -> {
+ assertThat(req.headers().get("x-goog-object-lock-mode")).isEqualTo("GOVERNANCE");
+ CreateMultipartUploadResponse response =
+ CreateMultipartUploadResponse.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .uploadId("test-upload-id")
+ .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();
+ CreateMultipartUploadRequest request =
+ CreateMultipartUploadRequest.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .contentType("application/octet-stream")
+ .objectLockMode(ObjectLockMode.GOVERNANCE)
+ .build();
+
+ multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(endpoint, request);
+ }
+ }
+
+ @Test
+ public void sendCreateMultipartUploadRequest_withObjectLockRetainUntilDate() throws Exception {
+ OffsetDateTime retainUtil = OffsetDateTime.of(2024, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
+ HttpRequestHandler handler =
+ req -> {
+ OffsetDateTime actual =
+ Utils.offsetDateTimeRfc3339Codec.decode(
+ req.headers().get("x-goog-object-lock-retain-until-date"));
+ assertThat(actual).isEqualTo(retainUtil);
+ CreateMultipartUploadResponse response =
+ CreateMultipartUploadResponse.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .uploadId("test-upload-id")
+ .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();
+ CreateMultipartUploadRequest request =
+ CreateMultipartUploadRequest.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .contentType("application/octet-stream")
+ .objectLockRetainUntilDate(retainUtil)
+ .build();
+
+ multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(endpoint, request);
+ }
+ }
+
+ @Test
+ public void sendCreateMultipartUploadRequest_withCustomTime() throws Exception {
+ OffsetDateTime customTime = OffsetDateTime.of(2024, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
+ HttpRequestHandler handler =
+ req -> {
+ OffsetDateTime actual =
+ Utils.offsetDateTimeRfc3339Codec.decode(req.headers().get("x-goog-custom-time"));
+ assertThat(actual).isEqualTo(customTime);
+ CreateMultipartUploadResponse response =
+ CreateMultipartUploadResponse.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .uploadId("test-upload-id")
+ .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();
+ CreateMultipartUploadRequest request =
+ CreateMultipartUploadRequest.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .contentType("application/octet-stream")
+ .customTime(customTime)
+ .build();
+
+ multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(endpoint, request);
+ }
+ }
+
+ @Test
+ public void sendListPartsRequest_success() throws Exception {
+ HttpRequestHandler handler =
+ req -> {
+ OffsetDateTime lastModified = OffsetDateTime.of(2024, 5, 8, 17, 50, 0, 0, ZoneOffset.UTC);
+ ListPartsResponse listPartsResponse =
+ ListPartsResponse.builder()
+ .setBucket("test-bucket")
+ .setKey("test-key")
+ .setUploadId("test-upload-id")
+ .setPartNumberMarker(0)
+ .setNextPartNumberMarker(1)
+ .setMaxParts(1)
+ .setIsTruncated(false)
+ .setParts(
+ Collections.singletonList(
+ Part.builder()
+ .partNumber(1)
+ .eTag("\"etag\"")
+ .size(123)
+ .lastModified(lastModified)
+ .build()))
+ .build();
+ ByteBuf buf = Unpooled.wrappedBuffer(xmlMapper.writeValueAsBytes(listPartsResponse));
+
+ 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();
+ ListPartsRequest request =
+ ListPartsRequest.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .uploadId("test-upload-id")
+ .maxParts(1)
+ .partNumberMarker(0)
+ .build();
+
+ ListPartsResponse response =
+ multipartUploadHttpRequestManager.sendListPartsRequest(endpoint, request);
+
+ assertThat(response).isNotNull();
+ assertThat(response.getBucket()).isEqualTo("test-bucket");
+ assertThat(response.getKey()).isEqualTo("test-key");
+ assertThat(response.getUploadId()).isEqualTo("test-upload-id");
+ assertThat(response.getPartNumberMarker()).isEqualTo(0);
+ assertThat(response.getNextPartNumberMarker()).isEqualTo(1);
+ assertThat(response.getMaxParts()).isEqualTo(1);
+ assertThat(response.isTruncated()).isFalse();
+ assertThat(response.getParts()).hasSize(1);
+ Part part = response.getParts().get(0);
+ assertThat(part.partNumber()).isEqualTo(1);
+ assertThat(part.eTag()).isEqualTo("\"etag\"");
+ assertThat(part.size()).isEqualTo(123);
+ assertThat(part.lastModified())
+ .isEqualTo(OffsetDateTime.of(2024, 5, 8, 17, 50, 0, 0, ZoneOffset.UTC));
+ }
+ }
+
+ @Test
+ public void sendListPartsRequest_bucketNotFound() throws Exception {
+ HttpRequestHandler handler =
+ req ->
+ new DefaultFullHttpResponse(
+ req.protocolVersion(),
+ HttpResponseStatus.NOT_FOUND,
+ Unpooled.wrappedBuffer("Bucket not found".getBytes(StandardCharsets.UTF_8)));
+
+ try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) {
+ URI endpoint = fakeHttpServer.getEndpoint();
+ ListPartsRequest request =
+ ListPartsRequest.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .uploadId("test-upload-id")
+ .build();
+
+ assertThrows(
+ HttpResponseException.class,
+ () -> multipartUploadHttpRequestManager.sendListPartsRequest(endpoint, request));
+ }
+ }
+
+ @Test
+ public void sendListPartsRequest_keyNotFound() throws Exception {
+ HttpRequestHandler handler =
+ req ->
+ new DefaultFullHttpResponse(
+ req.protocolVersion(),
+ HttpResponseStatus.NOT_FOUND,
+ Unpooled.wrappedBuffer("Key not found".getBytes(StandardCharsets.UTF_8)));
+
+ try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) {
+ URI endpoint = fakeHttpServer.getEndpoint();
+ ListPartsRequest request =
+ ListPartsRequest.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .uploadId("test-upload-id")
+ .build();
+
+ assertThrows(
+ HttpResponseException.class,
+ () -> multipartUploadHttpRequestManager.sendListPartsRequest(endpoint, request));
+ }
+ }
+
+ @Test
+ public void sendListPartsRequest_badRequest() throws Exception {
+ HttpRequestHandler handler =
+ req ->
+ new DefaultFullHttpResponse(
+ req.protocolVersion(),
+ HttpResponseStatus.BAD_REQUEST,
+ Unpooled.wrappedBuffer("Invalid uploadId".getBytes(StandardCharsets.UTF_8)));
+
+ try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) {
+ URI endpoint = fakeHttpServer.getEndpoint();
+ ListPartsRequest request =
+ ListPartsRequest.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .uploadId("invalid-upload-id")
+ .build();
+
+ assertThrows(
+ HttpResponseException.class,
+ () -> multipartUploadHttpRequestManager.sendListPartsRequest(endpoint, request));
+ }
+ }
+
+ @Test
+ public void sendListPartsRequest_errorResponse() 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();
+ ListPartsRequest request =
+ ListPartsRequest.builder()
+ .bucket("test-bucket")
+ .key("test-key")
+ .uploadId("test-upload-id")
+ .build();
+
+ assertThrows(
+ HttpResponseException.class,
+ () -> multipartUploadHttpRequestManager.sendListPartsRequest(endpoint, request));
+ }
+ }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/XmlObjectParserTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/XmlObjectParserTest.java
new file mode 100644
index 0000000000..c4acd8c64a
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/XmlObjectParserTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class XmlObjectParserTest {
+
+ @Mock private XmlMapper xmlMapper;
+
+ private AutoCloseable mocks;
+ private XmlObjectParser xmlObjectParser;
+
+ @Before
+ public void setUp() {
+ mocks = MockitoAnnotations.openMocks(this);
+ xmlObjectParser = new XmlObjectParser(xmlMapper);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mocks.close();
+ }
+
+ @Test
+ public void testParseAndClose() throws IOException {
+ InputStream in = new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8));
+ TestXmlObject expected = new TestXmlObject();
+ when(xmlMapper.readValue(any(Reader.class), any(Class.class))).thenReturn(expected);
+ TestXmlObject actual =
+ xmlObjectParser.parseAndClose(in, StandardCharsets.UTF_8, TestXmlObject.class);
+ assertThat(actual).isSameInstanceAs(expected);
+ }
+
+ private static class TestXmlObject {}
+}
diff --git a/samples/snippets/src/test/java/com/example/storage/ITBucketSnippets.java b/samples/snippets/src/test/java/com/example/storage/ITBucketSnippets.java
index 051bd05e23..11288347e2 100644
--- a/samples/snippets/src/test/java/com/example/storage/ITBucketSnippets.java
+++ b/samples/snippets/src/test/java/com/example/storage/ITBucketSnippets.java
@@ -101,6 +101,7 @@
import org.junit.After;
import org.junit.AfterClass;
import org.junit.BeforeClass;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
@@ -425,6 +426,7 @@ public void testAddListRemoveBucketIamMembers() throws Throwable {
.build());
}
+ @Ignore("This test cannot run in an environment with Public Access Prevention enforced.")
@Test
public void testMakeBucketPublic() throws Throwable {
MakeBucketPublic.makeBucketPublic(PROJECT_ID, BUCKET);
diff --git a/samples/snippets/src/test/java/com/example/storage/ITObjectSnippets.java b/samples/snippets/src/test/java/com/example/storage/ITObjectSnippets.java
index 28ceeb3081..45d6cdfc4f 100644
--- a/samples/snippets/src/test/java/com/example/storage/ITObjectSnippets.java
+++ b/samples/snippets/src/test/java/com/example/storage/ITObjectSnippets.java
@@ -92,6 +92,7 @@
import java.util.Random;
import javax.net.ssl.HttpsURLConnection;
import org.junit.Assert;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -167,6 +168,7 @@ public void testDownloadObjectIntoMemory() throws IOException {
assertThat(snippetOutput).contains("The contents of " + objectName);
}
+ @Ignore("This test cannot run in an environment with Public Access Prevention enforced.")
@Test
public void testDownloadPublicObject() throws Exception {
try (TemporaryBucket tmpBucket =
@@ -436,6 +438,7 @@ public void testV4SignedURLs() throws IOException {
}
}
+ @Ignore("This test cannot run in an environment with Public Access Prevention enforced.")
@Test
public void testMakeObjectPublic() {
String aclBlob = generator.randomObjectName();