Skip to content

Commit 0b7a0df

Browse files
authored
feat: add Storage.BlobListOption#includeTrailingDelimiter (#3038)
1 parent 74c46dd commit 0b7a0df

File tree

5 files changed

+114
-1
lines changed

5 files changed

+114
-1
lines changed

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2677,6 +2677,17 @@ public static BlobListOption includeFolders(boolean includeFolders) {
26772677
return new BlobListOption(UnifiedOpts.includeFoldersAsPrefixes(includeFolders));
26782678
}
26792679

2680+
/**
2681+
* Returns an option which will cause blobs that end in exactly one instance of `delimiter` will
2682+
* have their metadata included rather than being synthetic objects.
2683+
*
2684+
* @since 2.52.0
2685+
*/
2686+
@TransportCompatibility({Transport.HTTP, Transport.GRPC})
2687+
public static BlobListOption includeTrailingDelimiter() {
2688+
return new BlobListOption(UnifiedOpts.includeTrailingDelimiter());
2689+
}
2690+
26802691
/**
26812692
* Returns an option to define the billing user project. This option is required by buckets with
26822693
* `requester_pays` flag enabled to assign operation costs.

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,10 @@ static IncludeFoldersAsPrefixes includeFoldersAsPrefixes(boolean includeFoldersA
381381
return new IncludeFoldersAsPrefixes(includeFoldersAsPrefixes);
382382
}
383383

384+
static IncludeTrailingDelimiter includeTrailingDelimiter() {
385+
return new IncludeTrailingDelimiter(true);
386+
}
387+
384388
@Deprecated
385389
static DetectContentType detectContentType() {
386390
return DetectContentType.INSTANCE;
@@ -706,6 +710,20 @@ public Mapper<ListObjectsRequest.Builder> listObjects() {
706710
}
707711
}
708712

713+
static final class IncludeTrailingDelimiter extends RpcOptVal<Boolean> implements ObjectListOpt {
714+
715+
private static final long serialVersionUID = 321916692864878282L;
716+
717+
private IncludeTrailingDelimiter(boolean val) {
718+
super(StorageRpc.Option.INCLUDE_TRAILING_DELIMITER, val);
719+
}
720+
721+
@Override
722+
public Mapper<ListObjectsRequest.Builder> listObjects() {
723+
return b -> b.setIncludeTrailingDelimiter(val);
724+
}
725+
}
726+
709727
static final class Delimiter extends RpcOptVal<String> implements ObjectListOpt {
710728
private static final long serialVersionUID = -3789556789947615714L;
711729

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,8 @@ public Tuple<String, Iterable<StorageObject>> list(final String bucket, Map<Opti
496496
.setFields(Option.FIELDS.getString(options))
497497
.setUserProject(Option.USER_PROJECT.getString(options))
498498
.setSoftDeleted(Option.SOFT_DELETED.getBoolean(options))
499-
.setIncludeFoldersAsPrefixes(Option.INCLUDE_FOLDERS_AS_PREFIXES.getBoolean(options));
499+
.setIncludeFoldersAsPrefixes(Option.INCLUDE_FOLDERS_AS_PREFIXES.getBoolean(options))
500+
.setIncludeTrailingDelimiter(Option.INCLUDE_TRAILING_DELIMITER.getBoolean(options));
500501
setExtraHeaders(list, options);
501502
Objects objects = list.execute();
502503
Iterable<StorageObject> storageObjects =

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
@@ -78,6 +78,7 @@ enum Option {
7878
COPY_SOURCE_ACL("copySourceAcl"),
7979
GENERATION("generation"),
8080
INCLUDE_FOLDERS_AS_PREFIXES("includeFoldersAsPrefixes"),
81+
INCLUDE_TRAILING_DELIMITER("includeTrailingDelimiter"),
8182
X_UPLOAD_CONTENT_LENGTH("x-upload-content-length"),
8283
/**
8384
* An {@link com.google.common.collect.ImmutableMap ImmutableMap&lt;String, String>} of values

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,4 +1607,86 @@ public void blob_update() throws Exception {
16071607
() -> assertThat(gen1_2.getMetadata()).isEqualTo(meta3),
16081608
() -> assertThat(gen1_2.getGeneration()).isEqualTo(gen1.getGeneration()));
16091609
}
1610+
1611+
@Test
1612+
public void listBlob_includeTrailingDelimiter() throws Exception {
1613+
final byte[] A = new byte[] {(byte) 'A'};
1614+
1615+
String basePath = generator.randomObjectName();
1616+
// create a series of objects under a stable test specific path
1617+
BlobId a = BlobId.of(bucket.getName(), String.format("%s/a", basePath));
1618+
BlobId b = BlobId.of(bucket.getName(), String.format("%s/b", basePath));
1619+
BlobId c = BlobId.of(bucket.getName(), String.format("%s/c", basePath));
1620+
BlobId a_ = BlobId.of(bucket.getName(), String.format("%s/a/", basePath));
1621+
BlobId b_ = BlobId.of(bucket.getName(), String.format("%s/b/", basePath));
1622+
BlobId c_ = BlobId.of(bucket.getName(), String.format("%s/c/", basePath));
1623+
BlobId d_ = BlobId.of(bucket.getName(), String.format("%s/d/", basePath));
1624+
BlobId a_A1 = BlobId.of(bucket.getName(), String.format("%s/a/A1", basePath));
1625+
BlobId a_A2 = BlobId.of(bucket.getName(), String.format("%s/a/A2", basePath));
1626+
BlobId b_B1 = BlobId.of(bucket.getName(), String.format("%s/b/B1", basePath));
1627+
BlobId c_C2 = BlobId.of(bucket.getName(), String.format("%s/c/C2", basePath));
1628+
1629+
storage.create(BlobInfo.newBuilder(a).build(), A, BlobTargetOption.doesNotExist());
1630+
storage.create(BlobInfo.newBuilder(b).build(), A, BlobTargetOption.doesNotExist());
1631+
storage.create(BlobInfo.newBuilder(c).build(), A, BlobTargetOption.doesNotExist());
1632+
storage.create(BlobInfo.newBuilder(a_).build(), A, BlobTargetOption.doesNotExist());
1633+
storage.create(BlobInfo.newBuilder(b_).build(), A, BlobTargetOption.doesNotExist());
1634+
storage.create(BlobInfo.newBuilder(c_).build(), A, BlobTargetOption.doesNotExist());
1635+
storage.create(BlobInfo.newBuilder(d_).build(), A, BlobTargetOption.doesNotExist());
1636+
storage.create(BlobInfo.newBuilder(a_A1).build(), A, BlobTargetOption.doesNotExist());
1637+
storage.create(BlobInfo.newBuilder(a_A2).build(), A, BlobTargetOption.doesNotExist());
1638+
storage.create(BlobInfo.newBuilder(b_B1).build(), A, BlobTargetOption.doesNotExist());
1639+
storage.create(BlobInfo.newBuilder(c_C2).build(), A, BlobTargetOption.doesNotExist());
1640+
1641+
// define all our options
1642+
BlobListOption[] blobListOptions =
1643+
new BlobListOption[] {
1644+
BlobListOption.currentDirectory(),
1645+
BlobListOption.includeTrailingDelimiter(),
1646+
BlobListOption.fields(BlobField.NAME, BlobField.GENERATION, BlobField.SIZE),
1647+
BlobListOption.prefix(basePath + "/")
1648+
};
1649+
// list and collect all the object names
1650+
List<Blob> blobs =
1651+
storage.list(bucket.getName(), blobListOptions).streamAll().collect(Collectors.toList());
1652+
1653+
// figure out what the base prefix of the objects is, so we can trim it down to make assertions
1654+
// more terse.
1655+
int trimLen = String.format(Locale.US, "gs://%s/%s", bucket.getName(), basePath).length();
1656+
List<String> names =
1657+
blobs.stream()
1658+
.map(
1659+
bi -> {
1660+
String uri = bi.getBlobId().toGsUtilUriWithGeneration();
1661+
int genIdx = uri.indexOf("#");
1662+
String substring;
1663+
if (genIdx > -1) {
1664+
// trim the string representation of the generation to make assertions easier.
1665+
// We really only need to know that a generation is present, not it's exact
1666+
// value.
1667+
substring = uri.substring(trimLen, genIdx + 1);
1668+
} else {
1669+
substring = uri.substring(trimLen);
1670+
}
1671+
return "..." + substring;
1672+
})
1673+
.collect(Collectors.toList());
1674+
1675+
assertThat(names)
1676+
.containsExactly(
1677+
// items
1678+
".../a#",
1679+
".../b#",
1680+
".../c#",
1681+
// items included due to includeTrailingDelimiter
1682+
".../a/#",
1683+
".../b/#",
1684+
".../c/#",
1685+
".../d/#",
1686+
// prefixes
1687+
".../a/",
1688+
".../b/",
1689+
".../c/",
1690+
".../d/");
1691+
}
16101692
}

0 commit comments

Comments
 (0)