From 4f0bd03e9d47592547d9d791fe463644c33ea5ad Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:21:39 +0000 Subject: [PATCH 1/3] feat: implement deleteSourceObjects for compose operation Added deleteSourceObjects to Storage.ComposeRequest and Builder. Updated StorageImpl, GrpcStorageImpl and HttpStorageRpc to support the flag. Added unit test in StorageImplMockitoTest and integration test in ITObjectTest. Co-authored-by: nidhiii-27 <224584462+nidhiii-27@users.noreply.github.com> --- .../google/cloud/storage/GrpcStorageImpl.java | 3 ++ .../com/google/cloud/storage/Storage.java | 18 ++++++++++ .../com/google/cloud/storage/StorageImpl.java | 5 ++- .../cloud/storage/spi/v1/HttpStorageRpc.java | 3 ++ .../cloud/storage/spi/v1/StorageRpc.java | 1 + .../cloud/storage/StorageImplMockitoTest.java | 35 +++++++++++++++++++ .../google/cloud/storage/it/ITObjectTest.java | 28 +++++++++++++++ 7 files changed, 92 insertions(+), 1 deletion(-) diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java index b536340e9c23..0112b3d08a4a 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java @@ -645,6 +645,9 @@ public Blob compose(ComposeRequest composeRequest) { .forEach(builder::addSourceObjects); final Object target = codecs.blobInfo().encode(composeRequest.getTarget()); builder.setDestination(target); + if (composeRequest.isDeleteSourceObjects()) { + builder.setDeleteSourceObjects(true); + } ComposeObjectRequest req = opts.composeObjectsRequest().apply(builder).build(); GrpcCallContext merge = Utils.merge(grpcCallContext, Retrying.newCallContext()); return retrier.run( diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java index 12ac95dff7c4..9d71d8f73711 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java @@ -3185,6 +3185,7 @@ class ComposeRequest implements Serializable { private final List sourceBlobs; private final BlobInfo target; private final List targetOptions; + private final boolean deleteSourceObjects; private transient Opts targetOpts; @@ -3222,6 +3223,7 @@ public static class Builder { private final Set targetOptions = new LinkedHashSet<>(); private BlobInfo target; private Opts opts = Opts.empty(); + private boolean deleteSourceObjects; /** Add source blobs for compose operation. */ public Builder addSource(Iterable blobs) { @@ -3265,6 +3267,16 @@ public Builder setTargetOptions(Iterable options) { return this; } + /** + * Sets whether to delete source blobs after compose operation. + * + * @since 2.67.0 + */ + public Builder setDeleteSourceObjects(boolean deleteSourceObjects) { + this.deleteSourceObjects = deleteSourceObjects; + return this; + } + /** Creates a {@code ComposeRequest} object. */ public ComposeRequest build() { checkArgument(!sourceBlobs.isEmpty()); @@ -3280,6 +3292,7 @@ private ComposeRequest(Builder builder) { // keep targetOptions for serialization even though we will read targetOpts targetOptions = ImmutableList.copyOf(builder.targetOptions); targetOpts = builder.opts.prepend(Opts.unwrap(targetOptions).resolveFrom(target)); + deleteSourceObjects = builder.deleteSourceObjects; } /** Returns compose operation's source blobs. */ @@ -3297,6 +3310,11 @@ public List getTargetOptions() { return targetOptions; } + /** Returns whether to delete source blobs after compose operation. */ + public boolean isDeleteSourceObjects() { + return deleteSourceObjects; + } + @InternalApi Opts getTargetOpts() { return targetOpts; diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java index ebc4cbe5d736..8a510b84e7cc 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java @@ -651,7 +651,10 @@ public Blob compose(final ComposeRequest composeRequest) { } Opts targetOpts = composeRequest.getTargetOpts(); StorageObject targetPb = codecs.blobInfo().encode(composeRequest.getTarget()); - Map targetOptions = targetOpts.getRpcOptions(); + Map targetOptions = Maps.newHashMap(targetOpts.getRpcOptions()); + if (composeRequest.isDeleteSourceObjects()) { + targetOptions.put(StorageRpc.Option.DELETE_SOURCE_OBJECTS, true); + } ResultRetryAlgorithm algorithm = retryAlgorithmManager.getForObjectsCompose(sources, targetPb, targetOptions); return run( diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java index 5f910fb7775a..cdd2a2d874a8 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java @@ -817,6 +817,9 @@ public StorageObject compose( sourceObjects.add(sourceObject); } request.setSourceObjects(sourceObjects); + if (Option.DELETE_SOURCE_OBJECTS.getBoolean(targetOptions) != null) { + request.setDeleteSourceObjects(Option.DELETE_SOURCE_OBJECTS.getBoolean(targetOptions)); + } Span span = startSpan(HttpStorageRpcSpans.SPAN_NAME_COMPOSE); Scope scope = tracer.withSpan(span); try { diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java index 59a56df12022..33f24a54c854 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java @@ -82,6 +82,7 @@ enum Option { INCLUDE_TRAILING_DELIMITER("includeTrailingDelimiter"), X_UPLOAD_CONTENT_LENGTH("x-upload-content-length"), OBJECT_FILTER("objectFilter"), + DELETE_SOURCE_OBJECTS("deleteSourceObjects"), /** * An {@link com.google.common.collect.ImmutableMap ImmutableMap<String, String>} of values * which will be set as additional headers on the request. diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java index ec9dfe9e7460..5651b8e2de57 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java @@ -1038,4 +1038,39 @@ private void verifyBucketNotification(Notification value) { assertEquals(TOPIC, value.getTopic()); assertEquals(Arrays.asList(EVENT_TYPES), value.getEventTypes()); } + + @Test + public void testComposeWithDeleteSourceObjects() { + String bucket = "b1"; + String source1 = "s1"; + String source2 = "s2"; + String target = "t1"; + BlobId targetId = BlobId.of(bucket, target); + BlobInfo targetInfo = BlobInfo.newBuilder(targetId).build(); + Storage.ComposeRequest req = + Storage.ComposeRequest.newBuilder() + .addSource(source1, source2) + .setTarget(targetInfo) + .setDeleteSourceObjects(true) + .build(); + + StorageObject targetPb = Conversions.json().blobInfo().encode(targetInfo); + List sourcePbs = + ImmutableList.of( + Conversions.json().blobInfo().encode(BlobInfo.newBuilder(bucket, source1).build()), + Conversions.json().blobInfo().encode(BlobInfo.newBuilder(bucket, source2).build())); + + ArgumentCaptor> optionsCaptor = + ArgumentCaptor.forClass(Map.class); + + doReturn(targetPb) + .when(storageRpcMock) + .compose(Mockito.eq(sourcePbs), Mockito.eq(targetPb), optionsCaptor.capture()); + + initializeService(); + storage.compose(req); + + Map capturedOptions = optionsCaptor.getValue(); + assertEquals(true, capturedOptions.get(StorageRpc.Option.DELETE_SOURCE_OBJECTS)); + } } diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java index 83a7aeb5a912..f186afd077b6 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java @@ -812,6 +812,34 @@ public void testComposeBlobWithContentType() { assertArrayEquals(composedBytes, readBytes); } + @Test + public void testComposeBlobWithDeleteSourceObjects() { + String baseName = generator.randomObjectName(); + String sourceBlobName1 = baseName + "-1"; + String sourceBlobName2 = baseName + "-2"; + BlobInfo sourceBlob1 = BlobInfo.newBuilder(bucket, sourceBlobName1).build(); + BlobInfo sourceBlob2 = BlobInfo.newBuilder(bucket, sourceBlobName2).build(); + storage.create(sourceBlob1, BLOB_BYTE_CONTENT); + storage.create(sourceBlob2, BLOB_BYTE_CONTENT); + + String targetBlobName = baseName + "-target"; + BlobInfo targetBlob = BlobInfo.newBuilder(bucket, targetBlobName).build(); + ComposeRequest req = + ComposeRequest.newBuilder() + .addSource(sourceBlobName1, sourceBlobName2) + .setTarget(targetBlob) + .setDeleteSourceObjects(true) + .build(); + Blob remoteTargetBlob = storage.compose(req); + assertNotNull(remoteTargetBlob); + + assertNull(storage.get(bucket.getName(), sourceBlobName1)); + assertNull(storage.get(bucket.getName(), sourceBlobName2)); + + byte[] readBytes = storage.readAllBytes(bucket.getName(), targetBlobName); + assertThat(readBytes.length).isEqualTo(BLOB_BYTE_CONTENT.length * 2); + } + @Test public void testComposeBlobFail() { String baseName = generator.randomObjectName(); From 8efd0a5ebe864f053983229f2fbb847d4539d3f8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:08:07 +0000 Subject: [PATCH 2/3] feat: implement deleteSourceObjects for compose operation - Added deleteSourceObjects to Storage.ComposeRequest and Builder. - Updated StorageImpl, GrpcStorageImpl, and HttpStorageRpc to support the flag. - Added unit tests in StorageImplMockitoTest. - Added integration test in ITObjectTest. - Addressed PR feedback by directly setting the field in transport implementations. Co-authored-by: nidhiii-27 <224584462+nidhiii-27@users.noreply.github.com> --- .../main/java/com/google/cloud/storage/GrpcStorageImpl.java | 4 +--- .../java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java index 0112b3d08a4a..120b7a269724 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java @@ -645,9 +645,7 @@ public Blob compose(ComposeRequest composeRequest) { .forEach(builder::addSourceObjects); final Object target = codecs.blobInfo().encode(composeRequest.getTarget()); builder.setDestination(target); - if (composeRequest.isDeleteSourceObjects()) { - builder.setDeleteSourceObjects(true); - } + builder.setDeleteSourceObjects(composeRequest.isDeleteSourceObjects()); ComposeObjectRequest req = opts.composeObjectsRequest().apply(builder).build(); GrpcCallContext merge = Utils.merge(grpcCallContext, Retrying.newCallContext()); return retrier.run( diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java index cdd2a2d874a8..97814b597c37 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java @@ -817,9 +817,7 @@ public StorageObject compose( sourceObjects.add(sourceObject); } request.setSourceObjects(sourceObjects); - if (Option.DELETE_SOURCE_OBJECTS.getBoolean(targetOptions) != null) { - request.setDeleteSourceObjects(Option.DELETE_SOURCE_OBJECTS.getBoolean(targetOptions)); - } + request.setDeleteSourceObjects(Option.DELETE_SOURCE_OBJECTS.getBoolean(targetOptions)); Span span = startSpan(HttpStorageRpcSpans.SPAN_NAME_COMPOSE); Scope scope = tracer.withSpan(span); try { From fd8a016aff6921ed78a1ac7d58f6b51bb58829c4 Mon Sep 17 00:00:00 2001 From: cloud-java-bot Date: Wed, 22 Apr 2026 12:16:01 +0000 Subject: [PATCH 3/3] chore: generate libraries at Wed Apr 22 12:14:28 UTC 2026 --- gapic-libraries-bom/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gapic-libraries-bom/pom.xml b/gapic-libraries-bom/pom.xml index 9cbef9e2cdee..5acc011becd1 100644 --- a/gapic-libraries-bom/pom.xml +++ b/gapic-libraries-bom/pom.xml @@ -1259,7 +1259,7 @@ com.google.cloud google-cloud-spanner-bom - 6.116.0 + 6.116.1 pom import