Repository: googleapis/java-storage
Pull request #2123 (2023-07-17 18:59:55)

feat: introduce new BlobWriteSession
When writing a new Blob to GCS, there are secondary session related state and actions which can't be represented by WriteChannel.

BlobWriteSession provides a new construct to allow retrieving the resultant object which is created after the WritableByteChannel is closed.

Along with the new session, configuration for this is now performed at the StorageOptions level where cross session considerations can influence the implementation of the returned session. The configurable option present for this new StorageWriterConfig is chunkSize. In the future new configurations will be added with their corresponding options.

For example, in a future release it will be possible to change from in memory buffering to instead buffer to disk thereby reducing heap usage.


generated-files-bot[bot]: <!-- probot comment [12634855]-->
*Warning*: This pull request is touching the following templated files:

* samples/snapshot/pom.xml

Patch:
diff --git a/README.md b/README.md
index ec3dc8c5..1aa0a611 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ If you are using Maven without the BOM, add this to your dependencies:
 <dependency>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage</artifactId>
-  <version>2.24.0</version>
+  <version>2.25.0</version>
 </dependency>
 
 ```
@@ -57,13 +57,13 @@ implementation 'com.google.cloud:google-cloud-storage'
 If you are using Gradle without BOM, add this to your dependencies:
 
 ```Groovy
-implementation 'com.google.cloud:google-cloud-storage:2.24.0'
+implementation 'com.google.cloud:google-cloud-storage:2.25.0'
 ```
 
 If you are using SBT, add this to your dependencies:
 
 ```Scala
-libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.24.0"
+libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.25.0"
 ```
 <!-- {x-version-update-end} -->
 
@@ -428,7 +428,7 @@ Java is a registered trademark of Oracle and/or its affiliates.
 [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-storage/java11.html
 [stability-image]: https://img.shields.io/badge/stability-stable-green
 [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-storage.svg
-[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-storage/2.24.0
+[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-storage/2.25.0
 [authentication]: https://github.com/googleapis/google-cloud-java#authentication
 [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
 [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles
diff --git a/gapic-google-cloud-storage-v2/pom.xml b/gapic-google-cloud-storage-v2/pom.xml
index 83590fc9..b5d90a4d 100644
--- a/gapic-google-cloud-storage-v2/pom.xml
+++ b/gapic-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>gapic-google-cloud-storage-v2</artifactId>
-  <version>2.25.0-alpha</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
+  <version>2.25.1-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
   <name>gapic-google-cloud-storage-v2</name>
   <description>GRPC library for gapic-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.25.0</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.25.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/google-cloud-storage-bom/pom.xml b/google-cloud-storage-bom/pom.xml
index 9df55b37..522b0c80 100644
--- a/google-cloud-storage-bom/pom.xml
+++ b/google-cloud-storage-bom/pom.xml
@@ -19,7 +19,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage-bom</artifactId>
-  <version>2.25.0</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.25.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <packaging>pom</packaging>
   <parent>
     <groupId>com.google.cloud</groupId>
@@ -69,22 +69,22 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage</artifactId>
-        <version>2.25.0</version><!-- {x-version-update:google-cloud-storage:current} -->
+        <version>2.25.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>gapic-google-cloud-storage-v2</artifactId>
-        <version>2.25.0-alpha</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
+        <version>2.25.1-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>grpc-google-cloud-storage-v2</artifactId>
-        <version>2.25.0-alpha</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.25.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>proto-google-cloud-storage-v2</artifactId>
-        <version>2.25.0-alpha</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+        <version>2.25.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
       </dependency>
     </dependencies>
   </dependencyManagement>
diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml
index ad681c4b..84d99070 100644
--- a/google-cloud-storage/clirr-ignored-differences.xml
+++ b/google-cloud-storage/clirr-ignored-differences.xml
@@ -1,29 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- see https://www.mojohaus.org/clirr-maven-plugin/examples/ignored-differences.html -->
 <differences>
-  <!-- Not breaking, internal only interface and the new methods have default implementations -->
+  <!-- Not breaking, new method has a default implementation -->
   <difference>
     <differenceType>7012</differenceType>
-    <className>com/google/cloud/storage/UnbufferedWritableByteChannelSession$UnbufferedWritableByteChannel</className>
-    <method>* write(*)</method>
-  </difference>
-  <!-- Allow accessing the underlying Apiary instance -->
-  <difference>
-    <differenceType>7012</differenceType>
-    <className>com/google/cloud/storage/spi/v1/StorageRpc</className>
-    <method>* getStorage()</method>
-  </difference>
-
-  <difference>
-    <differenceType>8001</differenceType>
-    <className>com/google/cloud/storage/Hasher$ConstantConcatValueHasher</className>
-  </difference>
-
-  <!-- Not breaking, internal only class -->
-  <difference>
-    <differenceType>7002</differenceType>
-    <className>com/google/cloud/storage/HttpDownloadSessionBuilder$ReadableByteChannelSessionBuilder</className>
-    <method>com.google.cloud.storage.HttpDownloadSessionBuilder$ReadableByteChannelSessionBuilder setCallback(java.util.function.Consumer)</method>
+    <className>com/google/cloud/storage/Storage</className>
+    <method>com.google.cloud.storage.BlobWriteSession blobWriteSession(com.google.cloud.storage.BlobInfo, com.google.cloud.storage.Storage$BlobWriteOption[])</method>
   </difference>
 
 </differences>
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index 2a1fc317..aeb9589e 100644
--- a/google-cloud-storage/pom.xml
+++ b/google-cloud-storage/pom.xml
@@ -2,7 +2,7 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <artifactId>google-cloud-storage</artifactId>
-  <version>2.25.0</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.25.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <packaging>jar</packaging>
   <name>Google Cloud Storage</name>
   <url>https://github.com/googleapis/java-storage</url>
@@ -12,7 +12,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.25.0</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.25.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <properties>
     <site.installationModule>google-cloud-storage</site.installationModule>
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 00000000..02ea23a6
--- /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.
+ *
+ * <p>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.
+ *
+ * <p>Provides an api that allows writing to and retrieving the resulting {@link BlobInfo} after
+ * write finalization.
+ *
+ * <p>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.
+   *
+   * <p>A session may only be {@code open}ed once. If multiple calls to open are made, an illegal
+   * state exception will be thrown
+   *
+   * <p>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 <BlobInfo>} which will represent the state of the object upon
+   * finalization and success response from Google Cloud Storage.
+   *
+   * <p>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<BlobInfo> 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 00000000..de8622c7
--- /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}.
+ *
+ * <p>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<ObjectTargetOpt> opts,
+        Decoder<WriteObjectResponse, BlobInfo> 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 00000000..cc5e691e
--- /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.
+   *
+   * <p>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 00000000..878552a1
--- /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<BlobInfo> 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 00000000..1c5aa1d9
--- /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> T throwHttpJsonOnly(String methodName) {
+    return throwHttpJsonOnly(Storage.class, methodName);
+  }
+
+  static <T> T throwHttpJsonOnly(Class<?> clazz, String methodName) {
+    return throwTransportOnly(clazz, methodName, Transport.HTTP);
+  }
+
+  static <T> T throwGrpcOnly(String methodName) {
+    return throwGrpcOnly(Storage.class, methodName);
+  }
+
+  static <T> T throwGrpcOnly(Class<?> clazz, String methodName) {
+    return throwTransportOnly(clazz, methodName, Transport.GRPC);
+  }
+
+  static <T> 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 00000000..dfea0d41
--- /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.
+ *
+ * <p>Perform a resumable upload, uploading at most {@code chunkSize} bytes each PUT.
+ *
+ * <p>Configuration of chunk size can be performed via {@link
+ * DefaultBlobWriteSessionConfig#withChunkSize(int)}.
+ *
+ * <p>An instance of this class will provide a {@link BlobWriteSession} is logically equivalent to
+ * the following:
+ *
+ * <pre>{@code
+ * Storage storage = ...;
+ * WriteChannel writeChannel = storage.writer(BlobInfo, BlobWriteOption);
+ * writeChannel.setChunkSize(chunkSize);
+ * }</pre>
+ *
+ * @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.
+   *
+   * <p><i>Default:</i> {@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.
+   *
+   * <p><i>Default:</i> {@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<ObjectTargetOpt> opts,
+        Decoder<WriteObjectResponse, BlobInfo> 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<BufferedWritableByteChannel, WriteObjectResponse> session =
+            writer.newLazyWriteChannel().getSession();
+        return new DecoratedWritableByteChannelSession<>(session, d);
+      }
+      return CrossTransportUtils.throwGrpcOnly(DefaultBlobWriteSessionConfig.class, "");
+    }
+  }
+
+  private static final class DecoratedWritableByteChannelSession<WBC extends WritableByteChannel, T>
+      implements WritableByteChannelSession<WBC, BlobInfo> {
+
+    private final WritableByteChannelSession<WBC, T> delegate;
+    private final Decoder<T, BlobInfo> decoder;
+
+    private DecoratedWritableByteChannelSession(
+        WritableByteChannelSession<WBC, T> delegate, Decoder<T, BlobInfo> 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<WBC> openAsync() {
+      return delegate.openAsync();
+    }
+
+    @Override
+    public ApiFuture<BlobInfo> 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 cae70d67..038ff466 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 @@ final class GapicCopyWriter extends CopyWriter {
 
   @Override
   public RestorableState<CopyWriter> 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 b58b9663..4ae3f244 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<Object> {
 
   @Override
   public RestorableState<ReadChannel> capture() {
-    return GrpcStorageImpl.throwHttpJsonOnly(ReadChannel.class, "capture");
+    return CrossTransportUtils.throwHttpJsonOnly(ReadChannel.class, "capture");
   }
 
   @Override
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobWriteChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobWriteChannel.java
index f3520180..a1b1d303 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobWriteChannel.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobWriteChannel.java
@@ -50,7 +50,7 @@ final class GrpcBlobWriteChannel extends BaseStorageWriteChannel<WriteObjectResp
 
   @Override
   public RestorableState<WriteChannel> capture() {
-    return GrpcStorageImpl.throwHttpJsonOnly(WriteChannel.class, "capture");
+    return CrossTransportUtils.throwHttpJsonOnly(WriteChannel.class, "capture");
   }
 
   @Override
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 d7d40591..fdd67a7e 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
@@ -18,6 +18,8 @@ package com.google.cloud.storage;
 
 import static com.google.cloud.storage.ByteSizeConstants._16MiB;
 import static com.google.cloud.storage.ByteSizeConstants._256KiB;
+import static com.google.cloud.storage.CrossTransportUtils.fmtMethodName;
+import static com.google.cloud.storage.CrossTransportUtils.throwHttpJsonOnly;
 import static com.google.cloud.storage.GrpcToHttpStatusCodeTranslation.resultRetryAlgorithmToCodes;
 import static com.google.cloud.storage.StorageV2ProtoUtils.bucketAclEntityOrAltEq;
 import static com.google.cloud.storage.StorageV2ProtoUtils.objectAclEntityOrAltEq;
@@ -44,6 +46,7 @@ import com.google.cloud.BaseService;
 import com.google.cloud.Policy;
 import com.google.cloud.WriteChannel;
 import com.google.cloud.storage.Acl.Entity;
+import com.google.cloud.storage.BlobWriteSessionConfig.WriterFactory;
 import com.google.cloud.storage.BufferedWritableByteChannelSession.BufferedWritableByteChannel;
 import com.google.cloud.storage.Conversions.Decoder;
 import com.google.cloud.storage.HmacKey.HmacKeyMetadata;
@@ -131,7 +134,6 @@ import java.nio.file.Files;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -145,13 +147,12 @@ import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.function.UnaryOperator;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 import org.checkerframework.checker.nullness.qual.Nullable;
 
 @BetaApi
-final class GrpcStorageImpl extends BaseService<StorageOptions> implements Storage {
+final class GrpcStorageImpl extends BaseService<StorageOptions> implements StorageInternal {
 
   private static final byte[] ZERO_BYTES = new byte[0];
   private static final Set<OpenOption> READ_OPS = ImmutableSet.of(StandardOpenOption.READ);
@@ -163,22 +164,30 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   private static final BucketSourceOption[] EMPTY_BUCKET_SOURCE_OPTIONS = new BucketSourceOption[0];
 
   final StorageClient storageClient;
+  final WriterFactory writerFactory;
   final GrpcConversions codecs;
   final GrpcRetryAlgorithmManager retryAlgorithmManager;
   final SyntaxDecoders syntaxDecoders;
+  private final Decoder<WriteObjectResponse, BlobInfo> writeObjectResponseBlobInfoDecoder;
 
   // workaround for https://github.com/googleapis/java-storage/issues/1736
   private final Opts<UserProject> defaultOpts;
   @Deprecated private final ProjectId defaultProjectId;
 
   GrpcStorageImpl(
-      GrpcStorageOptions options, StorageClient storageClient, Opts<UserProject> defaultOpts) {
+      GrpcStorageOptions options,
+      StorageClient storageClient,
+      WriterFactory writerFactory,
+      Opts<UserProject> defaultOpts) {
     super(options);
     this.storageClient = storageClient;
+    this.writerFactory = writerFactory;
     this.defaultOpts = defaultOpts;
     this.codecs = Conversions.grpc();
     this.retryAlgorithmManager = options.getRetryAlgorithmManager();
     this.syntaxDecoders = new SyntaxDecoders();
+    this.writeObjectResponseBlobInfoDecoder =
+        codecs.blobInfo().compose(WriteObjectResponse::getResource);
     this.defaultProjectId = UnifiedOpts.projectId(options.getProjectId());
   }
 
@@ -278,15 +287,21 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   @Override
   public Blob createFrom(BlobInfo blobInfo, Path path, int bufferSize, BlobWriteOption... options)
       throws IOException {
+    Opts<ObjectTargetOpt> opts = Opts.unwrap(options).resolveFrom(blobInfo).prepend(defaultOpts);
+    return internalCreateFrom(path, blobInfo, opts);
+  }
+
+  @Override
+  public Blob internalCreateFrom(Path path, BlobInfo info, Opts<ObjectTargetOpt> opts)
+      throws IOException {
     requireNonNull(path, "path must be non null");
     if (Files.isDirectory(path)) {
       throw new StorageException(0, path + " is a directory");
     }
 
-    Opts<ObjectTargetOpt> opts = Opts.unwrap(options).resolveFrom(blobInfo).prepend(defaultOpts);
     GrpcCallContext grpcCallContext =
         opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
-    WriteObjectRequest req = getWriteObjectRequest(blobInfo, opts);
+    WriteObjectRequest req = getWriteObjectRequest(info, opts);
 
     ClientStreamingCallable<WriteObjectRequest, WriteObjectResponse> write =
         storageClient.writeObjectCallable().withDefaultCallContext(grpcCallContext);
@@ -714,9 +729,14 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   @Override
   public GrpcBlobWriteChannel writer(BlobInfo blobInfo, BlobWriteOption... options) {
     Opts<ObjectTargetOpt> opts = Opts.unwrap(options).resolveFrom(blobInfo).prepend(defaultOpts);
+    return internalWriter(blobInfo, opts);
+  }
+
+  @Override
+  public GrpcBlobWriteChannel internalWriter(BlobInfo info, Opts<ObjectTargetOpt> opts) {
     GrpcCallContext grpcCallContext =
         opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
-    WriteObjectRequest req = getWriteObjectRequest(blobInfo, opts);
+    WriteObjectRequest req = getWriteObjectRequest(info, opts);
     Hasher hasher = Hasher.noop();
     return new GrpcBlobWriteChannel(
         storageClient.writeObjectCallable(),
@@ -1483,6 +1503,15 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
             Decoder.identity()));
   }
 
+  @BetaApi
+  @Override
+  public BlobWriteSession blobWriteSession(BlobInfo info, BlobWriteOption... options) {
+    Opts<ObjectTargetOpt> opts = Opts.unwrap(options).resolveFrom(info);
+    WritableByteChannelSession<?, BlobInfo> writableByteChannelSession =
+        writerFactory.writeSession(this, info, opts, writeObjectResponseBlobInfoDecoder);
+    return BlobWriteSessions.of(writableByteChannelSession);
+  }
+
   @Override
   public GrpcStorageOptions getOptions() {
     return (GrpcStorageOptions) super.getOptions();
@@ -1720,25 +1749,6 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     return StreamSupport.stream(spliterator, false);
   }
 
-  static <T> T throwHttpJsonOnly(String methodName) {
-    return throwHttpJsonOnly(Storage.class, methodName);
-  }
-
-  static <T> T throwHttpJsonOnly(Class<?> clazz, String methodName) {
-    String message =
-        String.format(
-            "%s#%s is only supported for HTTP_JSON transport. Please use StorageOptions.http() to construct a compatible instance.",
-            clazz.getName(), methodName);
-    throw new UnsupportedOperationException(message);
-  }
-
-  private static String fmtMethodName(String name, Class<?>... args) {
-    return name
-        + "("
-        + Arrays.stream(args).map(Class::getName).collect(Collectors.joining(", "))
-        + ")";
-  }
-
   ReadObjectRequest getReadObjectRequest(BlobId blob, Opts<ObjectSourceOpt> opts) {
     Object object = codecs.blobId().encode(blob);
 
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 d623745a..8bb3115c 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
@@ -42,6 +42,7 @@ import com.google.cloud.TransportOptions;
 import com.google.cloud.Tuple;
 import com.google.cloud.grpc.GrpcTransportOptions;
 import com.google.cloud.spi.ServiceRpcFactory;
+import com.google.cloud.storage.Storage.BlobWriteOption;
 import com.google.cloud.storage.TransportCompatibility.Transport;
 import com.google.cloud.storage.UnifiedOpts.Opts;
 import com.google.cloud.storage.UnifiedOpts.UserProject;
@@ -58,6 +59,7 @@ import io.grpc.ManagedChannelBuilder;
 import java.io.IOException;
 import java.io.Serializable;
 import java.net.URI;
+import java.time.Clock;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -82,6 +84,7 @@ public final class GrpcStorageOptions extends StorageOptions
   private final Duration terminationAwaitDuration;
   private final boolean attemptDirectPath;
   private final GrpcInterceptorProvider grpcInterceptorProvider;
+  private final BlobWriteSessionConfig blobWriteSessionConfig;
 
   private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) {
     super(builder, serviceDefaults);
@@ -94,6 +97,7 @@ public final class GrpcStorageOptions extends StorageOptions
             builder.terminationAwaitDuration, serviceDefaults.getTerminationAwaitDuration());
     this.attemptDirectPath = builder.attemptDirectPath;
     this.grpcInterceptorProvider = builder.grpcInterceptorProvider;
+    this.blobWriteSessionConfig = builder.blobWriteSessionConfig;
   }
 
   @Override
@@ -346,6 +350,8 @@ public final class GrpcStorageOptions extends StorageOptions
     private boolean attemptDirectPath = GrpcStorageDefaults.INSTANCE.isAttemptDirectPath();
     private GrpcInterceptorProvider grpcInterceptorProvider =
         GrpcStorageDefaults.INSTANCE.grpcInterceptorProvider();
+    private BlobWriteSessionConfig blobWriteSessionConfig =
+        GrpcStorageDefaults.INSTANCE.getDefaultStorageWriterConfig();
 
     Builder() {}
 
@@ -506,6 +512,21 @@ public final class GrpcStorageOptions extends StorageOptions
       return this;
     }
 
+    /**
+     * @see BlobWriteSessionConfig
+     * @see BlobWriteSessionConfigs
+     * @see Storage#blobWriteSession(BlobInfo, BlobWriteOption...)
+     * @see GrpcStorageDefaults#getDefaultStorageWriterConfig()
+     * @since 2.26.0 This new api is in preview and is subject to breaking changes.
+     */
+    @BetaApi
+    public GrpcStorageOptions.Builder setBlobWriteSessionConfig(
+        @NonNull BlobWriteSessionConfig blobWriteSessionConfig) {
+      requireNonNull(blobWriteSessionConfig, "blobWriteSessionConfig must be non null");
+      this.blobWriteSessionConfig = blobWriteSessionConfig;
+      return this;
+    }
+
     /** @since 2.14.0 This new api is in preview and is subject to breaking changes. */
     @BetaApi
     @Override
@@ -569,6 +590,12 @@ public final class GrpcStorageOptions extends StorageOptions
     public GrpcInterceptorProvider grpcInterceptorProvider() {
       return INTERCEPTOR_PROVIDER;
     }
+
+    /** @since 2.26.0 This new api is in preview and is subject to breaking changes. */
+    @BetaApi
+    public BlobWriteSessionConfig getDefaultStorageWriterConfig() {
+      return BlobWriteSessionConfigs.getDefault();
+    }
   }
 
   /**
@@ -618,7 +645,10 @@ public final class GrpcStorageOptions extends StorageOptions
           StorageSettings storageSettings = t.x();
           Opts<UserProject> defaultOpts = t.y();
           return new GrpcStorageImpl(
-              grpcStorageOptions, StorageClient.create(storageSettings), defaultOpts);
+              grpcStorageOptions,
+              StorageClient.create(storageSettings),
+              grpcStorageOptions.blobWriteSessionConfig.createFactory(Clock.systemUTC()),
+              defaultOpts);
         } catch (IOException e) {
           throw new IllegalStateException(
               "Unable to instantiate gRPC com.google.cloud.storage.Storage client.", e);
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 daab166f..03729577 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
@@ -16,6 +16,8 @@
 
 package com.google.cloud.storage;
 
+import static com.google.cloud.storage.CrossTransportUtils.fmtMethodName;
+import static com.google.cloud.storage.CrossTransportUtils.throwGrpcOnly;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static java.util.Objects.requireNonNull;
@@ -4615,4 +4617,52 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
    */
   @Override
   default void close() throws Exception {}
+
+  /**
+   * Create a new {@link BlobWriteSession} for the specified {@code blobInfo} and {@code options}.
+   *
+   * <p>The returned {@code BlobWriteSession} can be used to write an individual version, a new
+   * session must be created each time you want to create a new version.
+   *
+   * <p>By default, any MD5 value in the provided {@code blobInfo} is ignored unless the option
+   * {@link BlobWriteOption#md5Match()} is included in {@code options}.
+   *
+   * <p>By default, any CRC32c value in the provided {@code blobInfo} is ignored unless the option
+   * {@link BlobWriteOption#crc32cMatch()} is included in {@code options}.
+   *
+   * <h4>Example of creating an object using {@code BlobWriteSession}:</h4>
+   *
+   * <pre>{@code
+   * String bucketName = "my-unique-bucket";
+   * String blobName = "my-blob-name";
+   * BlobId blobId = BlobId.of(bucketName, blobName);
+   * BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build();
+   * ReadableByteChannel readableByteChannel = ...;
+   * BlobWriteSession blobWriteSession = storage.blobWriteSession(blobInfo, BlobWriteOption.doesNotExist());
+   *
+   * // open the channel for writing
+   * try (WritableByteChannel writableByteChannel = blobWriteSession.open()) {
+   *   // copy all bytes
+   *   ByteStreams.copy(readableByteChannel, writableByteChannel);
+   * } catch (IOException e) {
+   *   // handle IOException
+   * }
+   *
+   * // get the resulting object metadata
+   * ApiFuture<BlobInfo> resultFuture = blobWriteSession.getResult();
+   * BlobInfo gen1 = resultFuture.get();
+   * }</pre>
+   *
+   * @param blobInfo blob to create
+   * @param options blob write options
+   * @since 2.26.0 This new api is in preview and is subject to breaking changes.
+   * @see BlobWriteSessionConfig
+   * @see BlobWriteSessionConfigs
+   * @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig)
+   */
+  @BetaApi
+  @TransportCompatibility({Transport.GRPC})
+  default BlobWriteSession blobWriteSession(BlobInfo blobInfo, BlobWriteOption... options) {
+    return throwGrpcOnly(fmtMethodName("blobWriteSession", BlobInfo.class, BlobWriteOption.class));
+  }
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageInternal.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageInternal.java
new file mode 100644
index 00000000..deb8a050
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageInternal.java
@@ -0,0 +1,29 @@
+/*
+ * 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.UnifiedOpts.ObjectTargetOpt;
+import com.google.cloud.storage.UnifiedOpts.Opts;
+import java.io.IOException;
+import java.nio.file.Path;
+
+interface StorageInternal extends Storage {
+
+  Blob internalCreateFrom(Path path, BlobInfo info, Opts<ObjectTargetOpt> opts) throws IOException;
+
+  StorageWriteChannel internalWriter(BlobInfo info, Opts<ObjectTargetOpt> opts);
+}
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 f51bc05d..f794c29b 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
@@ -1083,7 +1083,7 @@ final class UnifiedOpts {
 
     @Override
     public Mapper<ListObjectsRequest.Builder> listObjects() {
-      return GrpcStorageImpl.throwHttpJsonOnly(
+      return CrossTransportUtils.throwHttpJsonOnly(
           com.google.cloud.storage.Storage.BlobListOption.class, "matchGlob(String)");
     }
   }
diff --git a/grpc-google-cloud-storage-v2/pom.xml b/grpc-google-cloud-storage-v2/pom.xml
index 825c606a..a55f3c23 100644
--- a/grpc-google-cloud-storage-v2/pom.xml
+++ b/grpc-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>grpc-google-cloud-storage-v2</artifactId>
-  <version>2.25.0-alpha</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+  <version>2.25.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
   <name>grpc-google-cloud-storage-v2</name>
   <description>GRPC library for grpc-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.25.0</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.25.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/pom.xml b/pom.xml
index 83717919..787a218a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage-parent</artifactId>
   <packaging>pom</packaging>
-  <version>2.25.0</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.25.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <name>Storage Parent</name>
   <url>https://github.com/googleapis/java-storage</url>
   <description>
@@ -83,7 +83,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage</artifactId>
-        <version>2.25.0</version><!-- {x-version-update:google-cloud-storage:current} -->
+        <version>2.25.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.apis</groupId>
@@ -124,17 +124,17 @@
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>proto-google-cloud-storage-v2</artifactId>
-        <version>2.25.0-alpha</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+        <version>2.25.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>grpc-google-cloud-storage-v2</artifactId>
-        <version>2.25.0-alpha</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.25.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>gapic-google-cloud-storage-v2</artifactId>
-        <version>2.25.0-alpha</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.25.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
diff --git a/proto-google-cloud-storage-v2/pom.xml b/proto-google-cloud-storage-v2/pom.xml
index 19bbbf8f..0a67a9eb 100644
--- a/proto-google-cloud-storage-v2/pom.xml
+++ b/proto-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>proto-google-cloud-storage-v2</artifactId>
-  <version>2.25.0-alpha</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+  <version>2.25.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
   <name>proto-google-cloud-storage-v2</name>
   <description>PROTO library for proto-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.25.0</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.25.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index 7b16d416..f3d8b890 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -30,7 +30,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.24.0</version>
+      <version>2.25.0</version>
     </dependency>
     <!-- [END storage_install_without_bom] -->
 
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 26760883..89306564 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -28,7 +28,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.25.0</version><!-- {x-version-update:google-cloud-storage:current} -->
+      <version>2.25.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
     </dependency>
 
     <dependency>
diff --git a/versions.txt b/versions.txt
index 03bad262..3aafb456 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,7 +1,7 @@
 # Format:
 # module:released-version:current-version
 
-google-cloud-storage:2.25.0:2.25.0
-gapic-google-cloud-storage-v2:2.25.0-alpha:2.25.0-alpha
-grpc-google-cloud-storage-v2:2.25.0-alpha:2.25.0-alpha
-proto-google-cloud-storage-v2:2.25.0-alpha:2.25.0-alpha
+google-cloud-storage:2.25.0:2.25.1-SNAPSHOT
+gapic-google-cloud-storage-v2:2.25.0-alpha:2.25.1-alpha-SNAPSHOT
+grpc-google-cloud-storage-v2:2.25.0-alpha:2.25.1-alpha-SNAPSHOT
+proto-google-cloud-storage-v2:2.25.0-alpha:2.25.1-alpha-SNAPSHOT


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/TransportCompatibilityTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/TransportCompatibilityTest.java
index d22b1829..76219bec 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/TransportCompatibilityTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/TransportCompatibilityTest.java
@@ -38,7 +38,7 @@ public final class TransportCompatibilityTest {
             .setCredentials(NoCredentials.getInstance())
             .build();
     @SuppressWarnings("resource")
-    Storage s = new GrpcStorageImpl(options, null, Opts.empty());
+    Storage s = new GrpcStorageImpl(options, null, null, Opts.empty());
     ImmutableList<String> messages =
         Stream.<Supplier<?>>of(
                 s::batch,
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java
new file mode 100644
index 00000000..1bb24fb9
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.it;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.cloud.storage.BlobInfo;
+import com.google.cloud.storage.BlobWriteSession;
+import com.google.cloud.storage.BlobWriteSessionConfigs;
+import com.google.cloud.storage.BucketInfo;
+import com.google.cloud.storage.DataGenerator;
+import com.google.cloud.storage.GrpcStorageOptions;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.Storage.BlobWriteOption;
+import com.google.cloud.storage.StorageException;
+import com.google.cloud.storage.TransportCompatibility.Transport;
+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.Inject;
+import com.google.cloud.storage.it.runner.annotations.SingleBackend;
+import com.google.cloud.storage.it.runner.annotations.StorageFixture;
+import com.google.cloud.storage.it.runner.registry.Generator;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.WritableByteChannel;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(StorageITRunner.class)
+@SingleBackend(Backend.PROD)
+public final class ITBlobWriteSessionTest {
+
+  @Inject
+  @StorageFixture(Transport.GRPC)
+  public Storage storage;
+
+  @Inject public BucketInfo bucket;
+
+  @Inject public Generator generator;
+
+  @Test
+  public void allDefaults() throws Exception {
+    doTest(storage);
+  }
+
+  @Test
+  public void overrideDefaultBufferSize() throws Exception {
+    GrpcStorageOptions options =
+        ((GrpcStorageOptions) storage.getOptions())
+            .toBuilder()
+            .setBlobWriteSessionConfig(
+                BlobWriteSessionConfigs.getDefault().withChunkSize(256 * 1024))
+            .build();
+    try (Storage s = options.getService()) {
+      doTest(s);
+    }
+  }
+
+  @Test
+  public void closingAnOpenedSessionWithoutCallingWriteShouldMakeAnEmptyObject()
+      throws IOException, ExecutionException, InterruptedException, TimeoutException {
+    BlobInfo info = BlobInfo.newBuilder(bucket, generator.randomObjectName()).build();
+    BlobWriteSession session = storage.blobWriteSession(info, BlobWriteOption.doesNotExist());
+
+    WritableByteChannel open = session.open();
+    open.close();
+    BlobInfo gen1 = session.getResult().get(1, TimeUnit.SECONDS);
+    System.out.println("gen1 = " + gen1);
+
+    assertThat(gen1.getSize()).isEqualTo(0);
+  }
+
+  @Test
+  public void attemptingToOpenASessionWhichResultsInFailureShouldThrowAStorageException() {
+    // attempt to write to a bucket which we have not created
+    String badBucketName = bucket.getName() + "x";
+    BlobInfo info = BlobInfo.newBuilder(badBucketName, generator.randomObjectName()).build();
+
+    BlobWriteSession session = storage.blobWriteSession(info, BlobWriteOption.doesNotExist());
+    StorageException se = assertThrows(StorageException.class, () -> session.open().close());
+
+    assertThat(se.getCode()).isEqualTo(404);
+    assertThat(se).hasMessageThat().contains(badBucketName);
+  }
+
+  private void doTest(Storage underTest) throws Exception {
+    BlobWriteSession sess =
+        underTest.blobWriteSession(
+            BlobInfo.newBuilder(bucket, generator.randomObjectName()).build(),
+            BlobWriteOption.doesNotExist());
+
+    byte[] bytes = DataGenerator.base64Characters().genBytes(512 * 1024);
+    try (WritableByteChannel w = sess.open()) {
+      w.write(ByteBuffer.wrap(bytes));
+    }
+
+    BlobInfo gen1 = sess.getResult().get(10, TimeUnit.SECONDS);
+
+    byte[] allBytes = storage.readAllBytes(gen1.getBlobId());
+
+    assertThat(allBytes).isEqualTo(bytes);
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java
index 11440cb1..d264e5a6 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java
@@ -25,6 +25,7 @@ import com.google.cloud.storage.Acl.Entity;
 import com.google.cloud.storage.Blob;
 import com.google.cloud.storage.BlobId;
 import com.google.cloud.storage.BlobInfo;
+import com.google.cloud.storage.BlobWriteSession;
 import com.google.cloud.storage.Bucket;
 import com.google.cloud.storage.BucketInfo;
 import com.google.cloud.storage.CopyWriter;
@@ -478,6 +479,11 @@ abstract class AbstractStorageProxy implements Storage {
     return delegate.deleteNotification(bucket, notificationId);
   }
 
+  @Override
+  public BlobWriteSession blobWriteSession(BlobInfo blobInfo, BlobWriteOption... options) {
+    return delegate.blobWriteSession(blobInfo, options);
+  }
+
   @Override
   public void close() throws Exception {
     delegate.close();