Repository: googleapis/java-storage
Pull request #2345 (2024-01-04 17:33:54)

feat: Add ability to create a PCU Prefix at the object level
This feature enabled the user to create a prefix that will be unique per parallel composite upload. If the user specifies a prefix the prefix will be appended to the ultimate object name which will be appended to the part name to create a staging area for objects per PCU.

If the user does not specify a prefix then the parts will simply be created at the level of the ultimate object name.



Patch:
diff --git a/README.md b/README.md
index d7ce7e14..852d56ad 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file:
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>libraries-bom</artifactId>
-      <version>26.29.0</version>
+      <version>26.30.0</version>
       <type>pom</type>
       <scope>import</scope>
     </dependency>
@@ -50,7 +50,7 @@ If you are using Maven without the BOM, add this to your dependencies:
 If you are using Gradle 5.x or later, add this to your dependencies:
 
 ```Groovy
-implementation platform('com.google.cloud:libraries-bom:26.29.0')
+implementation platform('com.google.cloud:libraries-bom:26.30.0')
 
 implementation 'com.google.cloud:google-cloud-storage'
 ```
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index 94ffe2cd..9fe96884 100644
--- a/google-cloud-storage/pom.xml
+++ b/google-cloud-storage/pom.xml
@@ -16,7 +16,7 @@
   </parent>
   <properties>
     <site.installationModule>google-cloud-storage</site.installationModule>
-    <pubsub-proto.version>1.107.13</pubsub-proto.version>
+    <pubsub-proto.version>1.108.0</pubsub-proto.version>
     <junit-platform.version>5.10.1</junit-platform.version>
   </properties>
   <dependencies>
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfig.java
index d0240669..b877d82d 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfig.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfig.java
@@ -462,10 +462,7 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
 
       // encode it to base 64, yielding 22 characters
       String randomKey = B64.encodeToString(bytes);
-      HashCode hashCode =
-          OBJECT_NAME_HASH_FUNCTION.hashString(ultimateObjectName, StandardCharsets.UTF_8);
-      String nameDigest = B64.encodeToString(hashCode.asBytes());
-      return fmtFields(randomKey, nameDigest, partRange.encode());
+      return fmtFields(randomKey, ultimateObjectName, partRange.encode());
     }
 
     abstract String fmtFields(String randomKey, String nameDigest, String partRange);
@@ -523,6 +520,35 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
       return new WithPrefix(rand, prefixPattern);
     }
 
+    /**
+     * Strategy in which the end object name is the prefix included and is present on each part and
+     * intermediary compose object.
+     *
+     * <p>General format is
+     *
+     * <pre><code>
+     *   {objectName}-{randomKeyDigest};{objectInfoDigest};{partIndex}.part
+     * </code></pre>
+     *
+     * <p>{@code {objectInfoDigest}} will be fixed for an individual {@link BlobWriteSession}.
+     *
+     * <p><b><i>NOTE:</i></b>The way in which both {@code randomKeyDigest} and {@code
+     * objectInfoDigest} are generated is undefined and subject to change at any time.
+     *
+     * @see #withPartNamingStrategy(PartNamingStrategy)
+     * @since 2.30.2 This new api is in preview and is subject to breaking changes.
+     */
+    @BetaApi
+    public static PartNamingStrategy useObjectNameAsPrefix() {
+      return useObjectNameAsPrefix("");
+    }
+
+    private static PartNamingStrategy useObjectNameAsPrefix(String prefixPattern) {
+      checkNotNull(prefixPattern, "prefixPattern must be non null");
+      SecureRandom rand = new SecureRandom();
+      return new WithObjectLevelPrefix(rand, prefixPattern);
+    }
+
     static final class WithPrefix extends PartNamingStrategy {
       private static final long serialVersionUID = 5709330763161570411L;
 
@@ -534,7 +560,10 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
       }
 
       @Override
