diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
index d91603937..d1838985a 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
@@ -20,6 +20,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.Objects.requireNonNull;
+import com.google.api.core.BetaApi;
import com.google.api.core.InternalExtensionOnly;
import com.google.api.gax.paging.Page;
import com.google.auth.ServiceAccountSigner;
@@ -1300,6 +1301,18 @@ public static BlobListOption endOffset(@NonNull String endOffset) {
return new BlobListOption(UnifiedOpts.endOffset(endOffset));
}
+ /**
+ * Returns an option to set a glob pattern to filter results to blobs that match the pattern.
+ *
+ * @see List
+ * Objects
+ */
+ @BetaApi
+ @TransportCompatibility({Transport.HTTP})
+ public static BlobListOption matchGlob(@NonNull String glob) {
+ return new BlobListOption(UnifiedOpts.matchGlob(glob));
+ }
+
/**
* Returns an option to define the billing user project. This option is required by buckets with
* `requester_pays` flag enabled to assign operation costs.
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
index f632af925..295455f4e 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
@@ -413,6 +413,11 @@ static KmsKeyName kmsKeyName(@NonNull String kmsKeyName) {
return new KmsKeyName(kmsKeyName);
}
+ static MatchGlob matchGlob(@NonNull String glob) {
+ requireNonNull(glob, "glob must be non null");
+ return new MatchGlob(glob);
+ }
+
static Md5Match md5Match(@NonNull String md5) {
requireNonNull(md5, "md5 must be non null");
return new Md5Match(md5);
@@ -1070,6 +1075,20 @@ public Mapper rewriteObject() {
}
}
+ static final class MatchGlob extends RpcOptVal implements ObjectListOpt {
+ private static final long serialVersionUID = 8819855597395473178L;
+
+ private MatchGlob(String val) {
+ super(StorageRpc.Option.MATCH_GLOB, val);
+ }
+
+ @Override
+ public Mapper listObjects() {
+ return GrpcStorageImpl.throwHttpJsonOnly(
+ com.google.cloud.storage.Storage.BlobListOption.class, "matchGlob(String)");
+ }
+ }
+
@Deprecated
static final class Md5Match implements ObjectTargetOpt {
private static final long serialVersionUID = 5237207911268363887L;
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
index 27572417a..707ba4573 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
@@ -423,6 +423,7 @@ public Tuple> list(final String bucket, Map options = ImmutableMap.of(StorageRpc.Option.MATCH_GLOB, matchGlob);
+ ImmutableList blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
+ Tuple> result =
+ Tuple.of(
+ cursor, Iterables.transform(blobInfoList, Conversions.apiary().blobInfo()::encode));
+ doReturn(result)
+ .doThrow(UNEXPECTED_CALL_EXCEPTION)
+ .when(storageRpcMock)
+ .list(BUCKET_NAME1, options);
+
+ initializeService();
+ ImmutableList blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
+ Page page = storage.list(BUCKET_NAME1, Storage.BlobListOption.matchGlob(matchGlob));
+ assertEquals(cursor, page.getNextPageToken());
+ assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
+ }
+
@Test
public void testListBlobsWithException() {
doThrow(STORAGE_FAILURE).when(storageRpcMock).list(BUCKET_NAME1, EMPTY_RPC_OPTIONS);
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java
index f2afbb2db..1da2d7686 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java
@@ -16,6 +16,7 @@
package com.google.cloud.storage.it;
+import static com.google.cloud.storage.TestUtils.assertAll;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertArrayEquals;
@@ -65,6 +66,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;
@@ -542,6 +544,60 @@ public void testListBlobsCurrentDirectoryIncludesBothObjectsAndSyntheticDirector
assertThat(actual).contains(PackagePrivateMethodWorkarounds.noAcl(obj2Gen1));
}
+ @Test
+ // When gRPC support is added for matchGlob, enable this test for gRPC.
+ @Exclude(transports = Transport.GRPC)
+ public void testListBlobsWithMatchGlob() throws Exception {
+ BucketInfo bucketInfo = BucketInfo.newBuilder(generator.randomBucketName()).build();
+ try (TemporaryBucket tempBucket =
+ TemporaryBucket.newBuilder().setBucketInfo(bucketInfo).setStorage(storage).build()) {
+ BucketInfo bucket = tempBucket.getBucket();
+ assertNotNull(storage.create(BlobInfo.newBuilder(bucket, "foo/bar").build()));
+ assertNotNull(storage.create(BlobInfo.newBuilder(bucket, "foo/baz").build()));
+ assertNotNull(storage.create(BlobInfo.newBuilder(bucket, "foo/foobar").build()));
+ assertNotNull(storage.create(BlobInfo.newBuilder(bucket, "foobar").build()));
+
+ Page page1 = storage.list(bucket.getName(), BlobListOption.matchGlob("foo*bar"));
+ Page page2 = storage.list(bucket.getName(), BlobListOption.matchGlob("foo**bar"));
+ Page page3 = storage.list(bucket.getName(), BlobListOption.matchGlob("**/foobar"));
+ Page page4 = storage.list(bucket.getName(), BlobListOption.matchGlob("*/ba[rz]"));
+ Page page5 = storage.list(bucket.getName(), BlobListOption.matchGlob("*/ba[!a-y]"));
+ Page page6 =
+ storage.list(bucket.getName(), BlobListOption.matchGlob("**/{foobar,baz}"));
+ Page page7 =
+ storage.list(bucket.getName(), BlobListOption.matchGlob("foo/{foo*,*baz}"));
+ assertAll(
+ () ->
+ assertThat(Iterables.transform(page1.iterateAll(), blob -> blob.getName()))
+ .containsExactly("foobar")
+ .inOrder(),
+ () ->
+ assertThat(Iterables.transform(page2.iterateAll(), blob -> blob.getName()))
+ .containsExactly("foo/bar", "foo/foobar", "foobar")
+ .inOrder(),
+ () ->
+ assertThat(Iterables.transform(page3.iterateAll(), blob -> blob.getName()))
+ .containsExactly("foo/foobar", "foobar")
+ .inOrder(),
+ () ->
+ assertThat(Iterables.transform(page4.iterateAll(), blob -> blob.getName()))
+ .containsExactly("foo/bar", "foo/baz")
+ .inOrder(),
+ () ->
+ assertThat(Iterables.transform(page5.iterateAll(), blob -> blob.getName()))
+ .containsExactly("foo/baz")
+ .inOrder(),
+ () ->
+ assertThat(Iterables.transform(page6.iterateAll(), blob -> blob.getName()))
+ .containsExactly("foo/baz", "foo/foobar", "foobar")
+ .inOrder(),
+ () ->
+ assertThat(Iterables.transform(page7.iterateAll(), blob -> blob.getName()))
+ .containsExactly("foo/baz", "foo/foobar")
+ .inOrder());
+ }
+ }
+
@Test
public void testListBlobsMultiplePages() {
String basePath = generator.randomObjectName();