Repository: googleapis/java-storage (java)

Issue:
Add HNS folders samples
Add following samples for HNS folders:

* Sample for creating bucket with HNS configuration, region tag : `storage_create_bucket_hierarchical_namespace`.
* Sample for create, get, update, list, rename and delete folder operations in storage-control with following region tags:
  * `storage_control_create_folder`
  * `storage_control_delete_folder`
  * `storage_control_get_folder`
  * `storage_control_list_folders`
  * `storage_control_rename_folder`

Please use [PR#26020](https://github.com/googleapis/google-cloud-ruby/pull/26020) and [PR#26021](https://github.com/googleapis/google-cloud-ruby/pull/26021) for reference.

Pull request description:
docs: add Hierarchical Namespace Bucket and Folders samples
New samples
* `storage_create_bucket_hierarchical_namespace`
* `storage_control_create_folder`
* `storage_control_delete_folder`
* `storage_control_get_folder`
* `storage_control_list_folders`
* `storage_control_rename_folder`

Fixes #2569



Patch:
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedDirectWritableByteChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedDirectWritableByteChannel.java
index 861e867d..5e67440a 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedDirectWritableByteChannel.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedDirectWritableByteChannel.java
@@ -20,12 +20,14 @@ import static com.google.cloud.storage.GrpcUtils.contextWithBucketName;
 
 import com.google.api.core.SettableApiFuture;
 import com.google.api.gax.grpc.GrpcCallContext;
+import com.google.api.gax.rpc.ApiException;
 import com.google.api.gax.rpc.ApiStreamObserver;
 import com.google.api.gax.rpc.ClientStreamingCallable;
 import com.google.cloud.storage.ChunkSegmenter.ChunkSegment;
 import com.google.cloud.storage.Crc32cValue.Crc32cLengthKnown;
 import com.google.cloud.storage.UnbufferedWritableByteChannelSession.UnbufferedWritableByteChannel;
 import com.google.cloud.storage.WriteCtx.SimpleWriteObjectRequestBuilderFactory;
+import com.google.common.collect.ImmutableList;
 import com.google.protobuf.ByteString;
 import com.google.storage.v2.ChecksummedData;
 import com.google.storage.v2.ObjectChecksums;
@@ -34,11 +36,7 @@ import com.google.storage.v2.WriteObjectResponse;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.channels.ClosedChannelException;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.concurrent.ExecutionException;
-import java.util.function.Consumer;
-import java.util.function.LongConsumer;
 import org.checkerframework.checker.nullness.qual.NonNull;
 
 final class GapicUnbufferedDirectWritableByteChannel implements UnbufferedWritableByteChannel {
@@ -55,13 +53,14 @@ final class GapicUnbufferedDirectWritableByteChannel implements UnbufferedWritab
   private boolean open = true;
   private boolean first = true;
   private boolean finished = false;
+  private volatile WriteObjectRequest lastWrittenRequest;
 
   GapicUnbufferedDirectWritableByteChannel(
       SettableApiFuture<WriteObjectResponse> resultFuture,
       ChunkSegmenter chunkSegmenter,
       ClientStreamingCallable<WriteObjectRequest, WriteObjectResponse> write,
-      SimpleWriteObjectRequestBuilderFactory requestFactory) {
-    String bucketName = requestFactory.bucketName();
+      WriteCtx<SimpleWriteObjectRequestBuilderFactory> writeCtx) {
+    String bucketName = writeCtx.getRequestFactory().bucketName();
     this.resultFuture = resultFuture;
     this.chunkSegmenter = chunkSegmenter;
 
@@ -69,20 +68,63 @@ final class GapicUnbufferedDirectWritableByteChannel implements UnbufferedWritab
         contextWithBucketName(bucketName, GrpcCallContext.createDefault());
     this.write = write.withDefaultCallContext(internalContext);
 
-    this.writeCtx = new WriteCtx<>(requestFactory);
-    this.responseObserver = new Observer(writeCtx.getConfirmedBytes()::set, resultFuture::set);
+    this.writeCtx = writeCtx;
+    this.responseObserver = new Observer(internalContext);
   }
 
   @Override
   public long write(ByteBuffer[] srcs, int srcsOffset, int srcsLength) throws IOException {
-    return internalWrite(srcs, srcsOffset, srcsLength, false);
-  }
+    if (!open) {
+      throw new ClosedChannelException();
+    }
 
-  @Override
-  public long writeAndClose(ByteBuffer[] srcs, int srcsOffset, int srcsLength) throws IOException {
-    long write = internalWrite(srcs, srcsOffset, srcsLength, true);
-    close();
-    return write;
+    ChunkSegment[] data = chunkSegmenter.segmentBuffers(srcs, srcsOffset, srcsLength);
+    if (data.length == 0) {
+      return 0;
+    }
+
+    try {
+      ApiStreamObserver<WriteObjectRequest> openedStream = openedStream();
+      int bytesConsumed = 0;
+      for (ChunkSegment datum : data) {
+        Crc32cLengthKnown crc32c = datum.getCrc32c();
+        ByteString b = datum.getB();
+        int contentSize = b.size();
+        long offset = writeCtx.getTotalSentBytes().getAndAdd(contentSize);
+        Crc32cLengthKnown cumulative =
+            writeCtx
+                .getCumulativeCrc32c()
+                .accumulateAndGet(crc32c, chunkSegmenter.getHasher()::nullSafeConcat);
+        ChecksummedData.Builder checksummedData = ChecksummedData.newBuilder().setContent(b);
+        if (crc32c != null) {
+          checksummedData.setCrc32C(crc32c.getValue());
+        }
+        WriteObjectRequest.Builder builder = writeCtx.newRequestBuilder();
+        if (!first) {
+          builder.clearWriteObjectSpec();
+          builder.clearObjectChecksums();
+        }
+        builder.setWriteOffset(offset).setChecksummedData(checksummedData.build());
+        if (!datum.isOnlyFullBlocks()) {
+          builder.setFinishWrite(true);
+          if (cumulative != null) {
+            builder.setObjectChecksums(
+                ObjectChecksums.newBuilder().setCrc32C(cumulative.getValue()).build());
+          }
+          finished = true;
+        }
+
+        WriteObjectRequest build = builder.build();
+        first = false;
+        bytesConsumed += contentSize;
+        lastWrittenRequest = build;
+        openedStream.onNext(build);
+      }
+      return bytesConsumed;
+    } catch (RuntimeException e) {
+      resultFuture.setException(e);
+      throw e;
+    }
   }
 
   @Override
@@ -95,6 +137,7 @@ final class GapicUnbufferedDirectWritableByteChannel implements UnbufferedWritab
     ApiStreamObserver<WriteObjectRequest> openedStream = openedStream();
     if (!finished) {
       WriteObjectRequest message = finishMessage();
+      lastWrittenRequest = message;
       try {
         openedStream.onNext(message);
         openedStream.onCompleted();
@@ -115,79 +158,22 @@ final class GapicUnbufferedDirectWritableByteChannel implements UnbufferedWritab
     responseObserver.await();
   }
 
-  private long internalWrite(ByteBuffer[] srcs, int srcsOffset, int srcsLength, boolean finalize)
-      throws ClosedChannelException {
-    if (!open) {
-      throw new ClosedChannelException();
-    }
-
-    ChunkSegment[] data = chunkSegmenter.segmentBuffers(srcs, srcsOffset, srcsLength);
-
-    List<WriteObjectRequest> messages = new ArrayList<>();
-
-    ApiStreamObserver<WriteObjectRequest> openedStream = openedStream();
-    int bytesConsumed = 0;
-    for (ChunkSegment datum : data) {
-      Crc32cLengthKnown crc32c = datum.getCrc32c();
-      ByteString b = datum.getB();
-      int contentSize = b.size();
-      long offset = writeCtx.getTotalSentBytes().getAndAdd(contentSize);
-      Crc32cLengthKnown cumulative =
-          writeCtx
-              .getCumulativeCrc32c()
-              .accumulateAndGet(crc32c, chunkSegmenter.getHasher()::nullSafeConcat);
-      ChecksummedData.Builder checksummedData = ChecksummedData.newBuilder().setContent(b);
-      if (crc32c != null) {
-        checksummedData.setCrc32C(crc32c.getValue());
-      }
-      WriteObjectRequest.Builder builder =
-          writeCtx
-              .newRequestBuilder()
-              .setWriteOffset(offset)
-              .setChecksummedData(checksummedData.build());
-      if (!datum.isOnlyFullBlocks()) {
-        builder.setFinishWrite(true);
-        if (cumulative != null) {
-          builder.setObjectChecksums(
-              ObjectChecksums.newBuilder().setCrc32C(cumulative.getValue()).build());
-        }
-        finished = true;
-      }
-
-      WriteObjectRequest build = possiblyPairDownRequest(builder, first).build();
-      first = false;
-      messages.add(build);
-      bytesConsumed += contentSize;
-    }
-    if (finalize && !finished) {
-      messages.add(finishMessage());
-      finished = true;
-    }
-
-    try {
-      for (WriteObjectRequest message : messages) {
-        openedStream.onNext(message);
-      }
-    } catch (RuntimeException e) {
-      resultFuture.setException(e);
-      throw e;
-    }
-
-    return bytesConsumed;
-  }
-
   @NonNull
   private WriteObjectRequest finishMessage() {
     long offset = writeCtx.getTotalSentBytes().get();
     Crc32cLengthKnown crc32cValue = writeCtx.getCumulativeCrc32c().get();
 
-    WriteObjectRequest.Builder b =
-        writeCtx.newRequestBuilder().setFinishWrite(true).setWriteOffset(offset);
+    WriteObjectRequest.Builder b = writeCtx.newRequestBuilder();
+    if (!first) {
+      b.clearWriteObjectSpec();
+      b.clearObjectChecksums();
+      first = false;
+    }
+    b.setFinishWrite(true).setWriteOffset(offset);
     if (crc32cValue != null) {
       b.setObjectChecksums(ObjectChecksums.newBuilder().setCrc32C(crc32cValue.getValue()).build());
     }
-    WriteObjectRequest message = possiblyPairDownRequest(b, first).build();
-    return message;
+    return b.build();
   }
 
   private ApiStreamObserver<WriteObjectRequest> openedStream() {
@@ -201,48 +187,20 @@ final class GapicUnbufferedDirectWritableByteChannel implements UnbufferedWritab
     return stream;
   }
 
-  /**
-   * Several fields of a WriteObjectRequest are only allowed on the "first" message sent to gcs,
-   * this utility method centralizes the logic necessary to clear those fields for use by subsequent
-   * messages.
-   */
-  private static WriteObjectRequest.Builder possiblyPairDownRequest(
-      WriteObjectRequest.Builder b, boolean firstMessageOfStream) {
-    if (firstMessageOfStream && b.getWriteOffset() == 0) {
-      return b;
-    }
-    if (b.getWriteOffset() > 0) {
-      b.clearWriteObjectSpec();
-    }
-
-    if (b.getWriteOffset() > 0 && !b.getFinishWrite()) {
-      b.clearObjectChecksums();
-    }
-    return b;
-  }
-
-  static class Observer implements ApiStreamObserver<WriteObjectResponse> {
+  class Observer implements ApiStreamObserver<WriteObjectResponse> {
 
-    private final LongConsumer sizeCallback;
-    private final Consumer<WriteObjectResponse> completeCallback;
+    private final GrpcCallContext context;
 
     private final SettableApiFuture<Void> invocationHandle;
     private volatile WriteObjectResponse last;
 
-    Observer(LongConsumer sizeCallback, Consumer<WriteObjectResponse> completeCallback) {
-      this.sizeCallback = sizeCallback;
-      this.completeCallback = completeCallback;
+    Observer(GrpcCallContext context) {
+      this.context = context;
       this.invocationHandle = SettableApiFuture.create();
     }
 
     @Override
     public void onNext(WriteObjectResponse value) {
-      // incremental update
-      if (value.hasPersistedSize()) {
-        sizeCallback.accept(value.getPersistedSize());
-      } else if (value.hasResource()) {
-        sizeCallback.accept(value.getResource().getSize());
-      }
       last = value;
     }
 
@@ -257,15 +215,58 @@ final class GapicUnbufferedDirectWritableByteChannel implements UnbufferedWritab
      */
     @Override
     public void onError(Throwable t) {
-      invocationHandle.setException(t);
+      if (t instanceof ApiException) {
+        // use StorageExceptions logic to translate from ApiException to our status codes ensuring
+        // things fall in line with our retry handlers.
+        // This is suboptimal, as it will initialize a second exception, however this is the
+        // unusual case, and it should not cause a significant overhead given its rarity.
+        StorageException tmp = StorageException.asStorageException((ApiException) t);
+        StorageException storageException =
+            ResumableSessionFailureScenario.toStorageException(
+                tmp.getCode(), tmp.getMessage(), tmp.getReason(), getRequests(), null, context, t);
+        invocationHandle.setException(storageException);
+      } else {
+        invocationHandle.setException(t);
+      }
     }
 
     @Override
     public void onCompleted() {
-      if (last != null && last.hasResource()) {
-        completeCallback.accept(last);
+      try {
+        if (last == null) {
+          throw new StorageException(
+              0, "onComplete without preceding onNext, unable to determine success.");
+        } else if (last.hasResource()) {
+          long totalSentBytes = writeCtx.getTotalSentBytes().get();
+          long finalSize = last.getResource().getSize();
+          if (totalSentBytes == finalSize) {
+            writeCtx.getConfirmedBytes().set(finalSize);
+            resultFuture.set(last);
+          } else if (finalSize < totalSentBytes) {
+            throw ResumableSessionFailureScenario.SCENARIO_4_1.toStorageException(
+                getRequests(), last, context, null);
+          } else {
+            throw ResumableSessionFailureScenario.SCENARIO_4_2.toStorageException(
+                getRequests(), last, context, null);
+          }
+        } else {
+          throw ResumableSessionFailureScenario.SCENARIO_0.toStorageException(
+              getRequests(), last, context, null);
+        }
+      } catch (Throwable se) {
+        open = false;
+        invocationHandle.setException(se);
+      } finally {
+        invocationHandle.set(null);
+      }
+    }
+
+    private @NonNull ImmutableList<@NonNull WriteObjectRequest> getRequests() {
+      if (lastWrittenRequest == null) {
+        return ImmutableList.of();
+      } else {
+        return ImmutableList.of(lastWrittenRequest);
       }
-      invocationHandle.set(null);
     }
 
     void await() {
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicWritableByteChannelSessionBuilder.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicWritableByteChannelSessionBuilder.java
index 88540533..f4a8afdb 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicWritableByteChannelSessionBuilder.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicWritableByteChannelSessionBuilder.java
@@ -185,7 +185,7 @@ final class GapicWritableByteChannelSessionBuilder {
                         resultFuture,
                         getChunkSegmenter(),
                         write,
-                        WriteObjectRequestBuilderFactory.simple(start)))
+                        new WriteCtx<>(WriteObjectRequestBuilderFactory.simple(start))))
                 .andThen(StorageByteChannels.writable()::createSynchronized));
       }
     }
@@ -213,7 +213,7 @@ final class GapicWritableByteChannelSessionBuilder {
                         resultFuture,
                         getChunkSegmenter(),
                         write,
-                        WriteObjectRequestBuilderFactory.simple(start)))
+                        new WriteCtx<>(WriteObjectRequestBuilderFactory.simple(start))))
                 .andThen(c -> new DefaultBufferedWritableByteChannel(bufferHandle, c))
                 .andThen(StorageByteChannels.writable()::createSynchronized));
       }
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 294d481c..8c06d70d 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
@@ -64,11 +64,11 @@ enum ResumableSessionFailureScenario {
   SCENARIO_4_1(
       BaseServiceException.UNKNOWN_CODE,
       "dataLoss",
-      "Finalized resumable session, but object size less than expected."),
+      "Finalized upload, but object size less than expected."),
   SCENARIO_4_2(
       BaseServiceException.UNKNOWN_CODE,
       "dataLoss",
-      "Finalized resumable session, but object size greater than expected."),
+      "Finalized upload, but object size greater than expected."),
   SCENARIO_5(
       BaseServiceException.UNKNOWN_CODE,
       "dataLoss",
diff --git a/samples/snippets/src/main/java/com/example/storage/control/v2/CreateFolder.java b/samples/snippets/src/main/java/com/example/storage/control/v2/CreateFolder.java
new file mode 100644
index 00000000..6bfc6609
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/storage/control/v2/CreateFolder.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2024 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.example.storage.control.v2;
+
+// [START storage_control_create_folder]
+import com.google.storage.control.v2.BucketName;
+import com.google.storage.control.v2.CreateFolderRequest;
+import com.google.storage.control.v2.Folder;
+import com.google.storage.control.v2.StorageControlClient;
+import java.io.IOException;
+
+public final class CreateFolder {
+
+  public static void createFolder(String bucketName, String folderName) throws IOException {
+    // The name of the bucket
+    // String bucketName = "your-unique-bucket-name";
+
+    // The name of the folder within the bucket
+    // String folderName = "your-unique-folder-name";
+
+    try (StorageControlClient storageControl = StorageControlClient.create()) {
+
+      CreateFolderRequest request =
+          CreateFolderRequest.newBuilder()
+              // Set project to "_" to signify globally scoped bucket
+              .setParent(BucketName.format("_", bucketName))
+              .setFolderId(folderName)
+              .build();
+
+      Folder newFolder = storageControl.createFolder(request);
+
+      System.out.printf("Created folder: %s%n", newFolder.getName());
+    }
+  }
+}
+// [END storage_control_create_folder]
diff --git a/samples/snippets/src/main/java/com/example/storage/control/v2/CreateHierarchicalNamespaceBucket.java b/samples/snippets/src/main/java/com/example/storage/control/v2/CreateHierarchicalNamespaceBucket.java
new file mode 100644
index 00000000..f6d0868c
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/storage/control/v2/CreateHierarchicalNamespaceBucket.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 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.example.storage.control.v2;
+
+// [START storage_create_bucket_hierarchical_namespace]
+import com.google.cloud.storage.Bucket;
+import com.google.cloud.storage.BucketInfo;
+import com.google.cloud.storage.BucketInfo.HierarchicalNamespace;
+import com.google.cloud.storage.BucketInfo.IamConfiguration;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageOptions;
+
+public final class CreateHierarchicalNamespaceBucket {
+
+  public static void createHierarchicalNamespaceBucket(String projectId, String bucketName)
+      throws Exception {
+    // The ID of your GCP project
+    // String projectId = "your-project-id";
+
+    // The ID to give your GCS bucket
+    // String bucketName = "your-unique-bucket-name";
+    StorageOptions storageOptions = StorageOptions.newBuilder().setProjectId(projectId).build();
+    try (Storage storage = storageOptions.getService()) {
+
+      BucketInfo bucketInfo =
+          BucketInfo.newBuilder(bucketName)
+              .setIamConfiguration(
+                  // Hierarchical namespace buckets must use uniform bucket-level access.
+                  IamConfiguration.newBuilder().setIsUniformBucketLevelAccessEnabled(true).build())
+              .setHierarchicalNamespace(HierarchicalNamespace.newBuilder().setEnabled(true).build())
+              .build();
+
+      Bucket bucket = storage.create(bucketInfo);
+
+      System.out.printf(
+          "Created bucket %s with Hierarchical Namespace enabled.%n", bucket.getName());
+    }
+  }
+}
+// [END storage_create_bucket_hierarchical_namespace]
diff --git a/samples/snippets/src/main/java/com/example/storage/control/v2/DeleteFolder.java b/samples/snippets/src/main/java/com/example/storage/control/v2/DeleteFolder.java
new file mode 100644
index 00000000..187a682f
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/storage/control/v2/DeleteFolder.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024 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.example.storage.control.v2;
+
+// [START storage_control_delete_folder]
+
+import com.google.storage.control.v2.DeleteFolderRequest;
+import com.google.storage.control.v2.FolderName;
+import com.google.storage.control.v2.StorageControlClient;
+import java.io.IOException;
+
+public final class DeleteFolder {
+
+  public static void deleteFolder(String bucketName, String folderName) throws IOException {
+    // The name of the bucket
+    // String bucketName = "your-unique-bucket-name";
+
+    // The name of the folder within the bucket
+    // String folderName = "your-unique-folder-name";
+
+    try (StorageControlClient storageControl = StorageControlClient.create()) {
+
+      // Set project to "_" to signify globally scoped bucket
+      String folderResourceName = FolderName.format("_", bucketName, folderName);
+      DeleteFolderRequest request =
+          DeleteFolderRequest.newBuilder().setName(folderResourceName).build();
+
+      storageControl.deleteFolder(request);
+
+      System.out.printf("Deleted folder: %s%n", folderResourceName);
+    }
+  }
+}
+// [END storage_control_delete_folder]
diff --git a/samples/snippets/src/main/java/com/example/storage/control/v2/GetFolder.java b/samples/snippets/src/main/java/com/example/storage/control/v2/GetFolder.java
new file mode 100644
index 00000000..454ed7b3
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/storage/control/v2/GetFolder.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2024 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.example.storage.control.v2;
+
+// [START storage_control_get_folder]
+
+import com.google.storage.control.v2.Folder;
+import com.google.storage.control.v2.FolderName;
+import com.google.storage.control.v2.GetFolderRequest;
+import com.google.storage.control.v2.StorageControlClient;
+import java.io.IOException;
+
+public final class GetFolder {
+
+  public static void getFolder(String bucketName, String folderName) throws IOException {
+    // The name of the bucket
+    // String bucketName = "your-unique-bucket-name";
+
+    // The name of the folder within the bucket
+    // String folderName = "your-unique-folder-name";
+
+    try (StorageControlClient storageControl = StorageControlClient.create()) {
+
+      GetFolderRequest request =
+          GetFolderRequest.newBuilder()
+              // Set project to "_" to signify globally scoped bucket
+              .setName(FolderName.format("_", bucketName, folderName))
+              .build();
+
+      Folder newFolder = storageControl.getFolder(request);
+
+      System.out.printf("Got folder: %s%n", newFolder.getName());
+    }
+  }
+}
+// [END storage_control_get_folder]
diff --git a/samples/snippets/src/main/java/com/example/storage/control/v2/ListFolders.java b/samples/snippets/src/main/java/com/example/storage/control/v2/ListFolders.java
new file mode 100644
index 00000000..2778213e
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/storage/control/v2/ListFolders.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024 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.example.storage.control.v2;
+
+// [START storage_control_list_folders]
+
+import com.google.storage.control.v2.BucketName;
+import com.google.storage.control.v2.Folder;
+import com.google.storage.control.v2.ListFoldersRequest;
+import com.google.storage.control.v2.StorageControlClient;
+import java.io.IOException;
+
+public final class ListFolders {
+
+  public static void listFolders(String bucketName) throws IOException {
+    // The name of the bucket
+    // String bucketName = "your-unique-bucket-name";
+
+    try (StorageControlClient storageControl = StorageControlClient.create()) {
+
+      ListFoldersRequest request =
+          ListFoldersRequest.newBuilder()
+              // Set project to "_" to signify globally scoped bucket
+              .setParent(BucketName.format("_", bucketName))
+              .build();
+
+      Iterable<Folder> folders = storageControl.listFolders(request).iterateAll();
+      for (Folder folder : folders) {
+        System.out.printf("Found folder: %s%n", folder.getName());
+      }
+    }
+  }
+}
+// [END storage_control_list_folders]
diff --git a/samples/snippets/src/main/java/com/example/storage/control/v2/RenameFolder.java b/samples/snippets/src/main/java/com/example/storage/control/v2/RenameFolder.java
new file mode 100644
index 00000000..e8f95b6f
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/storage/control/v2/RenameFolder.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 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.example.storage.control.v2;
+
+// [START storage_control_rename_folder]
+
+import com.google.api.gax.longrunning.OperationFuture;
+import com.google.storage.control.v2.Folder;
+import com.google.storage.control.v2.FolderName;
+import com.google.storage.control.v2.RenameFolderMetadata;
+import com.google.storage.control.v2.RenameFolderRequest;
+import com.google.storage.control.v2.StorageControlClient;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public final class RenameFolder {
+
+  public static void renameFolder(
+      String bucketName, String sourceFolderName, String destinationFolderName)
+      throws IOException, ExecutionException, InterruptedException, TimeoutException {
+    // The name of the bucket
+    // String bucketName = "your-unique-bucket-name";
+
+    // The name of the folder within the bucket
+    // String sourceFolderName = "your-unique-source-folder-name";
+
+    // The new name of the folder within the bucket
+    // String destinationFolderName = "your-unique-destination-folder-name";
+
+    try (StorageControlClient storageControl = StorageControlClient.create()) {
+
+      // Set project to "_" to signify globally scoped bucket
+      String sourceFolderResourceName = FolderName.format("_", bucketName, sourceFolderName);
+      RenameFolderRequest request =
+          RenameFolderRequest.newBuilder()
+              .setName(sourceFolderResourceName)
+              .setDestinationFolderId(destinationFolderName)
+              .build();
+
+      OperationFuture<Folder, RenameFolderMetadata> renameOperation =
+          storageControl.renameFolderAsync(request);
+
+      Folder destinationFolder = renameOperation.get(30, TimeUnit.SECONDS);
+
+      System.out.printf(
+          "Renamed folder from %s to %s%n", sourceFolderResourceName, destinationFolder.getName());
+    }
+  }
+}
+// [END storage_control_rename_folder]


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITGapicUnbufferedDirectWritableByteChannelTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITGapicUnbufferedDirectWritableByteChannelTest.java
new file mode 100644
index 00000000..4a70e9a2
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITGapicUnbufferedDirectWritableByteChannelTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.storage;
+
+import static com.google.cloud.storage.ByteSizeConstants._256KiB;
+import static com.google.cloud.storage.ByteSizeConstants._512KiB;
+import static com.google.cloud.storage.ByteSizeConstants._768KiB;
+import static com.google.cloud.storage.TestUtils.assertAll;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.api.core.SettableApiFuture;
+import com.google.cloud.storage.ITGapicUnbufferedWritableByteChannelTest.DirectWriteService;
+import com.google.cloud.storage.WriteCtx.SimpleWriteObjectRequestBuilderFactory;
+import com.google.cloud.storage.WriteCtx.WriteObjectRequestBuilderFactory;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.storage.v2.Object;
+import com.google.storage.v2.StorageClient;
+import com.google.storage.v2.WriteObjectRequest;
+import com.google.storage.v2.WriteObjectResponse;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+
+public final class ITGapicUnbufferedDirectWritableByteChannelTest {
+
+  private static final ChunkSegmenter CHUNK_SEGMENTER =
+      new ChunkSegmenter(Hasher.noop(), ByteStringStrategy.copy(), _256KiB, _256KiB);
+
+  /** Attempting to finalize, ack equals expected */
+  @Test
+  public void ack_eq() throws Exception {
+    WriteObjectRequest req1 =
+        WriteObjectRequest.newBuilder().setWriteOffset(_256KiB).setFinishWrite(true).build();
+    WriteObjectResponse resp1 =
+        WriteObjectResponse.newBuilder()
+            .setResource(Object.newBuilder().setName("name").setSize(_256KiB).build())
+            .build();
+
+    ImmutableMap<List<WriteObjectRequest>, WriteObjectResponse> map =
+        ImmutableMap.of(ImmutableList.of(req1), resp1);
+    DirectWriteService service1 = new DirectWriteService(map);
+
+    try (FakeServer fakeServer = FakeServer.of(service1);
+        GrpcStorageImpl storage =
+            (GrpcStorageImpl) fakeServer.getGrpcStorageOptions().getService()) {
+      StorageClient storageClient = storage.storageClient;
+
+      SettableApiFuture<WriteObjectResponse> done = SettableApiFuture.create();
+      WriteCtx<SimpleWriteObjectRequestBuilderFactory> writeCtx =
+          new WriteCtx<>(WriteObjectRequestBuilderFactory.simple(req1));
+      writeCtx.getTotalSentBytes().set(_256KiB);
+      writeCtx.getConfirmedBytes().set(0);
+
+      GapicUnbufferedDirectWritableByteChannel channel =
+          new GapicUnbufferedDirectWritableByteChannel(
+              done, CHUNK_SEGMENTER, storageClient.writeObjectCallable(), writeCtx);
+
+      channel.close();
+
+      WriteObjectResponse writeObjectResponse = done.get(2, TimeUnit.SECONDS);
+      assertAll(
+          () -> assertThat(writeObjectResponse).isEqualTo(resp1),
+          () -> assertThat(writeCtx.getConfirmedBytes().get()).isEqualTo(_256KiB),
+          () -> assertThat(channel.isOpen()).isFalse());
+    }
+  }
+
+  /** Attempting to finalize, ack &lt; expected */
+  @Test
+  public void ack_lt() throws Exception {
+    WriteObjectRequest req1 =
+        WriteObjectRequest.newBuilder().setWriteOffset(_512KiB).setFinishWrite(true).build();
+    WriteObjectResponse resp1 =
+        WriteObjectResponse.newBuilder()
+            .setResource(Object.newBuilder().setName("name").setSize(_256KiB).build())
+            .build();
+
+    ImmutableMap<List<WriteObjectRequest>, WriteObjectResponse> map =
+        ImmutableMap.of(ImmutableList.of(req1), resp1);
+    DirectWriteService service1 = new DirectWriteService(map);
+
+    try (FakeServer fakeServer = FakeServer.of(service1);
+        GrpcStorageImpl storage =
+            (GrpcStorageImpl) fakeServer.getGrpcStorageOptions().getService()) {
+      StorageClient storageClient = storage.storageClient;
+
+      SettableApiFuture<WriteObjectResponse> done = SettableApiFuture.create();
+      WriteCtx<SimpleWriteObjectRequestBuilderFactory> writeCtx =
+          new WriteCtx<>(WriteObjectRequestBuilderFactory.simple(req1));
+      writeCtx.getTotalSentBytes().set(_512KiB);
+      writeCtx.getConfirmedBytes().set(0);
+
+      //noinspection resource
+      GapicUnbufferedDirectWritableByteChannel channel =
+          new GapicUnbufferedDirectWritableByteChannel(
+              done, CHUNK_SEGMENTER, storageClient.writeObjectCallable(), writeCtx);
+
+      StorageException se = assertThrows(StorageException.class, channel::close);
+      assertAll(
+          () -> assertThat(se.getCode()).isEqualTo(0),
+          () -> assertThat(se.getReason()).isEqualTo("dataLoss"),
+          () -> assertThat(writeCtx.getConfirmedBytes().get()).isEqualTo(0),
+          () -> assertThat(channel.isOpen()).isFalse());
+    }
+  }
+
+  /** Attempting to finalize, ack &gt; expected */
+  @Test
+  public void ack_gt() throws Exception {
+    WriteObjectRequest req1 =
+        WriteObjectRequest.newBuilder().setWriteOffset(_512KiB).setFinishWrite(true).build();
+    WriteObjectResponse resp1 =
+        WriteObjectResponse.newBuilder()
+            .setResource(Object.newBuilder().setName("name").setSize(_768KiB).build())
+            .build();
+
+    ImmutableMap<List<WriteObjectRequest>, WriteObjectResponse> map =
+        ImmutableMap.of(ImmutableList.of(req1), resp1);
+    DirectWriteService service1 = new DirectWriteService(map);
+
+    try (FakeServer fakeServer = FakeServer.of(service1);
+        GrpcStorageImpl storage =
+            (GrpcStorageImpl) fakeServer.getGrpcStorageOptions().getService()) {
+      StorageClient storageClient = storage.storageClient;
+
+      SettableApiFuture<WriteObjectResponse> done = SettableApiFuture.create();
+      WriteCtx<SimpleWriteObjectRequestBuilderFactory> writeCtx =
+          new WriteCtx<>(WriteObjectRequestBuilderFactory.simple(req1));
+      writeCtx.getTotalSentBytes().set(_512KiB);
+      writeCtx.getConfirmedBytes().set(0);
+
+      //noinspection resource
+      GapicUnbufferedDirectWritableByteChannel channel =
+          new GapicUnbufferedDirectWritableByteChannel(
+              done, CHUNK_SEGMENTER, storageClient.writeObjectCallable(), writeCtx);
+
+      StorageException se = assertThrows(StorageException.class, channel::close);
+      assertAll(
+          () -> assertThat(se.getCode()).isEqualTo(0),
+          () -> assertThat(se.getReason()).isEqualTo("dataLoss"),
+          () -> assertThat(writeCtx.getConfirmedBytes().get()).isEqualTo(0),
+          () -> assertThat(channel.isOpen()).isFalse());
+    }
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITGapicUnbufferedWritableByteChannelTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITGapicUnbufferedWritableByteChannelTest.java
index 239b9bf3..326cbb15 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITGapicUnbufferedWritableByteChannelTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITGapicUnbufferedWritableByteChannelTest.java
@@ -153,11 +153,14 @@ public final class ITGapicUnbufferedWritableByteChannelTest {
         new DirectWriteService(
             ImmutableMap.of(ImmutableList.of(req1, req2, req3, req4, req5), resp));
     try (FakeServer fake = FakeServer.of(service);
-        StorageClient sc = StorageClient.create(fake.storageSettings())) {
+        StorageClient sc =
+            PackagePrivateMethodWorkarounds.maybeGetStorageClient(
+                fake.getGrpcStorageOptions().getService())) {
+      assertThat(sc).isNotNull();
       SettableApiFuture<WriteObjectResponse> result = SettableApiFuture.create();
       try (GapicUnbufferedDirectWritableByteChannel c =
           new GapicUnbufferedDirectWritableByteChannel(
-              result, segmenter, sc.writeObjectCallable(), reqFactory)) {
+              result, segmenter, sc.writeObjectCallable(), new WriteCtx<>(reqFactory))) {
         c.write(ByteBuffer.wrap(bytes));
       }
       assertThat(result.get()).isEqualTo(resp);
diff --git a/samples/snippets/src/test/java/com/example/storage/control/v2/FoldersTest.java b/samples/snippets/src/test/java/com/example/storage/control/v2/FoldersTest.java
new file mode 100644
index 00000000..7b4f8f21
--- /dev/null
+++ b/samples/snippets/src/test/java/com/example/storage/control/v2/FoldersTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2024 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.example.storage.control.v2;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.api.gax.rpc.NotFoundException;
+import com.google.cloud.storage.BucketInfo;
+import com.google.cloud.storage.BucketInfo.HierarchicalNamespace;
+import com.google.cloud.storage.BucketInfo.IamConfiguration;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageOptions;
+import com.google.cloud.storage.testing.RemoteStorageHelper;
+import com.google.cloud.testing.junit4.StdOutCaptureRule;
+import com.google.storage.control.v2.BucketName;
+import com.google.storage.control.v2.Folder;
+import com.google.storage.control.v2.FolderName;
+import com.google.storage.control.v2.StorageControlClient;
+import java.io.IOException;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public final class FoldersTest {
+
+  @Rule public StdOutCaptureRule stdOut = new StdOutCaptureRule();
+
+  private Storage storage;
+  private BucketInfo bucket;
+  private StorageControlClient storageControl;
+
+  @Before
+  public void setUp() throws Exception {
+    storage = StorageOptions.http().build().getService();
+    String bucketName = RemoteStorageHelper.generateBucketName();
+    BucketInfo bucketInfo =
+        BucketInfo.newBuilder(bucketName)
+            .setIamConfiguration(
+                // Hierarchical namespace buckets must use uniform bucket-level access.
+                IamConfiguration.newBuilder().setIsUniformBucketLevelAccessEnabled(true).build())
+            .setHierarchicalNamespace(HierarchicalNamespace.newBuilder().setEnabled(true).build())
+            .build();
+
+    bucket = storage.create(bucketInfo).asBucketInfo();
+    storageControl = StorageControlClient.create();
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    // Use try-with-resource to handle the dance closing multiple things
+    //noinspection EmptyTryBlock
+    try (AutoCloseable ignore1 = storage;
+        AutoCloseable ignore2 = storageControl) {
+      // ignore
+    }
+  }
+
+  @Test
+  public void createFolder() throws IOException {
+    String folderName = UUID.randomUUID().toString();
+    CreateFolder.createFolder(bucket.getName(), folderName);
+    try {
+      assertThat(stdOut.getCapturedOutputAsUtf8String()).contains(folderName);
+    } finally {
+      storageControl.deleteFolder(FolderName.of("_", bucket.getName(), folderName));
+    }
+  }
+
+  @Test
+  public void getFolder() throws IOException {
+    FolderName folderName = FolderName.of("_", bucket.getName(), UUID.randomUUID().toString());
+    Folder gen1 =
+        storageControl.createFolder(
+            BucketName.of("_", bucket.getName()),
+            Folder.getDefaultInstance(),
+            folderName.getFolder());
+
+    GetFolder.getFolder(bucket.getName(), folderName.getFolder());
+    try {
+      assertThat(stdOut.getCapturedOutputAsUtf8String()).contains(folderName.toString());
+    } finally {
+      storageControl.deleteFolder(folderName);
+    }
+  }
+
+  @Test
+  public void renameFolder()
+      throws IOException, ExecutionException, InterruptedException, TimeoutException {
+    FolderName srcFolderName = FolderName.of("_", bucket.getName(), UUID.randomUUID().toString());
+    Folder gen1 =
+        storageControl.createFolder(
+            BucketName.of("_", bucket.getName()),
+            Folder.getDefaultInstance(),
+            srcFolderName.getFolder());
+
+    FolderName dstFolderName = FolderName.of("_", bucket.getName(), UUID.randomUUID().toString());
+
+    RenameFolder.renameFolder(
+        bucket.getName(), srcFolderName.getFolder(), dstFolderName.getFolder());
+    try {
+      assertThat(stdOut.getCapturedOutputAsUtf8String()).contains(srcFolderName.toString());
+      assertThat(stdOut.getCapturedOutputAsUtf8String()).contains(dstFolderName.toString());
+      assertThrows(NotFoundException.class, () -> storageControl.getFolder(srcFolderName));
+    } finally {
+      storageControl.deleteFolder(dstFolderName);
+    }
+  }
+
+  @Test
+  public void deleteFolder() throws IOException {
+    FolderName folderName = FolderName.of("_", bucket.getName(), UUID.randomUUID().toString());
+    Folder gen1 =
+        storageControl.createFolder(
+            BucketName.of("_", bucket.getName()),
+            Folder.getDefaultInstance(),
+            folderName.getFolder());
+
+    DeleteFolder.deleteFolder(bucket.getName(), folderName.getFolder());
+    assertThat(stdOut.getCapturedOutputAsUtf8String()).contains(folderName.toString());
+    assertThrows(NotFoundException.class, () -> storageControl.getFolder(folderName));
+  }
+
+  @Test
+  public void listFolder() throws IOException {
+    FolderName folderName = FolderName.of("_", bucket.getName(), UUID.randomUUID().toString());
+    Folder gen1 =
+        storageControl.createFolder(
+            BucketName.of("_", bucket.getName()),
+            Folder.getDefaultInstance(),
+            folderName.getFolder());
+
+    ListFolders.listFolders(bucket.getName());
+    try {
+      assertThat(stdOut.getCapturedOutputAsUtf8String()).contains(folderName.toString());
+    } finally {
+      storageControl.deleteFolder(folderName);
+    }
+  }
+}
diff --git a/samples/snippets/src/test/java/com/example/storage/control/v2/HNSTest.java b/samples/snippets/src/test/java/com/example/storage/control/v2/HNSTest.java
new file mode 100644
index 00000000..5d2ec83c
--- /dev/null
+++ b/samples/snippets/src/test/java/com/example/storage/control/v2/HNSTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 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.example.storage.control.v2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.example.storage.TestBase;
+import com.google.cloud.storage.Bucket;
+import com.google.cloud.storage.testing.RemoteStorageHelper;
+import org.junit.Test;
+
+public final class HNSTest extends TestBase {
+
+  @Test
+  public void createHierarchicalNamespaceBucket() throws Exception {
+    String newBucketName = RemoteStorageHelper.generateBucketName();
+    String projectId = storage.getOptions().getProjectId();
+    CreateHierarchicalNamespaceBucket.createHierarchicalNamespaceBucket(projectId, newBucketName);
+    try {
+      Bucket remoteBucket = storage.get(newBucketName);
+      assertThat(remoteBucket).isNotNull();
+      assertThat(remoteBucket.getHierarchicalNamespace().getEnabled()).isTrue();
+      assertThat(stdOut.getCapturedOutputAsUtf8String()).contains(newBucketName);
+    } finally {
+      storage.delete(newBucketName);
+    }
+  }
+}