-      protected String fmtFields(String randomKey, String nameDigest, String partRange) {
+      protected String fmtFields(String randomKey, String ultimateObjectName, String partRange) {
+        HashCode hashCode =
+            OBJECT_NAME_HASH_FUNCTION.hashString(ultimateObjectName, StandardCharsets.UTF_8);
+        String nameDigest = B64.encodeToString(hashCode.asBytes());
         return prefix
             + "/"
             + randomKey
@@ -546,6 +575,35 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
       }
     }
 
+    static final class WithObjectLevelPrefix extends PartNamingStrategy {
+
+      private static final long serialVersionUID = 5157942020618764450L;
+      private final String prefix;
+
+      private WithObjectLevelPrefix(SecureRandom rand, String prefix) {
+        super(rand);
+        // If no prefix is specified we will create the part files under the same directory as the
+        // ultimate object.
+        this.prefix = prefix.isEmpty() ? prefix : prefix + "/";
+      }
+
+      @Override
+      protected String fmtFields(String randomKey, String ultimateObjectName, String partRange) {
+        HashCode hashCode =
+            OBJECT_NAME_HASH_FUNCTION.hashString(ultimateObjectName, StandardCharsets.UTF_8);
+        String nameDigest = B64.encodeToString(hashCode.asBytes());
+        return prefix
+            + ultimateObjectName
+            + "-"
+            + randomKey
+            + FIELD_SEPARATOR
+            + nameDigest
+            + FIELD_SEPARATOR
+            + partRange
+            + ".part";
+      }
+    }
+
     static final class NoPrefix extends PartNamingStrategy {
       private static final long serialVersionUID = 5202415556658566017L;
 
diff --git a/pom.xml b/pom.xml
index d97926bc..93992574 100644
--- a/pom.xml
+++ b/pom.xml
@@ -86,7 +86,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-pubsub</artifactId>
-        <version>1.125.13</version>
+        <version>1.126.0</version>
         <scope>test</scope>
       </dependency>
       <dependency>
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index 8f09fcb6..c990ce8c 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -61,7 +61,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.125.13</version>
+      <version>1.126.0</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/samples/native-image-sample/pom.xml b/samples/native-image-sample/pom.xml
index 74189e26..37edffcd 100644
--- a/samples/native-image-sample/pom.xml
+++ b/samples/native-image-sample/pom.xml
@@ -29,7 +29,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>libraries-bom</artifactId>
-        <version>26.29.0</version>
+        <version>26.30.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
@@ -61,7 +61,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.125.13</version>
+      <version>1.126.0</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 5dd98db6..9ad81239 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -52,7 +52,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.125.13</version>
+      <version>1.126.0</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index 2b889923..593dfcb9 100644
--- a/samples/snippets/pom.xml
+++ b/samples/snippets/pom.xml
@@ -31,7 +31,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>libraries-bom</artifactId>
-        <version>26.29.0</version>
+        <version>26.30.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
@@ -72,7 +72,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.125.13</version>
+      <version>1.126.0</version>
       <scope>test</scope>
     </dependency>
   </dependencies>


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfigTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfigTest.java
index 55625940..86eddc5b 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfigTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfigTest.java
@@ -70,6 +70,23 @@ public final class ParallelCompositeUploadBlobWriteSessionConfigTest {
         () -> assertThat(fmt).startsWith("[%s]/"));
   }
 
+  @Test
+  public void partNameStrategy_objectNamePrefix() throws Exception {
+    // Creating an object level prefix without specifying an additional prefix will append the
+    // object name to the beginning of the part name.
+    PartNamingStrategy strategy = PartNamingStrategy.useObjectNameAsPrefix();
+
+    String fmt = strategy.fmtName("a/b/obj", PartRange.of(1, 96));
+    assertAll(
+        // random digest with prefix to spread over keyspace
+        // digest is 22, objectName is 7, slash is 1
+        () -> assertField(fmt, 0).hasLength(22 + 8),
+        // name digest
+        () -> assertField(fmt, 1).hasLength(22),
+        () -> assertField(fmt, 2).isEqualTo("0001-0096.part"),
+        () -> assertThat(fmt).startsWith("a/b/obj/"));
+  }
+
   private static StringSubject assertField(String fmt, int idx) {
     String[] split = fmt.split(";");
     String s = split[idx];