diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml
index ad681c4be3..84d9907047 100644
--- a/google-cloud-storage/clirr-ignored-differences.xml
+++ b/google-cloud-storage/clirr-ignored-differences.xml
@@ -1,29 +1,11 @@
-
+
7012
- com/google/cloud/storage/UnbufferedWritableByteChannelSession$UnbufferedWritableByteChannel
- * write(*)
-
-
-
- 7012
- com/google/cloud/storage/spi/v1/StorageRpc
- * getStorage()
-
-
-
- 8001
- com/google/cloud/storage/Hasher$ConstantConcatValueHasher
-
-
-
-
- 7002
- com/google/cloud/storage/HttpDownloadSessionBuilder$ReadableByteChannelSessionBuilder
- com.google.cloud.storage.HttpDownloadSessionBuilder$ReadableByteChannelSessionBuilder setCallback(java.util.function.Consumer)
+ com/google/cloud/storage/Storage
+ com.google.cloud.storage.BlobWriteSession blobWriteSession(com.google.cloud.storage.BlobInfo, com.google.cloud.storage.Storage$BlobWriteOption[])
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSession.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSession.java
new file mode 100644
index 0000000000..02ea23a6a7
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSession.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023 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.ApiFuture;
+import com.google.api.core.BetaApi;
+import java.io.IOException;
+import java.nio.channels.WritableByteChannel;
+
+/**
+ * A session to write an object to Google Cloud Storage.
+ *
+ *
A session can only write a single version of an object. If writing multiple versions of an
+ * object a new session must be created each time.
+ *
+ *
Provides an api that allows writing to and retrieving the resulting {@link BlobInfo} after
+ * write finalization.
+ *
+ *
The underlying implementation is dictated based upon the specified {@link
+ * BlobWriteSessionConfig} provided at {@link StorageOptions} creation time.
+ *
+ * @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig)
+ * @see BlobWriteSessionConfig
+ * @see BlobWriteSessionConfigs
+ * @since 2.26.0 This new api is in preview and is subject to breaking changes.
+ */
+@BetaApi
+public interface BlobWriteSession {
+
+ /**
+ * Open the {@link WritableByteChannel} for this session.
+ *
+ *
A session may only be {@code open}ed once. If multiple calls to open are made, an illegal
+ * state exception will be thrown
+ *
+ *
Upon calling {@link WritableByteChannel#close()} the object creation will be finalized, and
+ * {@link #getResult()}s future should resolve.
+ *
+ * @throws IOException When creating the {@link WritableByteChannel} if an unrecoverable
+ * underlying IOException occurs it can be rethrown
+ * @throws IllegalStateException if open is called more than once
+ * @since 2.26.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ WritableByteChannel open() throws IOException;
+
+ /**
+ * Return an {@link ApiFuture}{@code } which will represent the state of the object upon
+ * finalization and success response from Google Cloud Storage.
+ *
+ *
This future will not resolve until: 1. The object is successfully finalized and created in
+ * Google Cloud Storage 2. A terminal failure occurs, the terminal failure will become the
+ * exception result
+ *
+ * @since 2.26.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ ApiFuture getResult();
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfig.java
new file mode 100644
index 0000000000..de8622c754
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfig.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023 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.InternalApi;
+import com.google.cloud.storage.Conversions.Decoder;
+import com.google.cloud.storage.Storage.BlobWriteOption;
+import com.google.cloud.storage.UnifiedOpts.ObjectTargetOpt;
+import com.google.cloud.storage.UnifiedOpts.Opts;
+import com.google.storage.v2.WriteObjectResponse;
+import java.io.IOException;
+import java.time.Clock;
+
+/**
+ * A sealed internal implementation only class which provides the means of configuring a {@link
+ * BlobWriteSession}.
+ *
+ *
A {@code BlobWriteSessionConfig} will be used to configure all {@link BlobWriteSession}s
+ * produced by an instance of {@link Storage}.
+ *
+ * @see BlobWriteSessionConfigs
+ * @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig)
+ * @see Storage#blobWriteSession(BlobInfo, BlobWriteOption...)
+ * @since 2.26.0 This new api is in preview and is subject to breaking changes.
+ */
+// When we have java modules, actually seal this to internal extension only
+@InternalApi
+public abstract class BlobWriteSessionConfig {
+
+ @InternalApi
+ BlobWriteSessionConfig() {}
+
+ @InternalApi
+ abstract WriterFactory createFactory(Clock clock) throws IOException;
+
+ @InternalApi
+ interface WriterFactory {
+ @InternalApi
+ WritableByteChannelSession, BlobInfo> writeSession(
+ StorageInternal s,
+ BlobInfo info,
+ Opts opts,
+ Decoder d);
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfigs.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfigs.java
new file mode 100644
index 0000000000..cc5e691e6b
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfigs.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 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.GrpcStorageOptions.GrpcStorageDefaults;
+import com.google.cloud.storage.Storage.BlobWriteOption;
+
+/**
+ * Factory class to select and construct {@link BlobWriteSessionConfig}s.
+ *
+ * @see BlobWriteSessionConfig
+ * @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig)
+ * @see Storage#blobWriteSession(BlobInfo, BlobWriteOption...)
+ * @since 2.26.0 This new api is in preview and is subject to breaking changes.
+ */
+@BetaApi
+public final class BlobWriteSessionConfigs {
+
+ private BlobWriteSessionConfigs() {}
+
+ /**
+ * Factory to produce the default configuration for uploading an object to Cloud Storage.
+ *
+ *
Configuration of the chunk size can be performed via {@link
+ * DefaultBlobWriteSessionConfig#withChunkSize(int)}.
+ *
+ * @see GrpcStorageDefaults#getDefaultStorageWriterConfig()
+ * @since 2.26.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public static DefaultBlobWriteSessionConfig getDefault() {
+ return new DefaultBlobWriteSessionConfig(ByteSizeConstants._16MiB);
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessions.java
new file mode 100644
index 0000000000..878552a125
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessions.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 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.ApiFuture;
+import java.io.IOException;
+import java.nio.channels.WritableByteChannel;
+
+final class BlobWriteSessions {
+
+ private BlobWriteSessions() {}
+
+ static BlobWriteSession of(WritableByteChannelSession, BlobInfo> s) {
+ return new WritableByteChannelSessionAdapter(s);
+ }
+
+ static final class WritableByteChannelSessionAdapter implements BlobWriteSession {
+ private final WritableByteChannelSession, BlobInfo> delegate;
+
+ private WritableByteChannelSessionAdapter(WritableByteChannelSession, BlobInfo> delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public WritableByteChannel open() throws IOException {
+ return delegate.open();
+ }
+
+ @Override
+ public ApiFuture getResult() {
+ return delegate.getResult();
+ }
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/CrossTransportUtils.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/CrossTransportUtils.java
new file mode 100644
index 0000000000..1c5aa1d97d
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/CrossTransportUtils.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2023 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.cloud.storage.TransportCompatibility.Transport;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+final class CrossTransportUtils {
+
+ static T throwHttpJsonOnly(String methodName) {
+ return throwHttpJsonOnly(Storage.class, methodName);
+ }
+
+ static T throwHttpJsonOnly(Class> clazz, String methodName) {
+ return throwTransportOnly(clazz, methodName, Transport.HTTP);
+ }
+
+ static T throwGrpcOnly(String methodName) {
+ return throwGrpcOnly(Storage.class, methodName);
+ }
+
+ static T throwGrpcOnly(Class> clazz, String methodName) {
+ return throwTransportOnly(clazz, methodName, Transport.GRPC);
+ }
+
+ static T throwTransportOnly(Class> clazz, String methodName, Transport transport) {
+ String builder;
+ switch (transport) {
+ case HTTP:
+ builder = "StorageOptions.http()";
+ break;
+ case GRPC:
+ builder = "StorageOptions.grpc()";
+ break;
+ default:
+ throw new IllegalStateException(
+ String.format("Broken Java Enum: %s received value: '%s'", Transport.class, transport));
+ }
+ String message =
+ String.format(
+ "%s#%s is only supported for %s transport. Please use %s to construct a compatible instance.",
+ clazz.getName(), methodName, transport, builder);
+ throw new UnsupportedOperationException(message);
+ }
+
+ static String fmtMethodName(String name, Class>... args) {
+ return name
+ + "("
+ + Arrays.stream(args).map(Class::getName).collect(Collectors.joining(", "))
+ + ")";
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java
new file mode 100644
index 0000000000..dfea0d4190
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2023 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.ApiFuture;
+import com.google.api.core.ApiFutures;
+import com.google.api.core.BetaApi;
+import com.google.api.core.InternalApi;
+import com.google.cloud.storage.BufferedWritableByteChannelSession.BufferedWritableByteChannel;
+import com.google.cloud.storage.Conversions.Decoder;
+import com.google.cloud.storage.UnifiedOpts.ObjectTargetOpt;
+import com.google.cloud.storage.UnifiedOpts.Opts;
+import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.storage.v2.WriteObjectResponse;
+import java.nio.channels.WritableByteChannel;
+import java.time.Clock;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Default Configuration to represent uploading to Google Cloud Storage in a chunked manner.
+ *
+ *
Perform a resumable upload, uploading at most {@code chunkSize} bytes each PUT.
+ *
+ *
Configuration of chunk size can be performed via {@link
+ * DefaultBlobWriteSessionConfig#withChunkSize(int)}.
+ *
+ *
An instance of this class will provide a {@link BlobWriteSession} is logically equivalent to
+ * the following:
+ *
+ *
+ *
+ * @since 2.26.0 This new api is in preview and is subject to breaking changes.
+ */
+@Immutable
+@BetaApi
+public final class DefaultBlobWriteSessionConfig extends BlobWriteSessionConfig {
+
+ private final int chunkSize;
+
+ @InternalApi
+ DefaultBlobWriteSessionConfig(int chunkSize) {
+ this.chunkSize = chunkSize;
+ }
+
+ /**
+ * The number of bytes each chunk can be.
+ *
+ *
Default: {@code 16777216 (16 MiB)}
+ *
+ * @see #withChunkSize(int)
+ * @since 2.26.0 This new api is in preview and is subject to breaking changes.
+ */
+ public int getChunkSize() {
+ return chunkSize;
+ }
+
+ /**
+ * Create a new instance with the {@code chunkSize} set to the specified value.
+ *
+ *
Default: {@code 16777216 (16 MiB)}
+ *
+ * @param chunkSize The number of bytes each chunk should be. Must be >= {@code 262144 (256 KiB)}
+ * @return The new instance
+ * @see #getChunkSize()
+ * @since 2.26.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public DefaultBlobWriteSessionConfig withChunkSize(int chunkSize) {
+ Preconditions.checkArgument(
+ chunkSize >= ByteSizeConstants._256KiB,
+ "chunkSize must be >= %d",
+ ByteSizeConstants._256KiB);
+ return new DefaultBlobWriteSessionConfig(chunkSize);
+ }
+
+ @Override
+ @InternalApi
+ WriterFactory createFactory(Clock clock) {
+ return new Factory(chunkSize);
+ }
+
+ @InternalApi
+ private static final class Factory implements WriterFactory {
+
+ private final int chunkSize;
+
+ private Factory(int chunkSize) {
+ this.chunkSize = chunkSize;
+ }
+
+ @InternalApi
+ @Override
+ public WritableByteChannelSession, BlobInfo> writeSession(
+ StorageInternal s,
+ BlobInfo info,
+ Opts opts,
+ Decoder d) {
+ // todo: invert this
+ // make GrpcBlobWriteChannel use this factory to produce its WriteSession
+ if (s instanceof GrpcStorageImpl) {
+ GrpcStorageImpl g = (GrpcStorageImpl) s;
+ GrpcBlobWriteChannel writer = g.internalWriter(info, opts);
+ writer.setChunkSize(chunkSize);
+ WritableByteChannelSession session =
+ writer.newLazyWriteChannel().getSession();
+ return new DecoratedWritableByteChannelSession<>(session, d);
+ }
+ return CrossTransportUtils.throwGrpcOnly(DefaultBlobWriteSessionConfig.class, "");
+ }
+ }
+
+ private static final class DecoratedWritableByteChannelSession
+ implements WritableByteChannelSession {
+
+ private final WritableByteChannelSession delegate;
+ private final Decoder decoder;
+
+ private DecoratedWritableByteChannelSession(
+ WritableByteChannelSession delegate, Decoder decoder) {
+ this.delegate = delegate;
+ this.decoder = decoder;
+ }
+
+ @Override
+ public WBC open() {
+ try {
+ return WritableByteChannelSession.super.open();
+ } catch (Exception e) {
+ throw StorageException.coalesce(e);
+ }
+ }
+
+ @Override
+ public ApiFuture openAsync() {
+ return delegate.openAsync();
+ }
+
+ @Override
+ public ApiFuture getResult() {
+ return ApiFutures.transform(
+ delegate.getResult(), decoder::decode, MoreExecutors.directExecutor());
+ }
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicCopyWriter.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicCopyWriter.java
index cae70d6767..038ff46672 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicCopyWriter.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicCopyWriter.java
@@ -87,6 +87,6 @@ public void copyChunk() {
@Override
public RestorableState capture() {
- return GrpcStorageImpl.throwHttpJsonOnly(CopyWriter.class, "capture");
+ return CrossTransportUtils.throwHttpJsonOnly(CopyWriter.class, "capture");
}
}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobReadChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobReadChannel.java
index b58b9663f7..4ae3f24466 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobReadChannel.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobReadChannel.java
@@ -43,7 +43,7 @@ final class GrpcBlobReadChannel extends BaseStorageReadChannel