Skip to content

Commit 942347e

Browse files
authored
blobstore: add getTag and setTag apis (salesforce#117)
1 parent 32a920f commit 942347e

File tree

2 files changed

+123
-44
lines changed

2 files changed

+123
-44
lines changed

blob/blob-gcp/src/main/java/com/salesforce/multicloudj/blob/gcp/GcpBlobStore.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@
7575
import java.util.Iterator;
7676
import java.util.List;
7777
import java.util.Map;
78+
import java.util.Collections;
79+
import java.util.HashMap;
7880
import java.util.UUID;
7981
import java.util.concurrent.TimeUnit;
8082
import java.util.stream.Collectors;
@@ -87,6 +89,8 @@
8789
@AutoService(AbstractBlobStore.class)
8890
public class GcpBlobStore extends AbstractBlobStore<GcpBlobStore> {
8991

92+
private static final String TAG_PREFIX = "gcp-tag-";
93+
9094
private final Storage storage;
9195
private final GcpTransformer transformer;
9296

@@ -388,14 +392,44 @@ private Page<Blob> listMultipartParts(MultipartUpload mpu) {
388392

389393
@Override
390394
protected Map<String, String> doGetTags(String key) {
391-
throw new UnSupportedOperationException("Tags are not supported by GCP");
395+
Blob blob = storage.get(transformer.toBlobId(key, null));
396+
if(blob == null) {
397+
throw new SubstrateSdkException("Blob not found");
398+
}
399+
if(blob.getMetadata() == null) {
400+
return Collections.emptyMap();
401+
}
402+
return blob.getMetadata().entrySet().stream()
403+
.filter(entry -> entry.getKey().startsWith(TAG_PREFIX))
404+
.filter(entry -> entry.getValue()!=null)
405+
.map(entry -> Map.entry(entry.getKey().substring(TAG_PREFIX.length()), entry.getValue()))
406+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
392407
}
393-
408+
394409
@Override
395410
protected void doSetTags(String key, Map<String, String> tags) {
396-
throw new UnSupportedOperationException("Tags are not supported by GCP");
411+
Blob blob = storage.get(transformer.toBlobId(key, null));
412+
if(blob == null) {
413+
throw new SubstrateSdkException("Blob not found");
414+
}
415+
416+
Map<String, String> metadata = new HashMap<>();
417+
if(blob.getMetadata() != null) {
418+
metadata.putAll(blob.getMetadata());
419+
}
420+
// Remove all existing tags
421+
metadata.entrySet().removeIf(entry -> entry.getKey().startsWith(TAG_PREFIX));
422+
423+
// Add in all the new tags
424+
if(tags != null) {
425+
tags.forEach((tagName, tagValue) -> metadata.put(TAG_PREFIX + tagName, tagValue));
426+
}
427+
428+
Blob updatedBlob = blob.toBuilder().setMetadata(metadata).build();
429+
storage.update(updatedBlob);
397430
}
398431

432+
399433
@Override
400434
protected URL doGeneratePresignedUrl(PresignedUrlRequest request) {
401435
var blobInfo = transformer.toBlobInfo(request);

blob/blob-gcp/src/test/java/com/salesforce/multicloudj/blob/gcp/GcpBlobStoreTest.java

Lines changed: 86 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import org.mockito.Mock;
5454
import org.mockito.MockedStatic;
5555
import org.mockito.Mockito;
56+
import org.mockito.ArgumentCaptor;
5657
import org.mockito.junit.jupiter.MockitoExtension;
5758

5859
import java.io.ByteArrayInputStream;
@@ -772,6 +773,91 @@ void testDoListPage() {
772773
assertEquals(2048L, response.getBlobs().get(1).getObjectSize());
773774
}
774775

776+
@Test
777+
void testDoGetTags_FiltersAndStripsPrefix() {
778+
when(mockTransformer.toBlobId(TEST_KEY, null)).thenReturn(mockBlobId);
779+
when(mockStorage.get(mockBlobId)).thenReturn(mockBlob);
780+
Map<String, String> metadata = new HashMap<>();
781+
metadata.put("gcp-tag-env", "prod");
782+
metadata.put("gcp-tag-team", "sre");
783+
metadata.put("unrelated", "keep");
784+
metadata.put("gcp-tag-empty", null);
785+
when(mockBlob.getMetadata()).thenReturn(metadata);
786+
787+
Map<String, String> tags = gcpBlobStore.doGetTags(TEST_KEY);
788+
789+
assertEquals(2, tags.size());
790+
assertEquals("prod", tags.get("env"));
791+
assertEquals("sre", tags.get("team"));
792+
}
793+
794+
@Test
795+
void testDoGetTags_NoMetadataReturnsEmpty() {
796+
when(mockTransformer.toBlobId(TEST_KEY, null)).thenReturn(mockBlobId);
797+
when(mockStorage.get(mockBlobId)).thenReturn(mockBlob);
798+
when(mockBlob.getMetadata()).thenReturn(null);
799+
800+
Map<String, String> tags = gcpBlobStore.doGetTags(TEST_KEY);
801+
assertTrue(tags.isEmpty());
802+
}
803+
804+
@Test
805+
void testDoSetTags_ReplacesPrefixedKeepsOthers() {
806+
when(mockTransformer.toBlobId(TEST_KEY, null)).thenReturn(mockBlobId);
807+
when(mockStorage.get(mockBlobId)).thenReturn(mockBlob);
808+
809+
Map<String, String> existing = new HashMap<>();
810+
existing.put("owner", "alice");
811+
existing.put("gcp-tag-old", "toRemove");
812+
when(mockBlob.getMetadata()).thenReturn(existing);
813+
814+
// Mock builder chain to capture metadata map
815+
Blob.Builder mockBuilder = mock(Blob.Builder.class);
816+
Blob updatedBlob = mock(Blob.class);
817+
ArgumentCaptor<Map<String, String>> mdCaptor = ArgumentCaptor.forClass(Map.class);
818+
when(mockBlob.toBuilder()).thenReturn(mockBuilder);
819+
when(mockBuilder.setMetadata(mdCaptor.capture())).thenReturn(mockBuilder);
820+
when(mockBuilder.build()).thenReturn(updatedBlob);
821+
822+
Map<String, String> newTags = Map.of("tag1", "v1", "tag2", "v2");
823+
824+
gcpBlobStore.doSetTags(TEST_KEY, newTags);
825+
826+
verify(mockStorage).update(updatedBlob);
827+
Map<String, String> finalMd = mdCaptor.getValue();
828+
assertEquals("alice", finalMd.get("owner"));
829+
assertFalse(finalMd.containsKey("gcp-tag-old"));
830+
assertEquals("v1", finalMd.get("gcp-tag-tag1"));
831+
assertEquals("v2", finalMd.get("gcp-tag-tag2"));
832+
}
833+
834+
@Test
835+
void testDoSetTags_EmptyMapRemovesAllPrefixed() {
836+
when(mockTransformer.toBlobId(TEST_KEY, null)).thenReturn(mockBlobId);
837+
when(mockStorage.get(mockBlobId)).thenReturn(mockBlob);
838+
839+
Map<String, String> existing = new HashMap<>();
840+
existing.put("gcp-tag-a", "1");
841+
existing.put("gcp-tag-b", "2");
842+
existing.put("keep", "x");
843+
when(mockBlob.getMetadata()).thenReturn(existing);
844+
845+
Blob.Builder mockBuilder = mock(Blob.Builder.class);
846+
Blob updatedBlob = mock(Blob.class);
847+
ArgumentCaptor<Map<String, String>> mdCaptor = ArgumentCaptor.forClass(Map.class);
848+
when(mockBlob.toBuilder()).thenReturn(mockBuilder);
849+
when(mockBuilder.setMetadata(mdCaptor.capture())).thenReturn(mockBuilder);
850+
when(mockBuilder.build()).thenReturn(updatedBlob);
851+
852+
gcpBlobStore.doSetTags(TEST_KEY, Collections.emptyMap());
853+
854+
verify(mockStorage).update(updatedBlob);
855+
Map<String, String> finalMd = mdCaptor.getValue();
856+
assertEquals("x", finalMd.get("keep"));
857+
assertFalse(finalMd.containsKey("gcp-tag-a"));
858+
assertFalse(finalMd.containsKey("gcp-tag-b"));
859+
}
860+
775861
@Test
776862
void testDoListPageEmpty() {
777863
// Given
@@ -836,24 +922,6 @@ void testDoListPageWithPagination() {
836922
assertEquals(2048L, response.getBlobs().get(1).getObjectSize());
837923
}
838924

839-
@Test
840-
void testDoGetTags() {
841-
SubstrateSdkException exception = assertThrows(SubstrateSdkException.class, () -> {
842-
gcpBlobStore.doGetTags(TEST_KEY);
843-
});
844-
assertEquals("Tags are not supported by GCP", exception.getMessage());
845-
}
846-
847-
@Test
848-
void testDoSetTags() {
849-
850-
Map<String, String> tags = Map.of("tag1","value1","tag2","value2");
851-
SubstrateSdkException exception = assertThrows(SubstrateSdkException.class, () -> {
852-
gcpBlobStore.doSetTags(TEST_KEY, tags);
853-
});
854-
assertEquals("Tags are not supported by GCP", exception.getMessage());
855-
}
856-
857925
@Test
858926
void testDoDoesObjectExist_WithVersionId() {
859927
// Given
@@ -1810,29 +1878,6 @@ void testDoGeneratePresignedUrl_WithZeroExpiration() throws Exception {
18101878
verify(mockStorage).signUrl(eq(mockBlobInfo), eq(0L), eq(TimeUnit.MILLISECONDS), any(), any());
18111879
}
18121880

1813-
@Test
1814-
void testDoSetTags_WithEmptyMap() {
1815-
Map<String, String> emptyTags = new HashMap<>();
1816-
1817-
assertThrows(UnSupportedOperationException.class, () -> {
1818-
gcpBlobStore.doSetTags(TEST_KEY, emptyTags);
1819-
});
1820-
}
1821-
1822-
@Test
1823-
void testDoSetTags_WithNullMap() {
1824-
assertThrows(UnSupportedOperationException.class, () -> {
1825-
gcpBlobStore.doSetTags(TEST_KEY, null);
1826-
});
1827-
}
1828-
1829-
@Test
1830-
void testDoGetTags_WithNullBlob() {
1831-
assertThrows(UnSupportedOperationException.class, () -> {
1832-
gcpBlobStore.doGetTags(TEST_KEY);
1833-
});
1834-
}
1835-
18361881
@Test
18371882
void testDoUpload_WithPath_EmptyFile() throws IOException {
18381883
Path tempFile = Files.createTempFile("empty", ".txt");

0 commit comments

Comments
 (0)