Skip to content

Commit 93be97a

Browse files
feat: Add matchGlob parameter to BlobListOption (#1965)
This is a new feature (b/236167515) where List Objects allows filtering results based on a provided glob pattern. Currently only the JSON API supports the matchGlob parameter, so using the option with gRPC will throw an exception. gRPC support is planned for later. --------- Co-authored-by: BenWhitehead <[email protected]>
1 parent 0a033b3 commit 93be97a

File tree

6 files changed

+111
-0
lines changed

6 files changed

+111
-0
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static com.google.common.base.Preconditions.checkNotNull;
2121
import static java.util.Objects.requireNonNull;
2222

23+
import com.google.api.core.BetaApi;
2324
import com.google.api.core.InternalExtensionOnly;
2425
import com.google.api.gax.paging.Page;
2526
import com.google.auth.ServiceAccountSigner;
@@ -1300,6 +1301,18 @@ public static BlobListOption endOffset(@NonNull String endOffset) {
13001301
return new BlobListOption(UnifiedOpts.endOffset(endOffset));
13011302
}
13021303

1304+
/**
1305+
* Returns an option to set a glob pattern to filter results to blobs that match the pattern.
1306+
*
1307+
* @see <a href="https://cloud.google.com/storage/docs/json_api/v1/objects/list">List
1308+
* Objects</a>
1309+
*/
1310+
@BetaApi
1311+
@TransportCompatibility({Transport.HTTP})
1312+
public static BlobListOption matchGlob(@NonNull String glob) {
1313+
return new BlobListOption(UnifiedOpts.matchGlob(glob));
1314+
}
1315+
13031316
/**
13041317
* Returns an option to define the billing user project. This option is required by buckets with
13051318
* `requester_pays` flag enabled to assign operation costs.

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,11 @@ static KmsKeyName kmsKeyName(@NonNull String kmsKeyName) {
413413
return new KmsKeyName(kmsKeyName);
414414
}
415415

416+
static MatchGlob matchGlob(@NonNull String glob) {
417+
requireNonNull(glob, "glob must be non null");
418+
return new MatchGlob(glob);
419+
}
420+
416421
static Md5Match md5Match(@NonNull String md5) {
417422
requireNonNull(md5, "md5 must be non null");
418423
return new Md5Match(md5);
@@ -1070,6 +1075,20 @@ public Mapper<RewriteObjectRequest.Builder> rewriteObject() {
10701075
}
10711076
}
10721077

1078+
static final class MatchGlob extends RpcOptVal<String> implements ObjectListOpt {
1079+
private static final long serialVersionUID = 8819855597395473178L;
1080+
1081+
private MatchGlob(String val) {
1082+
super(StorageRpc.Option.MATCH_GLOB, val);
1083+
}
1084+
1085+
@Override
1086+
public Mapper<ListObjectsRequest.Builder> listObjects() {
1087+
return GrpcStorageImpl.throwHttpJsonOnly(
1088+
com.google.cloud.storage.Storage.BlobListOption.class, "matchGlob(String)");
1089+
}
1090+
}
1091+
10731092
@Deprecated
10741093
static final class Md5Match implements ObjectTargetOpt {
10751094
private static final long serialVersionUID = 5237207911268363887L;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ public Tuple<String, Iterable<StorageObject>> list(final String bucket, Map<Opti
423423
.setDelimiter(Option.DELIMITER.getString(options))
424424
.setStartOffset(Option.START_OFF_SET.getString(options))
425425
.setEndOffset(Option.END_OFF_SET.getString(options))
426+
.setMatchGlob(Option.MATCH_GLOB.getString(options))
426427
.setPrefix(Option.PREFIX.getString(options))
427428
.setMaxResults(Option.MAX_RESULTS.getLong(options))
428429
.setPageToken(Option.PAGE_TOKEN.getString(options))

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
@@ -61,6 +61,7 @@ enum Option {
6161
DELIMITER("delimiter"),
6262
START_OFF_SET("startOffset"),
6363
END_OFF_SET("endOffset"),
64+
MATCH_GLOB("matchGlob"),
6465
VERSIONS("versions"),
6566
FIELDS("fields"),
6667
CUSTOMER_SUPPLIED_KEY("customerSuppliedKey"),

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,6 +1308,27 @@ public void testListBlobsWithOffset() {
13081308
assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
13091309
}
13101310

1311+
@Test
1312+
public void testListBlobsMatchGlob() {
1313+
String cursor = "cursor";
1314+
String matchGlob = "foo*bar";
1315+
Map<StorageRpc.Option, ?> options = ImmutableMap.of(StorageRpc.Option.MATCH_GLOB, matchGlob);
1316+
ImmutableList<BlobInfo> blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
1317+
Tuple<String, Iterable<com.google.api.services.storage.model.StorageObject>> result =
1318+
Tuple.of(
1319+
cursor, Iterables.transform(blobInfoList, Conversions.apiary().blobInfo()::encode));
1320+
doReturn(result)
1321+
.doThrow(UNEXPECTED_CALL_EXCEPTION)
1322+
.when(storageRpcMock)
1323+
.list(BUCKET_NAME1, options);
1324+
1325+
initializeService();
1326+
ImmutableList<Blob> blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
1327+
Page<Blob> page = storage.list(BUCKET_NAME1, Storage.BlobListOption.matchGlob(matchGlob));
1328+
assertEquals(cursor, page.getNextPageToken());
1329+
assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
1330+
}
1331+
13111332
@Test
13121333
public void testListBlobsWithException() {
13131334
doThrow(STORAGE_FAILURE).when(storageRpcMock).list(BUCKET_NAME1, EMPTY_RPC_OPTIONS);

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.cloud.storage.it;
1818

19+
import static com.google.cloud.storage.TestUtils.assertAll;
1920
import static com.google.common.truth.Truth.assertThat;
2021
import static java.nio.charset.StandardCharsets.UTF_8;
2122
import static org.junit.Assert.assertArrayEquals;
@@ -65,6 +66,7 @@
6566
import com.google.common.collect.ImmutableList;
6667
import com.google.common.collect.ImmutableMap;
6768
import com.google.common.collect.ImmutableSet;
69+
import com.google.common.collect.Iterables;
6870
import com.google.common.collect.Iterators;
6971
import com.google.common.hash.Hashing;
7072
import com.google.common.io.BaseEncoding;
@@ -542,6 +544,60 @@ public void testListBlobsCurrentDirectoryIncludesBothObjectsAndSyntheticDirector
542544
assertThat(actual).contains(PackagePrivateMethodWorkarounds.noAcl(obj2Gen1));
543545
}
544546

547+
@Test
548+
// When gRPC support is added for matchGlob, enable this test for gRPC.
549+
@Exclude(transports = Transport.GRPC)
550+
public void testListBlobsWithMatchGlob() throws Exception {
551+
BucketInfo bucketInfo = BucketInfo.newBuilder(generator.randomBucketName()).build();
552+
try (TemporaryBucket tempBucket =
553+
TemporaryBucket.newBuilder().setBucketInfo(bucketInfo).setStorage(storage).build()) {
554+
BucketInfo bucket = tempBucket.getBucket();
555+
assertNotNull(storage.create(BlobInfo.newBuilder(bucket, "foo/bar").build()));
556+
assertNotNull(storage.create(BlobInfo.newBuilder(bucket, "foo/baz").build()));
557+
assertNotNull(storage.create(BlobInfo.newBuilder(bucket, "foo/foobar").build()));
558+
assertNotNull(storage.create(BlobInfo.newBuilder(bucket, "foobar").build()));
559+
560+
Page<Blob> page1 = storage.list(bucket.getName(), BlobListOption.matchGlob("foo*bar"));
561+
Page<Blob> page2 = storage.list(bucket.getName(), BlobListOption.matchGlob("foo**bar"));
562+
Page<Blob> page3 = storage.list(bucket.getName(), BlobListOption.matchGlob("**/foobar"));
563+
Page<Blob> page4 = storage.list(bucket.getName(), BlobListOption.matchGlob("*/ba[rz]"));
564+
Page<Blob> page5 = storage.list(bucket.getName(), BlobListOption.matchGlob("*/ba[!a-y]"));
565+
Page<Blob> page6 =
566+
storage.list(bucket.getName(), BlobListOption.matchGlob("**/{foobar,baz}"));
567+
Page<Blob> page7 =
568+
storage.list(bucket.getName(), BlobListOption.matchGlob("foo/{foo*,*baz}"));
569+
assertAll(
570+
() ->
571+
assertThat(Iterables.transform(page1.iterateAll(), blob -> blob.getName()))
572+
.containsExactly("foobar")
573+
.inOrder(),
574+
() ->
575+
assertThat(Iterables.transform(page2.iterateAll(), blob -> blob.getName()))
576+
.containsExactly("foo/bar", "foo/foobar", "foobar")
577+
.inOrder(),
578+
() ->
579+
assertThat(Iterables.transform(page3.iterateAll(), blob -> blob.getName()))
580+
.containsExactly("foo/foobar", "foobar")
581+
.inOrder(),
582+
() ->
583+
assertThat(Iterables.transform(page4.iterateAll(), blob -> blob.getName()))
584+
.containsExactly("foo/bar", "foo/baz")
585+
.inOrder(),
586+
() ->
587+
assertThat(Iterables.transform(page5.iterateAll(), blob -> blob.getName()))
588+
.containsExactly("foo/baz")
589+
.inOrder(),
590+
() ->
591+
assertThat(Iterables.transform(page6.iterateAll(), blob -> blob.getName()))
592+
.containsExactly("foo/baz", "foo/foobar", "foobar")
593+
.inOrder(),
594+
() ->
595+
assertThat(Iterables.transform(page7.iterateAll(), blob -> blob.getName()))
596+
.containsExactly("foo/baz", "foo/foobar")
597+
.inOrder());
598+
}
599+
}
600+
545601
@Test
546602
public void testListBlobsMultiplePages() {
547603
String basePath = generator.randomObjectName();

0 commit comments

Comments
 (0)