diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml index 1b8cb3e8f1..b495f8665e 100644 --- a/google-cloud-storage/pom.xml +++ b/google-cloud-storage/pom.xml @@ -19,6 +19,22 @@ 1.123.5 + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-annotations + 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();