Skip to content

Commit 3aa6ae4

Browse files
Implement deleteSourceObjects in Storage.compose
Modified Storage.ComposeRequest to include the optional deleteSourceObjects parameter. Added DELETE_SOURCE_OBJECTS option to StorageRpc. Updated StorageImpl, GrpcStorageImpl, and HttpStorageRpc to propagate the new option to the backend. Added unit and integration tests to verify the implementation. Co-authored-by: nidhiii-27 <224584462+nidhiii-27@users.noreply.github.com>
1 parent 9a6eda5 commit 3aa6ae4

8 files changed

Lines changed: 124 additions & 1 deletion

File tree

google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,9 @@ public Blob compose(ComposeRequest composeRequest) {
645645
.forEach(builder::addSourceObjects);
646646
final Object target = codecs.blobInfo().encode(composeRequest.getTarget());
647647
builder.setDestination(target);
648+
if (composeRequest.deleteSourceObjects()) {
649+
builder.setDeleteSourceObjects(true);
650+
}
648651
ComposeObjectRequest req = opts.composeObjectsRequest().apply(builder).build();
649652
GrpcCallContext merge = Utils.merge(grpcCallContext, Retrying.newCallContext());
650653
return retrier.run(

google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3185,6 +3185,7 @@ class ComposeRequest implements Serializable {
31853185
private final List<SourceBlob> sourceBlobs;
31863186
private final BlobInfo target;
31873187
private final List<BlobTargetOption> targetOptions;
3188+
private final boolean deleteSourceObjects;
31883189

31893190
private transient Opts<ObjectTargetOpt> targetOpts;
31903191

@@ -3222,6 +3223,7 @@ public static class Builder {
32223223
private final Set<BlobTargetOption> targetOptions = new LinkedHashSet<>();
32233224
private BlobInfo target;
32243225
private Opts<ObjectTargetOpt> opts = Opts.empty();
3226+
private boolean deleteSourceObjects;
32253227

32263228
/** Add source blobs for compose operation. */
32273229
public Builder addSource(Iterable<String> blobs) {
@@ -3265,6 +3267,16 @@ public Builder setTargetOptions(Iterable<BlobTargetOption> options) {
32653267
return this;
32663268
}
32673269

3270+
/**
3271+
* Sets whether to delete the source objects after the compose operation.
3272+
*
3273+
* @return the builder
3274+
*/
3275+
public Builder setDeleteSourceObjects(boolean deleteSourceObjects) {
3276+
this.deleteSourceObjects = deleteSourceObjects;
3277+
return this;
3278+
}
3279+
32683280
/** Creates a {@code ComposeRequest} object. */
32693281
public ComposeRequest build() {
32703282
checkArgument(!sourceBlobs.isEmpty());
@@ -3280,6 +3292,7 @@ private ComposeRequest(Builder builder) {
32803292
// keep targetOptions for serialization even though we will read targetOpts
32813293
targetOptions = ImmutableList.copyOf(builder.targetOptions);
32823294
targetOpts = builder.opts.prepend(Opts.unwrap(targetOptions).resolveFrom(target));
3295+
deleteSourceObjects = builder.deleteSourceObjects;
32833296
}
32843297

32853298
/** Returns compose operation's source blobs. */
@@ -3297,6 +3310,11 @@ public List<BlobTargetOption> getTargetOptions() {
32973310
return targetOptions;
32983311
}
32993312

3313+
/** Returns whether to delete the source objects after the compose operation. */
3314+
public boolean deleteSourceObjects() {
3315+
return deleteSourceObjects;
3316+
}
3317+
33003318
@InternalApi
33013319
Opts<ObjectTargetOpt> getTargetOpts() {
33023320
return targetOpts;

google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,14 @@ public Blob compose(final ComposeRequest composeRequest) {
651651
}
652652
Opts<ObjectTargetOpt> targetOpts = composeRequest.getTargetOpts();
653653
StorageObject targetPb = codecs.blobInfo().encode(composeRequest.getTarget());
654-
Map<StorageRpc.Option, ?> targetOptions = targetOpts.getRpcOptions();
654+
final Map<StorageRpc.Option, ?> targetOptions;
655+
if (composeRequest.deleteSourceObjects()) {
656+
Map<StorageRpc.Option, Object> mutableOptions = new HashMap<>(targetOpts.getRpcOptions());
657+
mutableOptions.put(StorageRpc.Option.DELETE_SOURCE_OBJECTS, true);
658+
targetOptions = Collections.unmodifiableMap(mutableOptions);
659+
} else {
660+
targetOptions = targetOpts.getRpcOptions();
661+
}
655662
ResultRetryAlgorithm<?> algorithm =
656663
retryAlgorithmManager.getForObjectsCompose(sources, targetPb, targetOptions);
657664
return run(

google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,9 @@ public StorageObject compose(
817817
sourceObjects.add(sourceObject);
818818
}
819819
request.setSourceObjects(sourceObjects);
820+
if (Boolean.TRUE.equals(targetOptions.get(Option.DELETE_SOURCE_OBJECTS))) {
821+
request.setDeleteSourceObjects(true);
822+
}
820823
Span span = startSpan(HttpStorageRpcSpans.SPAN_NAME_COMPOSE);
821824
Scope scope = tracer.withSpan(span);
822825
try {

google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ enum Option {
8282
INCLUDE_TRAILING_DELIMITER("includeTrailingDelimiter"),
8383
X_UPLOAD_CONTENT_LENGTH("x-upload-content-length"),
8484
OBJECT_FILTER("objectFilter"),
85+
DELETE_SOURCE_OBJECTS("deleteSourceObjects"),
8586
/**
8687
* An {@link com.google.common.collect.ImmutableMap ImmutableMap&lt;String, String>} of values
8788
* which will be set as additional headers on the request.

google-cloud-storage/src/test/java/com/google/cloud/storage/SerializationTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,20 @@ public void composeRequest() throws IOException, ClassNotFoundException {
361361
}
362362
}
363363

364+
@Test
365+
public void testComposeRequestSerialization() throws Exception {
366+
Storage.ComposeRequest request =
367+
Storage.ComposeRequest.newBuilder()
368+
.setTarget(BLOB_INFO)
369+
.addSource("s1", "s2")
370+
.setDeleteSourceObjects(true)
371+
.build();
372+
Storage.ComposeRequest copy = serializeAndDeserialize(request);
373+
assertThat(copy.getTarget()).isEqualTo(request.getTarget());
374+
assertThat(copy.getSourceBlobs().size()).isEqualTo(request.getSourceBlobs().size());
375+
assertThat(copy.deleteSourceObjects()).isTrue();
376+
}
377+
364378
/**
365379
* Here we override the super classes implementation to remove the "assertNotSame".
366380
*

google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
import java.security.spec.PKCS8EncodedKeySpec;
5353
import java.security.spec.X509EncodedKeySpec;
5454
import java.util.Arrays;
55+
import java.util.Collections;
56+
import java.util.HashMap;
5557
import java.util.List;
5658
import java.util.Map;
5759
import javax.crypto.spec.SecretKeySpec;
@@ -134,6 +136,12 @@ public class StorageImplMockitoTest {
134136
Storage.BlobTargetOption.doesNotExist();
135137
private static final Storage.BlobTargetOption BLOB_TARGET_PREDEFINED_ACL =
136138
Storage.BlobTargetOption.predefinedAcl(Storage.PredefinedAcl.PRIVATE);
139+
private static final Storage.ComposeRequest COMPOSE_REQUEST =
140+
Storage.ComposeRequest.newBuilder()
141+
.setTarget(BLOB_INFO1)
142+
.addSource(BLOB_NAME2, BLOB_NAME3)
143+
.setDeleteSourceObjects(true)
144+
.build();
137145
private static final Map<StorageRpc.Option, ?> BLOB_TARGET_OPTIONS_CREATE =
138146
ImmutableMap.of(
139147
StorageRpc.Option.IF_METAGENERATION_MATCH, BLOB_INFO1.getMetageneration(),
@@ -1028,6 +1036,47 @@ public void testDeleteNotification() {
10281036
assertEquals(isDeleted, Boolean.TRUE);
10291037
}
10301038

1039+
@Test
1040+
public void testCompose() {
1041+
List<StorageObject> sources =
1042+
ImmutableList.of(
1043+
Conversions.json().blobInfo().encode(BlobInfo.newBuilder(BUCKET_NAME1, BLOB_NAME2).build()),
1044+
Conversions.json().blobInfo().encode(BlobInfo.newBuilder(BUCKET_NAME1, BLOB_NAME3).build()));
1045+
StorageObject target = Conversions.json().blobInfo().encode(BLOB_INFO1);
1046+
Map<StorageRpc.Option, Object> targetOptions = new HashMap<>();
1047+
targetOptions.put(StorageRpc.Option.DELETE_SOURCE_OBJECTS, true);
1048+
doReturn(target)
1049+
.doThrow(UNEXPECTED_CALL_EXCEPTION)
1050+
.when(storageRpcMock)
1051+
.compose(sources, target, targetOptions);
1052+
initializeService();
1053+
Blob blob = storage.compose(COMPOSE_REQUEST);
1054+
assertEquals(expectedBlob1, blob);
1055+
}
1056+
1057+
@Test
1058+
public void testComposeDeleteSourceObjectsFalse() {
1059+
List<StorageObject> sources =
1060+
ImmutableList.of(
1061+
Conversions.json().blobInfo().encode(BlobInfo.newBuilder(BUCKET_NAME1, BLOB_NAME2).build()),
1062+
Conversions.json().blobInfo().encode(BlobInfo.newBuilder(BUCKET_NAME1, BLOB_NAME3).build()));
1063+
StorageObject target = Conversions.json().blobInfo().encode(BLOB_INFO1);
1064+
Map<StorageRpc.Option, Object> targetOptions = new HashMap<>();
1065+
doReturn(target)
1066+
.doThrow(UNEXPECTED_CALL_EXCEPTION)
1067+
.when(storageRpcMock)
1068+
.compose(sources, target, targetOptions);
1069+
initializeService();
1070+
Storage.ComposeRequest request =
1071+
Storage.ComposeRequest.newBuilder()
1072+
.setTarget(BLOB_INFO1)
1073+
.addSource(BLOB_NAME2, BLOB_NAME3)
1074+
.setDeleteSourceObjects(false)
1075+
.build();
1076+
Blob blob = storage.compose(request);
1077+
assertEquals(expectedBlob1, blob);
1078+
}
1079+
10311080
private void verifyBucketNotification(Notification value) {
10321081
assertNull(value.getNotificationId());
10331082
assertEquals(CUSTOM_ATTRIBUTES, value.getCustomAttributes());

google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,34 @@ public void testComposeBlobWithContentType() {
812812
assertArrayEquals(composedBytes, readBytes);
813813
}
814814

815+
@Test
816+
public void testComposeBlobDeleteSource() {
817+
String baseName = generator.randomObjectName();
818+
String sourceBlobName1 = baseName + "-1";
819+
String sourceBlobName2 = baseName + "-2";
820+
BlobInfo sourceBlob1 = BlobInfo.newBuilder(bucket, sourceBlobName1).build();
821+
BlobInfo sourceBlob2 = BlobInfo.newBuilder(bucket, sourceBlobName2).build();
822+
storage.create(sourceBlob1, BLOB_BYTE_CONTENT);
823+
storage.create(sourceBlob2, BLOB_BYTE_CONTENT);
824+
825+
String targetBlobName = baseName + "-target";
826+
BlobInfo targetBlob = BlobInfo.newBuilder(bucket, targetBlobName).build();
827+
ComposeRequest req =
828+
ComposeRequest.newBuilder()
829+
.addSource(sourceBlobName1, sourceBlobName2)
830+
.setTarget(targetBlob)
831+
.setDeleteSourceObjects(true)
832+
.build();
833+
Blob remoteTargetBlob = storage.compose(req);
834+
assertNotNull(remoteTargetBlob);
835+
836+
assertNull(storage.get(bucket.getName(), sourceBlobName1));
837+
assertNull(storage.get(bucket.getName(), sourceBlobName2));
838+
839+
byte[] readBytes = storage.readAllBytes(bucket.getName(), targetBlobName);
840+
assertThat(readBytes.length).isEqualTo(BLOB_BYTE_CONTENT.length * 2);
841+
}
842+
815843
@Test
816844
public void testComposeBlobFail() {
817845
String baseName = generator.randomObjectName();

0 commit comments

Comments
 (0)