This feature is currently only supported for HNS (Hierarchical Namespace) buckets.
+ *
* @since 2.48.0
*/
@TransportCompatibility({Transport.HTTP, Transport.GRPC})
From a5d6c7e3e15e233805a1a29912fcd60a916569ec Mon Sep 17 00:00:00 2001
From: Mend Renovate
Date: Thu, 13 Feb 2025 21:09:02 +0100
Subject: [PATCH 04/21] test(deps): update cross product test dependencies
(#2928)
---
google-cloud-storage/pom.xml | 2 +-
pom.xml | 2 +-
samples/install-without-bom/pom.xml | 2 +-
samples/snapshot/pom.xml | 2 +-
samples/snippets/pom.xml | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index 94384ff42b..6f7d51b486 100644
--- a/google-cloud-storage/pom.xml
+++ b/google-cloud-storage/pom.xml
@@ -16,7 +16,7 @@
google-cloud-storage
- 1.118.1
+ 1.119.0
diff --git a/pom.xml b/pom.xml
index 20cfa8e4af..cf4e8e8bf2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -133,7 +133,7 @@
com.google.cloudgoogle-cloud-pubsub
- 1.136.1
+ 1.137.0test
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index a577ad518d..6d9a6de12d 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -66,7 +66,7 @@
com.google.cloudgoogle-cloud-pubsub
- 1.136.1
+ 1.137.0test
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 72f14d0b45..f97ded7fa1 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -58,7 +58,7 @@
com.google.cloudgoogle-cloud-pubsub
- 1.136.1
+ 1.137.0test
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index 121deb87e6..ce4cea38c3 100644
--- a/samples/snippets/pom.xml
+++ b/samples/snippets/pom.xml
@@ -76,7 +76,7 @@
com.google.cloudgoogle-cloud-pubsub
- 1.136.1
+ 1.137.0test
From 4fde024b74d069aa62b6fb011c3f95c7d5b355e9 Mon Sep 17 00:00:00 2001
From: Mend Renovate
Date: Thu, 13 Feb 2025 21:40:01 +0100
Subject: [PATCH 05/21] chore(deps): update storage release dependencies to
v2.48.2 (#2926)
---
samples/install-without-bom/pom.xml | 6 +++---
samples/snippets/pom.xml | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index 6d9a6de12d..c81c72d9ca 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -30,12 +30,12 @@
com.google.cloudgoogle-cloud-storage
- 2.48.1
+ 2.48.2com.google.cloudgoogle-cloud-storage-control
- 2.48.1
+ 2.48.2
@@ -72,7 +72,7 @@
com.google.cloudgoogle-cloud-storage
- 2.48.1
+ 2.48.2teststest
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index ce4cea38c3..7a51759598 100644
--- a/samples/snippets/pom.xml
+++ b/samples/snippets/pom.xml
@@ -93,7 +93,7 @@
com.google.cloudgoogle-cloud-storage
- 2.48.1
+ 2.48.2teststest
From 9946d6bdc7ec8398bf1bd1df63f272df1351539e Mon Sep 17 00:00:00 2001
From: cloud-java-bot <122572305+cloud-java-bot@users.noreply.github.com>
Date: Fri, 14 Feb 2025 15:06:40 -0500
Subject: [PATCH 06/21] chore: Update generation configuration at Fri Feb 14
02:22:08 UTC 2025 (#2934)
* chore: Update generation configuration at Fri Feb 14 02:22:08 UTC 2025
* chore: generate libraries at Fri Feb 14 02:22:48 UTC 2025
---
README.md | 4 ++--
generation_config.yaml | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index bdfcc95617..6ca40a1a3c 100644
--- a/README.md
+++ b/README.md
@@ -46,12 +46,12 @@ If you are using Maven without the BOM, add this to your dependencies:
com.google.cloudgoogle-cloud-storage
- 2.48.1
+ 2.48.2com.google.cloudgoogle-cloud-storage-control
- 2.48.1
+ 2.48.2
```
diff --git a/generation_config.yaml b/generation_config.yaml
index 1d62fcf9fa..0268d85ff6 100644
--- a/generation_config.yaml
+++ b/generation_config.yaml
@@ -1,5 +1,5 @@
-gapic_generator_version: 2.52.0
-googleapis_commitish: a1c746a0304b9d0d913ab013cb248ce605a6871b
+gapic_generator_version: 2.53.0
+googleapis_commitish: a003cab30e2a263e16e9252256041f8934f40e2c
libraries_bom_version: 26.54.0
libraries:
- api_shortname: storage
From e8ba858ec81d74fa15a00a13e94d0944148b0a74 Mon Sep 17 00:00:00 2001
From: Mend Renovate
Date: Fri, 14 Feb 2025 21:07:23 +0100
Subject: [PATCH 07/21] test(deps): update cross product test dependencies
(#2933)
---
google-cloud-storage/pom.xml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index 6f7d51b486..ec307a7ddb 100644
--- a/google-cloud-storage/pom.xml
+++ b/google-cloud-storage/pom.xml
@@ -239,14 +239,14 @@
com.google.api.grpcproto-google-cloud-kms-v1
- 0.151.0
+ 0.152.0testcom.google.cloudgoogle-cloud-kms
- 2.60.0
+ 2.61.0test
From 86e9ae80772aa202d0b6563b8dd37722d8b5e0e0 Mon Sep 17 00:00:00 2001
From: BenWhitehead
Date: Tue, 18 Feb 2025 17:35:25 -0500
Subject: [PATCH 08/21] feat(transfer-manager): add
ParallelUploadConfig.Builder#setUploadBlobInfoFactory (#2936)
When uploading a file from the files system, there are scenarios when a job would need to customize the actual BlobInfo used to upload to GCS.
Add the new UploadBlobInfoFactory which allows a user to produce their own BlobInfo instance given the bucketName and fileName. When producing the BlobInfo the application can also customize other metadata fields.
A few convenience adapter methods are available in UploadBlobInfoFactory to simplify common operations.
Fixes #2638
---
.../BucketNameMismatchException.java | 27 +++
.../transfermanager/ParallelUploadConfig.java | 158 ++++++++++++++++--
.../transfermanager/TransferManagerImpl.java | 13 +-
.../transfermanager/TransferManagerUtils.java | 8 -
.../storage/it/ITTransferManagerTest.java | 108 ++++++++++++
.../transfermanager/TransferManagerTest.java | 65 +++++++
6 files changed, 357 insertions(+), 22 deletions(-)
create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/transfermanager/BucketNameMismatchException.java
create mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/transfermanager/TransferManagerTest.java
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/transfermanager/BucketNameMismatchException.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/transfermanager/BucketNameMismatchException.java
new file mode 100644
index 0000000000..0391722989
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/transfermanager/BucketNameMismatchException.java
@@ -0,0 +1,27 @@
+/*
+ * 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.transfermanager;
+
+public final class BucketNameMismatchException extends RuntimeException {
+
+ public BucketNameMismatchException(String actual, String expected) {
+ super(
+ String.format(
+ "Bucket name in produced BlobInfo did not match bucket name from config. (%s != %s)",
+ actual, expected));
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/transfermanager/ParallelUploadConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/transfermanager/ParallelUploadConfig.java
index 615d85427b..1497210f86 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/transfermanager/ParallelUploadConfig.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/transfermanager/ParallelUploadConfig.java
@@ -18,11 +18,13 @@
import static com.google.common.base.Preconditions.checkNotNull;
+import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.Storage.BlobWriteOption;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.Objects;
+import java.util.function.Function;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
@@ -33,19 +35,19 @@
public final class ParallelUploadConfig {
private final boolean skipIfExists;
- @NonNull private final String prefix;
@NonNull private final String bucketName;
+ @NonNull private final UploadBlobInfoFactory uploadBlobInfoFactory;
@NonNull private final List writeOptsPerRequest;
private ParallelUploadConfig(
boolean skipIfExists,
- @NonNull String prefix,
@NonNull String bucketName,
+ @NonNull UploadBlobInfoFactory uploadBlobInfoFactory,
@NonNull List writeOptsPerRequest) {
this.skipIfExists = skipIfExists;
- this.prefix = prefix;
this.bucketName = bucketName;
+ this.uploadBlobInfoFactory = uploadBlobInfoFactory;
this.writeOptsPerRequest = applySkipIfExists(skipIfExists, writeOptsPerRequest);
}
@@ -63,9 +65,26 @@ public boolean isSkipIfExists() {
* A common prefix that will be applied to all object paths in the destination bucket
*
* @see Builder#setPrefix(String)
+ * @see Builder#setUploadBlobInfoFactory(UploadBlobInfoFactory)
+ * @see UploadBlobInfoFactory#prefixObjectNames(String)
*/
public @NonNull String getPrefix() {
- return prefix;
+ if (uploadBlobInfoFactory instanceof PrefixObjectNames) {
+ PrefixObjectNames prefixObjectNames = (PrefixObjectNames) uploadBlobInfoFactory;
+ return prefixObjectNames.prefix;
+ }
+ return "";
+ }
+
+ /**
+ * The {@link UploadBlobInfoFactory} which will be used to produce a {@link BlobInfo}s based on a
+ * provided bucket name and file name.
+ *
+ * @see Builder#setUploadBlobInfoFactory(UploadBlobInfoFactory)
+ * @since 2.49.0
+ */
+ public @NonNull UploadBlobInfoFactory getUploadBlobInfoFactory() {
+ return uploadBlobInfoFactory;
}
/**
@@ -96,22 +115,22 @@ public boolean equals(Object o) {
}
ParallelUploadConfig that = (ParallelUploadConfig) o;
return skipIfExists == that.skipIfExists
- && prefix.equals(that.prefix)
&& bucketName.equals(that.bucketName)
+ && uploadBlobInfoFactory.equals(that.uploadBlobInfoFactory)
&& writeOptsPerRequest.equals(that.writeOptsPerRequest);
}
@Override
public int hashCode() {
- return Objects.hash(skipIfExists, prefix, bucketName, writeOptsPerRequest);
+ return Objects.hash(skipIfExists, bucketName, uploadBlobInfoFactory, writeOptsPerRequest);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("skipIfExists", skipIfExists)
- .add("prefix", prefix)
.add("bucketName", bucketName)
+ .add("uploadBlobInfoFactory", uploadBlobInfoFactory)
.add("writeOptsPerRequest", writeOptsPerRequest)
.toString();
}
@@ -137,13 +156,13 @@ private static List applySkipIfExists(
public static final class Builder {
private boolean skipIfExists;
- private @NonNull String prefix;
private @NonNull String bucketName;
+ private @NonNull UploadBlobInfoFactory uploadBlobInfoFactory;
private @NonNull List writeOptsPerRequest;
private Builder() {
- this.prefix = "";
this.bucketName = "";
+ this.uploadBlobInfoFactory = UploadBlobInfoFactory.defaultInstance();
this.writeOptsPerRequest = ImmutableList.of();
}
@@ -162,11 +181,37 @@ public Builder setSkipIfExists(boolean skipIfExists) {
/**
* Sets a common prefix that will be applied to all object paths in the destination bucket.
*
+ *
NOTE: this method and {@link #setUploadBlobInfoFactory(UploadBlobInfoFactory)} are
+ * mutually exclusive, and last invocation "wins".
+ *
* @return the builder instance with the value for prefix modified.
* @see ParallelUploadConfig#getPrefix()
+ * @see ParallelUploadConfig.Builder#setUploadBlobInfoFactory(UploadBlobInfoFactory)
+ * @see UploadBlobInfoFactory#prefixObjectNames(String)
*/
public Builder setPrefix(@NonNull String prefix) {
- this.prefix = prefix;
+ this.uploadBlobInfoFactory = UploadBlobInfoFactory.prefixObjectNames(prefix);
+ return this;
+ }
+
+ /**
+ * Sets a {@link UploadBlobInfoFactory} which can be used to produce a custom BlobInfo based on
+ * a provided bucket name and file name.
+ *
+ *
The bucket name in the returned BlobInfo MUST be equal to the value provided to {@link
+ * #setBucketName(String)}, if not that upload will fail with a {@link
+ * TransferStatus#FAILED_TO_START} and a {@link BucketNameMismatchException}.
+ *
+ *
NOTE: this method and {@link #setPrefix(String)} are mutually exclusive, and last
+ * invocation "wins".
+ *
+ * @return the builder instance with the value for uploadBlobInfoFactory modified.
+ * @see ParallelUploadConfig#getPrefix()
+ * @see ParallelUploadConfig#getUploadBlobInfoFactory()
+ * @since 2.49.0
+ */
+ public Builder setUploadBlobInfoFactory(@NonNull UploadBlobInfoFactory uploadBlobInfoFactory) {
+ this.uploadBlobInfoFactory = uploadBlobInfoFactory;
return this;
}
@@ -199,10 +244,99 @@ public Builder setWriteOptsPerRequest(@NonNull List writeOptsPe
* @return {@link ParallelUploadConfig}
*/
public ParallelUploadConfig build() {
- checkNotNull(prefix);
checkNotNull(bucketName);
+ checkNotNull(uploadBlobInfoFactory);
checkNotNull(writeOptsPerRequest);
- return new ParallelUploadConfig(skipIfExists, prefix, bucketName, writeOptsPerRequest);
+ return new ParallelUploadConfig(
+ skipIfExists, bucketName, uploadBlobInfoFactory, writeOptsPerRequest);
+ }
+ }
+
+ public interface UploadBlobInfoFactory {
+
+ /**
+ * Method to produce a {@link BlobInfo} to be used for the upload to Cloud Storage.
+ *
+ *
The bucket name in the returned BlobInfo MUST be equal to the value provided to the {@link
+ * ParallelUploadConfig.Builder#setBucketName(String)}, if not that upload will fail with a
+ * {@link TransferStatus#FAILED_TO_START} and a {@link BucketNameMismatchException}.
+ *
+ * @param bucketName The name of the bucket to be uploaded to. The value provided here will be
+ * the value from {@link ParallelUploadConfig#getBucketName()}.
+ * @param fileName The String representation of the absolute path of the file to be uploaded
+ * @return The instance of {@link BlobInfo} that should be used to upload the file to Cloud
+ * Storage.
+ */
+ BlobInfo apply(String bucketName, String fileName);
+
+ /**
+ * Adapter factory to provide the same semantics as if using {@link Builder#setPrefix(String)}
+ */
+ static UploadBlobInfoFactory prefixObjectNames(String prefix) {
+ return new PrefixObjectNames(prefix);
+ }
+
+ /** The default instance which applies not modification to the provided {@code fileName} */
+ static UploadBlobInfoFactory defaultInstance() {
+ return DefaultUploadBlobInfoFactory.INSTANCE;
+ }
+
+ /**
+ * Convenience method to "lift" a {@link Function} that transforms the file name to an {@link
+ * UploadBlobInfoFactory}
+ */
+ static UploadBlobInfoFactory transformFileName(Function fileNameTransformer) {
+ return (b, f) -> BlobInfo.newBuilder(b, fileNameTransformer.apply(f)).build();
+ }
+ }
+
+ private static final class DefaultUploadBlobInfoFactory implements UploadBlobInfoFactory {
+ private static final DefaultUploadBlobInfoFactory INSTANCE = new DefaultUploadBlobInfoFactory();
+
+ private DefaultUploadBlobInfoFactory() {}
+
+ @Override
+ public BlobInfo apply(String bucketName, String fileName) {
+ return BlobInfo.newBuilder(bucketName, fileName).build();
+ }
+ }
+
+ private static final class PrefixObjectNames implements UploadBlobInfoFactory {
+ private final String prefix;
+
+ private PrefixObjectNames(String prefix) {
+ this.prefix = prefix;
+ }
+
+ @Override
+ public BlobInfo apply(String bucketName, String fileName) {
+ String separator = "";
+ if (!fileName.startsWith("/")) {
+ separator = "/";
+ }
+ return BlobInfo.newBuilder(bucketName, prefix + separator + fileName).build();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof PrefixObjectNames)) {
+ return false;
+ }
+ PrefixObjectNames that = (PrefixObjectNames) o;
+ return Objects.equals(prefix, that.prefix);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(prefix);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this).add("prefix", prefix).toString();
}
}
}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/transfermanager/TransferManagerImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/transfermanager/TransferManagerImpl.java
index 57e56ea201..13f7d400a2 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/transfermanager/TransferManagerImpl.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/transfermanager/TransferManagerImpl.java
@@ -110,8 +110,17 @@ public void close() throws Exception {
List> uploadTasks = new ArrayList<>();
for (Path file : files) {
if (Files.isDirectory(file)) throw new IllegalStateException("Directories are not supported");
- String blobName = TransferManagerUtils.createBlobName(config, file);
- BlobInfo blobInfo = BlobInfo.newBuilder(config.getBucketName(), blobName).build();
+ String bucketName = config.getBucketName();
+ BlobInfo blobInfo =
+ config.getUploadBlobInfoFactory().apply(bucketName, file.toAbsolutePath().toString());
+ if (!blobInfo.getBucket().equals(bucketName)) {
+ uploadTasks.add(
+ ApiFutures.immediateFuture(
+ UploadResult.newBuilder(blobInfo, TransferStatus.FAILED_TO_START)
+ .setException(new BucketNameMismatchException(blobInfo.getBucket(), bucketName))
+ .build()));
+ continue;
+ }
if (transferManagerConfig.isAllowParallelCompositeUpload()
&& qos.parallelCompositeUpload(Files.size(file))) {
ParallelCompositeUploadCallable callable =
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/transfermanager/TransferManagerUtils.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/transfermanager/TransferManagerUtils.java
index 607dd3148b..a884aa7067 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/transfermanager/TransferManagerUtils.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/transfermanager/TransferManagerUtils.java
@@ -26,14 +26,6 @@ final class TransferManagerUtils {
private TransferManagerUtils() {}
- static String createBlobName(ParallelUploadConfig config, Path file) {
- if (config.getPrefix().isEmpty()) {
- return file.toString();
- } else {
- return config.getPrefix().concat(file.toString());
- }
- }
-
static Path createDestPath(ParallelDownloadConfig config, BlobInfo originalBlob) {
Path newPath =
config
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITTransferManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITTransferManagerTest.java
index 504de7164e..bd116cf172 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITTransferManagerTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITTransferManagerTest.java
@@ -36,6 +36,7 @@
import com.google.cloud.storage.it.runner.annotations.CrossRun;
import com.google.cloud.storage.it.runner.annotations.Inject;
import com.google.cloud.storage.it.runner.registry.Generator;
+import com.google.cloud.storage.transfermanager.BucketNameMismatchException;
import com.google.cloud.storage.transfermanager.DownloadJob;
import com.google.cloud.storage.transfermanager.DownloadResult;
import com.google.cloud.storage.transfermanager.ParallelDownloadConfig;
@@ -47,6 +48,7 @@
import com.google.cloud.storage.transfermanager.UploadJob;
import com.google.cloud.storage.transfermanager.UploadResult;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
@@ -57,7 +59,11 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.stream.Collectors;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -525,6 +531,108 @@ public void downloadBlobsOneFailure() throws Exception {
}
}
+ @Test
+ public void uploadChangePrefix() throws Exception {
+ try (TmpFile tmpFile1 = DataGenerator.base64Characters().tempFile(baseDir, 373);
+ TmpFile tmpFile2 =
+ DataGenerator.base64Characters().tempFile(baseDir, 2 * 1024 * 1024 + 13);
+ TransferManager tm =
+ TransferManagerConfig.newBuilder()
+ .setMaxWorkers(1)
+ .setPerWorkerBufferSize(4 * 1024 * 1024)
+ .setAllowDivideAndConquerDownload(false)
+ .setAllowParallelCompositeUpload(false)
+ .setStorageOptions(storage.getOptions())
+ .build()
+ .getService()) {
+
+ String prefix = "asdfasdf";
+ ImmutableMap<@NonNull String, @Nullable String> metadata = ImmutableMap.of("k", "v");
+ String contentType = "text/plain;charset=utf-8";
+ ParallelUploadConfig uploadConfig =
+ ParallelUploadConfig.newBuilder()
+ .setBucketName(bucket.getName())
+ .setSkipIfExists(false)
+ .setUploadBlobInfoFactory(
+ (b, f) ->
+ BlobInfo.newBuilder(
+ b, prefix + f.replace(baseDir.toAbsolutePath().toString(), ""))
+ .setContentType(contentType)
+ .setMetadata(metadata)
+ .build())
+ .setWriteOptsPerRequest(ImmutableList.of(BlobWriteOption.doesNotExist()))
+ .build();
+
+ ImmutableList files = ImmutableList.of(tmpFile1.getPath(), tmpFile2.getPath());
+ UploadJob uploadJob = tm.uploadFiles(files, uploadConfig);
+ List uploadResults = uploadJob.getUploadResults();
+
+ List expected =
+ files.stream()
+ .map(p -> p.getFileName().toString())
+ .map(s -> prefix + "/" + s)
+ .collect(Collectors.toList());
+
+ List actualGsUtilUris =
+ uploadResults.stream()
+ .map(UploadResult::getUploadedBlob)
+ .map(BlobInfo::getName)
+ .collect(Collectors.toList());
+ assertThat(actualGsUtilUris).containsExactlyElementsIn(expected);
+
+ List
+
+ org.slf4j
+ jul-to-slf4j
+ 2.0.16
+ test
+
+
+ ch.qos.logback
+ logback-classic
+ 1.3.15
+ test
+
@@ -386,6 +398,22 @@
-->
io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spiio.opentelemetry.semconv:opentelemetry-semconv
+
+ org.slf4j:slf4j-api
+ org.slf4j:jul-to-slf4j
+ ch.qos.logback:logback-classic
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/StorageITRunner.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/StorageITRunner.java
index 814c5da65c..d99df4b802 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/StorageITRunner.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/StorageITRunner.java
@@ -42,6 +42,8 @@
import org.junit.runners.model.FrameworkField;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.TestClass;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Storage custom runner which will handle {@link CrossRun}, {@link SingleBackend}, {@link
@@ -53,6 +55,12 @@
* @see org.junit.runners.BlockJUnit4ClassRunner
*/
public final class StorageITRunner extends Suite {
+ static {
+ org.slf4j.bridge.SLF4JBridgeHandler.removeHandlersForRootLogger();
+ org.slf4j.bridge.SLF4JBridgeHandler.install();
+ }
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(StorageITRunner.class);
private final Lock childrenLock = new ReentrantLock();
private volatile ImmutableList filteredChildren = null;
@@ -67,6 +75,7 @@ public StorageITRunner(Class> klass) throws InitializationError {
@Override
public void run(RunNotifier notifier) {
+ LOGGER.debug("run(notifier : {})", notifier);
super.run(new RunNotifierUnion(notifier, Registry.getInstance()));
}
diff --git a/google-cloud-storage/src/test/resources/logback.xml b/google-cloud-storage/src/test/resources/logback.xml
new file mode 100644
index 0000000000..e97103c9b6
--- /dev/null
+++ b/google-cloud-storage/src/test/resources/logback.xml
@@ -0,0 +1,93 @@
+
+
+
+
+ true
+
+
+
+ %date %-5.5level [%-24.24thread] %-45.45logger{45} - %message%n
+
+
+
+
+ %date %-5.5level [%-24.24thread] %-45.45logger{45} - %message%nopex%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From 95e540afe323bd70b087c8ce3106ba654b0d24bb Mon Sep 17 00:00:00 2001
From: BenWhitehead
Date: Mon, 24 Feb 2025 15:57:29 -0500
Subject: [PATCH 14/21] chore: add renovate logging config (#2949)
Update default log levels for http and grpc requests
---
google-cloud-storage/src/test/resources/logback.xml | 4 ++--
renovate.json | 10 +++++++++-
2 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/google-cloud-storage/src/test/resources/logback.xml b/google-cloud-storage/src/test/resources/logback.xml
index e97103c9b6..70cdedad80 100644
--- a/google-cloud-storage/src/test/resources/logback.xml
+++ b/google-cloud-storage/src/test/resources/logback.xml
@@ -78,8 +78,8 @@
-
-
+
+
diff --git a/renovate.json b/renovate.json
index 48b00313a6..b6261a8427 100644
--- a/renovate.json
+++ b/renovate.json
@@ -94,7 +94,9 @@
"^org.mockito:mockito-core",
"^org.objenesis:objenesis",
"^com.google.cloud:google-cloud-conformance-tests",
- "^io.github.classgraph:classgraph"
+ "^io.github.classgraph:classgraph",
+ "^ch.qos.logback:logback-classic",
+ "^org.slf4j:jul-to-slf4j"
],
"semanticCommitType": "test",
"semanticCommitScope": "deps"
@@ -139,6 +141,12 @@
"^com.google.cloud:sdk-platform-java-config"
],
"groupName": "sdk-platform-java dependencies"
+ },
+ {
+ "packagePatterns": [
+ "^ch.qos.logback:logback-classic"
+ ],
+ "allowedVersions": "<1.5.0"
}
],
"semanticCommits": true,
From 51fcce1fb325e1eb6fa7942047ddff501f9e9d34 Mon Sep 17 00:00:00 2001
From: cloud-java-bot <122572305+cloud-java-bot@users.noreply.github.com>
Date: Tue, 25 Feb 2025 11:15:47 -0500
Subject: [PATCH 15/21] chore: Update generation configuration at Tue Feb 25
02:24:21 UTC 2025 (#2951)
* chore: Update generation configuration at Tue Feb 25 02:24:21 UTC 2025
* chore: generate libraries at Tue Feb 25 02:24:58 UTC 2025
---
README.md | 2 +-
generation_config.yaml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index e44bc8f65c..87185f7ca4 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file:
com.google.cloudlibraries-bom
- 26.54.0
+ 26.55.0pomimport
diff --git a/generation_config.yaml b/generation_config.yaml
index c288bdd230..1a74239123 100644
--- a/generation_config.yaml
+++ b/generation_config.yaml
@@ -1,5 +1,5 @@
gapic_generator_version: 2.53.0
-googleapis_commitish: 3b2e8657f0bef4d9638f2957ed9d988adc65427c
+googleapis_commitish: 6bc8e91bf92cc985da5ed0c227b48f12315cb695
libraries_bom_version: 26.55.0
libraries:
- api_shortname: storage
From cc4c7f411e7f7950594ac6910eb225ee1952196d Mon Sep 17 00:00:00 2001
From: BenWhitehead
Date: Tue, 25 Feb 2025 11:38:08 -0500
Subject: [PATCH 16/21] chore: renovate config fix for
ch.qos.logback:logback-classic (#2952)
---
renovate.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/renovate.json b/renovate.json
index b6261a8427..c4eb8ff74d 100644
--- a/renovate.json
+++ b/renovate.json
@@ -146,7 +146,7 @@
"packagePatterns": [
"^ch.qos.logback:logback-classic"
],
- "allowedVersions": "<1.5.0"
+ "allowedVersions": "<1.4.0"
}
],
"semanticCommits": true,
From 297802d1715e3289dd720fba851c563004b8c5f2 Mon Sep 17 00:00:00 2001
From: BenWhitehead
Date: Tue, 25 Feb 2025 13:50:08 -0500
Subject: [PATCH 17/21] feat: add new Options to allow per method header values
(#2941)
Add new "Option"s for those methods which already have option types to allow providing an ImmutableMap to be applied as extra headers to all requests sent as part of that operation.
If an operation has multiple sources of input Options (rewrite) the "first" (i.e. source option) will be the one added to the request.
The following resources do not have "Option"s and therefor do not have extra headers support at this time:
* Acl
* DefaultAcl
* ServiceAccount
* Notification
---
.../ApiaryUnbufferedReadableByteChannel.java | 13 +-
.../google/cloud/storage/GrpcStorageImpl.java | 11 +-
.../cloud/storage/GrpcStorageOptions.java | 8 +-
.../cloud/storage/JsonResumableSession.java | 5 +-
.../storage/JsonResumableSessionPutTask.java | 15 +-
.../JsonResumableSessionQueryTask.java | 15 +-
.../cloud/storage/JsonResumableWrite.java | 11 +
.../ResumableSessionFailureScenario.java | 5 +-
.../com/google/cloud/storage/Storage.java | 796 +++++++++++++++++-
.../com/google/cloud/storage/UnifiedOpts.java | 196 ++++-
.../java/com/google/cloud/storage/Utils.java | 5 +
.../storage/XGoogApiClientHeaderProvider.java | 3 +-
.../cloud/storage/spi/v1/HttpStorageRpc.java | 433 ++++++----
.../cloud/storage/spi/v1/StorageRpc.java | 7 +-
.../storage/ITExtraHeadersOptionTest.java | 377 +++++++++
.../ITJsonResumableSessionPutTaskTest.java | 35 +-
.../ITJsonResumableSessionQueryTaskTest.java | 15 +-
.../cloud/storage/UnifiedOptsGrpcTest.java | 98 +++
.../storage/it/AssertRequestHeaders.java | 35 +
.../cloud/storage/it/GrpcRequestAuditing.java | 39 +-
.../cloud/storage/it/RequestAuditing.java | 29 +-
21 files changed, 1913 insertions(+), 238 deletions(-)
create mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/ITExtraHeadersOptionTest.java
create mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/it/AssertRequestHeaders.java
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryUnbufferedReadableByteChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryUnbufferedReadableByteChannel.java
index f61814d4f9..86afe7a98c 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryUnbufferedReadableByteChannel.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryUnbufferedReadableByteChannel.java
@@ -33,6 +33,7 @@
import com.google.cloud.storage.spi.v1.StorageRpc;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableMap;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;
@@ -51,8 +52,8 @@
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.ScatteringByteChannel;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.function.Function;
import javax.annotation.concurrent.Immutable;
import org.checkerframework.checker.nullness.qual.NonNull;
@@ -273,6 +274,14 @@ static Get createGetRequest(
"x-goog-encryption-key-sha256",
base64.encode(hashFunction.hashBytes(base64.decode(key)).asBytes()));
});
+ ifNonNull(
+ options.get(StorageRpc.Option.EXTRA_HEADERS),
+ ApiaryUnbufferedReadableByteChannel::cast,
+ (ImmutableMap extraHeaders) -> {
+ for (Entry e : extraHeaders.entrySet()) {
+ headers.set(e.getKey(), e.getValue());
+ }
+ });
// gzip handling is performed upstream of here. Ensure we always get the raw input stream from
// the request
@@ -302,7 +311,7 @@ private static String getHeaderValue(@NonNull HttpHeaders headers, @NonNull Stri
if (list.isEmpty()) {
return null;
} else {
- return list.get(0).trim().toLowerCase(Locale.ENGLISH);
+ return Utils.headerNameToLowerCase(list.get(0).trim());
}
} else if (o instanceof String) {
return (String) o;
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
index f822130823..3211210fbc 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
@@ -650,8 +650,10 @@ public CopyWriter copy(CopyRequest copyRequest) {
Opts dstOpts =
Opts.unwrap(copyRequest.getTargetOptions()).resolveFrom(dst).prepend(defaultOpts);
- Mapper mapper =
+ Mapper requestBuilderMapper =
srcOpts.rewriteObjectsRequest().andThen(dstOpts.rewriteObjectsRequest());
+ Mapper grpcCallContextMapper =
+ srcOpts.grpcMetadataMapper().andThen(dstOpts.grpcMetadataMapper());
Object srcProto = codecs.blobId().encode(src);
Object dstProto = codecs.blobInfo().encode(dst);
@@ -686,9 +688,8 @@ public CopyWriter copy(CopyRequest copyRequest) {
b.setMaxBytesRewrittenPerCall(copyRequest.getMegabytesCopiedPerChunk() * _1MiB);
}
- RewriteObjectRequest req = mapper.apply(b).build();
- GrpcCallContext grpcCallContext =
- srcOpts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
+ RewriteObjectRequest req = requestBuilderMapper.apply(b).build();
+ GrpcCallContext grpcCallContext = grpcCallContextMapper.apply(GrpcCallContext.createDefault());
UnaryCallable callable =
storageClient.rewriteObjectCallable().withDefaultCallContext(grpcCallContext);
GrpcCallContext retryContext = Retrying.newCallContext();
@@ -733,7 +734,7 @@ public GrpcBlobReadChannel reader(String bucket, String blob, BlobSourceOption..
public GrpcBlobReadChannel reader(BlobId blob, BlobSourceOption... options) {
Opts opts = Opts.unwrap(options).resolveFrom(blob).prepend(defaultOpts);
ReadObjectRequest request = getReadObjectRequest(blob, opts);
- GrpcCallContext grpcCallContext = Retrying.newCallContext();
+ GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(Retrying.newCallContext());
return new GrpcBlobReadChannel(
storageClient.readObjectCallable().withDefaultCallContext(grpcCallContext),
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java
index 279418c5f3..43e575a29d 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java
@@ -96,7 +96,6 @@
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
@@ -161,6 +160,11 @@ StorageSettings getStorageSettings() throws IOException {
return resolveSettingsAndOpts().x();
}
+ @InternalApi
+ GrpcInterceptorProvider getGrpcInterceptorProvider() {
+ return grpcInterceptorProvider;
+ }
+
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.openTelemetry = HttpStorageOptions.getDefaultInstance().getOpenTelemetry();
@@ -225,7 +229,7 @@ private Tuple> resolveSettingsAndOpts() throw
Map> requestMetadata = credentials.getRequestMetadata(uri);
for (Entry> e : requestMetadata.entrySet()) {
String key = e.getKey();
- if ("x-goog-user-project".equals(key.trim().toLowerCase(Locale.ENGLISH))) {
+ if ("x-goog-user-project".equals(Utils.headerNameToLowerCase(key.trim()))) {
List value = e.getValue();
if (!value.isEmpty()) {
foundQuotaProject = true;
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSession.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSession.java
index d997400dee..e29094fd09 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSession.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSession.java
@@ -54,14 +54,13 @@ final class JsonResumableSession {
* have the concept of nested retry handling.
*/
ResumableOperationResult<@Nullable StorageObject> query() {
- return new JsonResumableSessionQueryTask(context, resumableWrite.getUploadId()).call();
+ return new JsonResumableSessionQueryTask(context, resumableWrite).call();
}
ResumableOperationResult<@Nullable StorageObject> put(
RewindableContent content, HttpContentRange contentRange) {
JsonResumableSessionPutTask task =
- new JsonResumableSessionPutTask(
- context, resumableWrite.getUploadId(), content, contentRange);
+ new JsonResumableSessionPutTask(context, resumableWrite, content, contentRange);
HttpRpcContext httpRpcContext = HttpRpcContext.getInstance();
try {
httpRpcContext.newInvocationId();
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSessionPutTask.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSessionPutTask.java
index 149c7ad9af..9ebd8e5868 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSessionPutTask.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSessionPutTask.java
@@ -17,6 +17,7 @@
package com.google.cloud.storage;
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.HttpResponse;
import com.google.api.client.http.HttpResponseException;
@@ -31,6 +32,7 @@
import java.io.IOException;
import java.math.BigInteger;
import java.util.Locale;
+import java.util.Map.Entry;
import java.util.concurrent.Callable;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -38,7 +40,7 @@ final class JsonResumableSessionPutTask
implements Callable> {
private final HttpClientContext context;
- private final String uploadId;
+ private final JsonResumableWrite jsonResumableWrite;
private final RewindableContent content;
private final HttpContentRange originalContentRange;
@@ -47,11 +49,11 @@ final class JsonResumableSessionPutTask
@VisibleForTesting
JsonResumableSessionPutTask(
HttpClientContext httpClientContext,
- String uploadId,
+ JsonResumableWrite jsonResumableWrite,
RewindableContent content,
HttpContentRange originalContentRange) {
this.context = httpClientContext;
- this.uploadId = uploadId;
+ this.jsonResumableWrite = jsonResumableWrite;
this.content = content;
this.originalContentRange = originalContentRange;
this.contentRange = originalContentRange;
@@ -87,13 +89,18 @@ public void rewindTo(long offset) {
boolean success = false;
boolean finalizing = originalContentRange.isFinalizing();
+ String uploadId = jsonResumableWrite.getUploadId();
HttpRequest req =
context
.getRequestFactory()
.buildPutRequest(new GenericUrl(uploadId), content)
.setParser(context.getObjectParser());
req.setThrowExceptionOnExecuteError(false);
- req.getHeaders().setContentRange(contentRange.getHeaderValue());
+ HttpHeaders headers = req.getHeaders();
+ headers.setContentRange(contentRange.getHeaderValue());
+ for (Entry e : jsonResumableWrite.getExtraHeaders().entrySet()) {
+ headers.set(e.getKey(), e.getValue());
+ }
HttpResponse response = null;
try {
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSessionQueryTask.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSessionQueryTask.java
index 57c5868c8e..dba12171ab 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSessionQueryTask.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSessionQueryTask.java
@@ -20,6 +20,7 @@
import com.google.api.client.http.EmptyContent;
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.HttpResponse;
import com.google.api.client.http.HttpResponseException;
@@ -27,6 +28,7 @@
import java.io.IOException;
import java.math.BigInteger;
import java.util.Locale;
+import java.util.Map.Entry;
import java.util.concurrent.Callable;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -34,15 +36,16 @@ final class JsonResumableSessionQueryTask
implements Callable> {
private final HttpClientContext context;
- private final String uploadId;
+ private final JsonResumableWrite jsonResumableWrite;
- JsonResumableSessionQueryTask(HttpClientContext context, String uploadId) {
+ JsonResumableSessionQueryTask(HttpClientContext context, JsonResumableWrite jsonResumableWrite) {
this.context = context;
- this.uploadId = uploadId;
+ this.jsonResumableWrite = jsonResumableWrite;
}
public ResumableOperationResult<@Nullable StorageObject> call() {
HttpResponse response = null;
+ String uploadId = jsonResumableWrite.getUploadId();
try {
HttpRequest req =
context
@@ -50,7 +53,11 @@ final class JsonResumableSessionQueryTask
.buildPutRequest(new GenericUrl(uploadId), new EmptyContent())
.setParser(context.getObjectParser());
req.setThrowExceptionOnExecuteError(false);
- req.getHeaders().setContentRange(HttpContentRange.query().getHeaderValue());
+ HttpHeaders headers = req.getHeaders();
+ headers.setContentRange(HttpContentRange.query().getHeaderValue());
+ for (Entry e : jsonResumableWrite.getExtraHeaders().entrySet()) {
+ headers.set(e.getKey(), e.getValue());
+ }
response = req.execute();
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableWrite.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableWrite.java
index 336ce0e477..41bbec72ec 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableWrite.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableWrite.java
@@ -21,6 +21,7 @@
import com.google.api.services.storage.model.StorageObject;
import com.google.cloud.storage.spi.v1.StorageRpc;
import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.stream.JsonReader;
import java.io.IOException;
@@ -60,6 +61,16 @@ private JsonResumableWrite(
this.beginOffset = beginOffset;
}
+ ImmutableMap getExtraHeaders() {
+ if (options != null) {
+ Object tmp = options.get(StorageRpc.Option.EXTRA_HEADERS);
+ if (tmp != null) {
+ return (ImmutableMap) tmp;
+ }
+ }
+ return ImmutableMap.of();
+ }
+
public @NonNull String getUploadId() {
return uploadId;
}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ResumableSessionFailureScenario.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ResumableSessionFailureScenario.java
index aab5263397..390b90dcfd 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ResumableSessionFailureScenario.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ResumableSessionFailureScenario.java
@@ -35,7 +35,6 @@
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -295,12 +294,12 @@ static boolean isContinue(int code) {
// The header names from HttpHeaders are lower cased, define some utility methods to create
// predicates where we can specify values ignoring case
private static Predicate matches(String expected) {
- String lower = expected.toLowerCase(Locale.US);
+ String lower = Utils.headerNameToLowerCase(expected);
return lower::equals;
}
private static Predicate startsWith(String prefix) {
- String lower = prefix.toLowerCase(Locale.US);
+ String lower = Utils.headerNameToLowerCase(prefix);
return s -> s.startsWith(lower);
}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
index 8d6f479eb9..6ed3c5af88 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
@@ -471,6 +471,59 @@ public static BucketTargetOption projection(@NonNull String projection) {
return new BucketTargetOption(UnifiedOpts.projection(projection));
}
+ /**
+ * A set of extra headers to be set for all requests performed within the scope of the operation
+ * this option is passed to (a get, read, resumable upload etc).
+ *
+ *
If the same header name is specified across multiple options provided to a method, the
+ * first occurrence will be the value included in the request(s).
+ *
+ *
The following headers are not allowed to be specified, and will result in an {@link
+ * IllegalArgumentException}.
+ *
+ *
+ *
+ * @since 2.49.0
+ */
+ @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+ public static BucketTargetOption extraHeaders(
+ @NonNull ImmutableMap extraHeaders) {
+ return new BucketTargetOption(UnifiedOpts.extraHeaders(extraHeaders));
+ }
+
/**
* Deduplicate any options which are the same parameter. The value which comes last in {@code
* os} will be the value included in the return.
@@ -545,6 +598,59 @@ public static BucketSourceOption requestedPolicyVersion(long version) {
return new BucketSourceOption(UnifiedOpts.requestedPolicyVersion(version));
}
+ /**
+ * A set of extra headers to be set for all requests performed within the scope of the operation
+ * this option is passed to (a get, read, resumable upload etc).
+ *
+ *
If the same header name is specified across multiple options provided to a method, the
+ * first occurrence will be the value included in the request(s).
+ *
+ *
The following headers are not allowed to be specified, and will result in an {@link
+ * IllegalArgumentException}.
+ *
+ *
+ *
+ * @since 2.49.0
+ */
+ @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+ public static BucketSourceOption extraHeaders(
+ @NonNull ImmutableMap extraHeaders) {
+ return new BucketSourceOption(UnifiedOpts.extraHeaders(extraHeaders));
+ }
+
/**
* Deduplicate any options which are the same parameter. The value which comes last in {@code
* os} will be the value included in the return.
@@ -633,6 +739,59 @@ public static ListHmacKeysOption projectId(@NonNull String projectId) {
return new ListHmacKeysOption(UnifiedOpts.projectId(projectId));
}
+ /**
+ * A set of extra headers to be set for all requests performed within the scope of the operation
+ * this option is passed to (a get, read, resumable upload etc).
+ *
+ *
If the same header name is specified across multiple options provided to a method, the
+ * first occurrence will be the value included in the request(s).
+ *
+ *
The following headers are not allowed to be specified, and will result in an {@link
+ * IllegalArgumentException}.
+ *
+ *
+ *
+ * @since 2.49.0
+ */
+ @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+ public static ListHmacKeysOption extraHeaders(
+ @NonNull ImmutableMap extraHeaders) {
+ return new ListHmacKeysOption(UnifiedOpts.extraHeaders(extraHeaders));
+ }
+
/**
* Deduplicate any options which are the same parameter. The value which comes last in {@code
* os} will be the value included in the return.
@@ -691,6 +850,59 @@ public static CreateHmacKeyOption projectId(@NonNull String projectId) {
return new CreateHmacKeyOption(UnifiedOpts.projectId(projectId));
}
+ /**
+ * A set of extra headers to be set for all requests performed within the scope of the operation
+ * this option is passed to (a get, read, resumable upload etc).
+ *
+ *
If the same header name is specified across multiple options provided to a method, the
+ * first occurrence will be the value included in the request(s).
+ *
+ *
The following headers are not allowed to be specified, and will result in an {@link
+ * IllegalArgumentException}.
+ *
+ *
+ *
+ * @since 2.49.0
+ */
+ @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+ public static CreateHmacKeyOption extraHeaders(
+ @NonNull ImmutableMap extraHeaders) {
+ return new CreateHmacKeyOption(UnifiedOpts.extraHeaders(extraHeaders));
+ }
+
/**
* Deduplicate any options which are the same parameter. The value which comes last in {@code
* os} will be the value included in the return.
@@ -726,6 +938,7 @@ public static CreateHmacKeyOption[] dedupe(
/** Class for specifying getHmacKey options */
class GetHmacKeyOption extends Option {
+
private GetHmacKeyOption(HmacKeySourceOpt opt) {
super(opt);
}
@@ -748,6 +961,59 @@ public static GetHmacKeyOption projectId(@NonNull String projectId) {
return new GetHmacKeyOption(UnifiedOpts.projectId(projectId));
}
+ /**
+ * A set of extra headers to be set for all requests performed within the scope of the operation
+ * this option is passed to (a get, read, resumable upload etc).
+ *
+ *
If the same header name is specified across multiple options provided to a method, the
+ * first occurrence will be the value included in the request(s).
+ *
+ *
The following headers are not allowed to be specified, and will result in an {@link
+ * IllegalArgumentException}.
+ *
+ *
+ *
+ * @since 2.49.0
+ */
+ @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+ public static GetHmacKeyOption extraHeaders(
+ @NonNull ImmutableMap extraHeaders) {
+ return new GetHmacKeyOption(UnifiedOpts.extraHeaders(extraHeaders));
+ }
+
/**
* Deduplicate any options which are the same parameter. The value which comes last in {@code
* os} will be the value included in the return.
@@ -782,6 +1048,7 @@ public static GetHmacKeyOption[] dedupe(GetHmacKeyOption[] array, GetHmacKeyOpti
/** Class for specifying deleteHmacKey options */
class DeleteHmacKeyOption extends Option {
+
private DeleteHmacKeyOption(HmacKeyTargetOpt opt) {
super(opt);
}
@@ -795,6 +1062,59 @@ public static DeleteHmacKeyOption userProject(@NonNull String userProject) {
return new DeleteHmacKeyOption(UnifiedOpts.userProject(userProject));
}
+ /**
+ * A set of extra headers to be set for all requests performed within the scope of the operation
+ * this option is passed to (a get, read, resumable upload etc).
+ *
+ *
If the same header name is specified across multiple options provided to a method, the
+ * first occurrence will be the value included in the request(s).
+ *
+ *
The following headers are not allowed to be specified, and will result in an {@link
+ * IllegalArgumentException}.
+ *
+ *
+ *
+ * @since 2.49.0
+ */
+ @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+ public static DeleteHmacKeyOption extraHeaders(
+ @NonNull ImmutableMap extraHeaders) {
+ return new DeleteHmacKeyOption(UnifiedOpts.extraHeaders(extraHeaders));
+ }
+
/**
* Deduplicate any options which are the same parameter. The value which comes last in {@code
* os} will be the value included in the return.
@@ -830,6 +1150,7 @@ public static DeleteHmacKeyOption[] dedupe(
/** Class for specifying updateHmacKey options */
class UpdateHmacKeyOption extends Option {
+
private UpdateHmacKeyOption(HmacKeyTargetOpt opt) {
super(opt);
}
@@ -843,6 +1164,59 @@ public static UpdateHmacKeyOption userProject(@NonNull String userProject) {
return new UpdateHmacKeyOption(UnifiedOpts.userProject(userProject));
}
+ /**
+ * A set of extra headers to be set for all requests performed within the scope of the operation
+ * this option is passed to (a get, read, resumable upload etc).
+ *
+ *
If the same header name is specified across multiple options provided to a method, the
+ * first occurrence will be the value included in the request(s).
+ *
+ *
The following headers are not allowed to be specified, and will result in an {@link
+ * IllegalArgumentException}.
+ *
+ *
+ *
+ * @since 2.49.0
+ */
+ @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+ public static UpdateHmacKeyOption extraHeaders(
+ @NonNull ImmutableMap extraHeaders) {
+ return new UpdateHmacKeyOption(UnifiedOpts.extraHeaders(extraHeaders));
+ }
+
/**
* Deduplicate any options which are the same parameter. The value which comes last in {@code
* os} will be the value included in the return.
@@ -929,6 +1303,58 @@ public static BucketGetOption fields(BucketField... fields) {
return new BucketGetOption(UnifiedOpts.fields(set));
}
+ /**
+ * A set of extra headers to be set for all requests performed within the scope of the operation
+ * this option is passed to (a get, read, resumable upload etc).
+ *
+ *
If the same header name is specified across multiple options provided to a method, the
+ * first occurrence will be the value included in the request(s).
+ *
+ *
The following headers are not allowed to be specified, and will result in an {@link
+ * IllegalArgumentException}.
+ *
+ *
+ *
+ * @since 2.49.0
+ */
+ @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+ public static BucketGetOption extraHeaders(@NonNull ImmutableMap extraHeaders) {
+ return new BucketGetOption(UnifiedOpts.extraHeaders(extraHeaders));
+ }
+
/**
* Deduplicate any options which are the same parameter. The value which comes last in {@code
* os} will be the value included in the return.
@@ -1120,6 +1546,59 @@ public static BlobTargetOption overrideUnlockedRetention(boolean overrideUnlocke
return new BlobTargetOption(UnifiedOpts.overrideUnlockedRetention(overrideUnlockedRetention));
}
+ /**
+ * A set of extra headers to be set for all requests performed within the scope of the operation
+ * this option is passed to (a get, read, resumable upload etc).
+ *
+ *
If the same header name is specified across multiple options provided to a method, the
+ * first occurrence will be the value included in the request(s).
+ *
+ *
The following headers are not allowed to be specified, and will result in an {@link
+ * IllegalArgumentException}.
+ *
+ *
+ *
+ * @since 2.49.0
+ */
+ @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+ public static BlobTargetOption extraHeaders(
+ @NonNull ImmutableMap extraHeaders) {
+ return new BlobTargetOption(UnifiedOpts.extraHeaders(extraHeaders));
+ }
+
/**
* Deduplicate any options which are the same parameter. The value which comes last in {@code
* os} will be the value included in the return.
@@ -1346,6 +1825,58 @@ public static BlobWriteOption expectedObjectSize(long objectContentSize) {
return new BlobWriteOption(UnifiedOpts.resumableUploadExpectedObjectSize(objectContentSize));
}
+ /**
+ * A set of extra headers to be set for all requests performed within the scope of the operation
+ * this option is passed to (a get, read, resumable upload etc).
+ *
+ *
If the same header name is specified across multiple options provided to a method, the
+ * first occurrence will be the value included in the request(s).
+ *
+ *
The following headers are not allowed to be specified, and will result in an {@link
+ * IllegalArgumentException}.
+ *
+ *
+ *
+ * @since 2.49.0
+ */
+ @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+ public static BlobWriteOption extraHeaders(@NonNull ImmutableMap extraHeaders) {
+ return new BlobWriteOption(UnifiedOpts.extraHeaders(extraHeaders));
+ }
+
/**
* Deduplicate any options which are the same parameter. The value which comes last in {@code
* os} will be the value included in the return.
@@ -1490,6 +2021,59 @@ public static BlobSourceOption shouldReturnRawInputStream(boolean shouldReturnRa
return new BlobSourceOption(UnifiedOpts.returnRawInputStream(shouldReturnRawInputStream));
}
+ /**
+ * A set of extra headers to be set for all requests performed within the scope of the operation
+ * this option is passed to (a get, read, resumable upload etc).
+ *
+ *
If the same header name is specified across multiple options provided to a method, the
+ * first occurrence will be the value included in the request(s).
+ *
+ *
The following headers are not allowed to be specified, and will result in an {@link
+ * IllegalArgumentException}.
+ *
+ *
+ *
+ * @since 2.49.0
+ */
+ @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+ public static BlobSourceOption extraHeaders(
+ @NonNull ImmutableMap extraHeaders) {
+ return new BlobSourceOption(UnifiedOpts.extraHeaders(extraHeaders));
+ }
+
/**
* Deduplicate any options which are the same parameter. The value which comes last in {@code
* os} will be the value included in the return.
@@ -1658,6 +2242,58 @@ public static BlobGetOption softDeleted(boolean softDeleted) {
return new BlobGetOption(UnifiedOpts.softDeleted(softDeleted));
}
+ /**
+ * A set of extra headers to be set for all requests performed within the scope of the operation
+ * this option is passed to (a get, read, resumable upload etc).
+ *
+ *
If the same header name is specified across multiple options provided to a method, the
+ * first occurrence will be the value included in the request(s).
+ *
+ *
The following headers are not allowed to be specified, and will result in an {@link
+ * IllegalArgumentException}.
+ *
+ *
+ *
+ * @since 2.49.0
+ */
+ @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+ public static BlobGetOption extraHeaders(@NonNull ImmutableMap extraHeaders) {
+ return new BlobGetOption(UnifiedOpts.extraHeaders(extraHeaders));
+ }
+
/**
* Deduplicate any options which are the same parameter. The value which comes last in {@code
* os} will be the value included in the return.
@@ -1743,6 +2379,59 @@ public static BlobRestoreOption metagenerationNotMatch(long generation) {
public static BlobRestoreOption copySourceAcl(boolean copySourceAcl) {
return new BlobRestoreOption(UnifiedOpts.copySourceAcl(copySourceAcl));
}
+
+ /**
+ * A set of extra headers to be set for all requests performed within the scope of the operation
+ * this option is passed to (a get, read, resumable upload etc).
+ *
+ *
If the same header name is specified across multiple options provided to a method, the
+ * first occurrence will be the value included in the request(s).
+ *
+ *
The following headers are not allowed to be specified, and will result in an {@link
+ * IllegalArgumentException}.
+ *
+ *
+ *
+ * @since 2.49.0
+ */
+ @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+ public static BlobRestoreOption extraHeaders(
+ @NonNull ImmutableMap extraHeaders) {
+ return new BlobRestoreOption(UnifiedOpts.extraHeaders(extraHeaders));
+ }
}
/** Class for specifying bucket list options. */
@@ -1802,6 +2491,59 @@ public static BucketListOption fields(BucketField... fields) {
return new BucketListOption(UnifiedOpts.fields(set));
}
+ /**
+ * A set of extra headers to be set for all requests performed within the scope of the operation
+ * this option is passed to (a get, read, resumable upload etc).
+ *
+ *
If the same header name is specified across multiple options provided to a method, the
+ * first occurrence will be the value included in the request(s).
+ *
+ *
The following headers are not allowed to be specified, and will result in an {@link
+ * IllegalArgumentException}.
+ *
+ *
+ *
+ * @since 2.49.0
+ */
+ @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+ public static BucketListOption extraHeaders(
+ @NonNull ImmutableMap extraHeaders) {
+ return new BucketListOption(UnifiedOpts.extraHeaders(extraHeaders));
+ }
+
/**
* Deduplicate any options which are the same parameter. The value which comes last in {@code
* os} will be the value included in the return.
@@ -1979,6 +2721,58 @@ public static BlobListOption softDeleted(boolean softDeleted) {
return new BlobListOption(UnifiedOpts.softDeleted(softDeleted));
}
+ /**
+ * A set of extra headers to be set for all requests performed within the scope of the operation
+ * this option is passed to (a get, read, resumable upload etc).
+ *
+ *
If the same header name is specified across multiple options provided to a method, the
+ * first occurrence will be the value included in the request(s).
+ *
+ *
The following headers are not allowed to be specified, and will result in an {@link
+ * IllegalArgumentException}.
+ *
+ *
+ *
+ * @since 2.49.0
+ */
+ @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+ public static BlobListOption extraHeaders(@NonNull ImmutableMap extraHeaders) {
+ return new BlobListOption(UnifiedOpts.extraHeaders(extraHeaders));
+ }
+
/**
* Deduplicate any options which are the same parameter. The value which comes last in {@code
* os} will be the value included in the return.
@@ -5036,8 +5830,6 @@ default BlobWriteSession blobWriteSession(BlobInfo blobInfo, BlobWriteOption...
* {@link Storage#delete(BlobId)}, however without the ability to change metadata fields for the
* target object.
*
- *
This feature is currently only supported for HNS (Hierarchical Namespace) buckets.
- *
* @since 2.48.0
*/
@TransportCompatibility({Transport.HTTP, Transport.GRPC})
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
index 6c9828c1b1..09a335f35a 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
@@ -62,7 +62,10 @@
import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
-import java.util.Locale;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
@@ -565,6 +568,18 @@ static Md5MatchExtractor md5MatchExtractor() {
return Md5MatchExtractor.INSTANCE;
}
+ static Headers extraHeaders(ImmutableMap extraHeaders) {
+ requireNonNull(extraHeaders, "extraHeaders must be non null");
+ String blockedHeaders =
+ extraHeaders.keySet().stream()
+ .map(Utils::headerNameToLowerCase)
+ .filter(Headers.BLOCKLIST)
+ .sorted(Comparator.naturalOrder())
+ .collect(Collectors.joining(", ", "[", "]"));
+ checkArgument("[]".equals(blockedHeaders), "Disallowed headers: %s", blockedHeaders);
+ return new Headers(extraHeaders);
+ }
+
static final class Crc32cMatch implements ObjectTargetOpt {
private static final long serialVersionUID = 8172282701777561769L;
private final int val;
@@ -1920,6 +1935,183 @@ public Mapper moveObject() {
}
}
+ static final class Headers extends RpcOptVal>
+ implements BucketSourceOpt,
+ BucketTargetOpt,
+ BucketListOpt,
+ ObjectSourceOpt,
+ ObjectTargetOpt,
+ ObjectListOpt,
+ HmacKeySourceOpt,
+ HmacKeyTargetOpt,
+ HmacKeyListOpt {
+
+ /**
+ * The set of header names which are blocked from being able to be provided for an instance of
+ * this class.
+ *
+ *