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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy