Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@ void includeTrailingDelimiterAddsThePlaceholderObjectToItems() throws Exception
assertThat(itemNames(withFlag)).contains("logs/");
}

@Test
void maxResultsPaginatesDirectoryPrefixes() {
var page = storage.list(BUCKET,
Storage.BlobListOption.currentDirectory(),
Storage.BlobListOption.pageSize(1));

assertThat(page.getValues()).singleElement()
.extracting(Blob::getName)
.isEqualTo("logs/");
assertThat(page.getNextPageToken()).isNotNull();
}

private static String listRaw(String query) throws Exception {
URI uri = URI.create(TestFixtures.endpoint() + "/storage/v1/b/" + BUCKET + "/o?" + query);
HttpResponse<String> response = HttpClient.newHttpClient().send(
Expand Down
60 changes: 55 additions & 5 deletions src/main/java/io/floci/gcp/services/gcs/GcsObjectController.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

@ApplicationScoped
Expand Down Expand Up @@ -110,21 +112,69 @@ public Response listObjects(@PathParam("bucket") String bucket,
}
all = rolledUp;
}
PageToken.Page<GcsObjectMeta> page = PageToken.paginate(all, maxResults, pageToken);
List<ListedResult> listedResults;
if (delimiter == null || delimiter.isEmpty()) {
listedResults = all.stream().map(ListedResult::object).toList();
} else {
TreeMap<String, List<GcsObjectMeta>> objectsByName = new TreeMap<>();
for (GcsObjectMeta meta : all) {
objectsByName.computeIfAbsent(meta.getName(), ignored -> new ArrayList<>()).add(meta);
}
Comparator<GcsObjectMeta> generationOrder = Comparator.comparingLong(meta -> Long.parseLong(meta.getGeneration()));
objectsByName.values().forEach(objects -> objects.sort(generationOrder));

Set<String> names = new TreeSet<>(prefixes);
names.addAll(objectsByName.keySet());
listedResults = new ArrayList<>();
for (String name : names) {
List<GcsObjectMeta> objects = objectsByName.getOrDefault(name, List.of());
if (prefixes.contains(name) && !objects.isEmpty()) {
listedResults.add(ListedResult.prefixAndObject(name, objects.getFirst()));
objects.stream().skip(1).map(ListedResult::object).forEach(listedResults::add);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
} else if (prefixes.contains(name)) {
listedResults.add(ListedResult.prefix(name));
} else {
objects.stream().map(ListedResult::object).forEach(listedResults::add);
}
}
}
PageToken.Page<ListedResult> page = PageToken.paginate(listedResults, maxResults, pageToken);
Map<String, Object> response = new LinkedHashMap<>();
response.put("kind", "storage#objects");
if (!page.items().isEmpty()) {
response.put("items", page.items());
List<GcsObjectMeta> pageItems = page.items().stream()
.map(ListedResult::object)
.filter(Objects::nonNull)
.toList();
if (!pageItems.isEmpty()) {
response.put("items", pageItems);
}
if (!prefixes.isEmpty()) {
response.put("prefixes", new ArrayList<>(prefixes));
List<String> pagePrefixes = page.items().stream()
.map(ListedResult::prefix)
.filter(Objects::nonNull)
.toList();
if (!pagePrefixes.isEmpty()) {
response.put("prefixes", pagePrefixes);
}
if (page.nextPageToken() != null) {
response.put("nextPageToken", page.nextPageToken());
}
return Response.ok(response).build();
}

private record ListedResult(String prefix, GcsObjectMeta object) {
private static ListedResult prefix(String prefix) {
return new ListedResult(prefix, null);
}

private static ListedResult object(GcsObjectMeta object) {
return new ListedResult(null, object);
}

private static ListedResult prefixAndObject(String prefix, GcsObjectMeta object) {
return new ListedResult(prefix, object);
}
}

@GET
@Path("/{object: .+}/acl")
public Response listObjectAcls(@PathParam("bucket") String bucket,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,77 @@ void includeTrailingDelimiterAlsoReturnsThePlaceholderAsAnItem() {
.body("prefixes", hasItem("a/"));
}

@Test
void maxResultsPaginatesItemsAndPrefixesTogether() {
seed();
String nextPageToken = given().queryParam("delimiter", "/").queryParam("maxResults", 2)
.when().get("/storage/v1/b/" + BUCKET + "/o")
.then().statusCode(200)
.body("items", org.hamcrest.Matchers.anyOf(org.hamcrest.Matchers.nullValue(), empty()))
.body("prefixes", contains("a/", "b/"))
.extract().path("nextPageToken");

given().queryParam("delimiter", "/").queryParam("maxResults", 2)
.queryParam("pageToken", nextPageToken)
.when().get("/storage/v1/b/" + BUCKET + "/o")
.then().statusCode(200)
.body("items.name", contains("c.txt"))
.body("prefixes", contains("logs/"));
}

@Test
void trailingDelimiterItemAndPrefixShareOneResultSlot() {
seed();
given().queryParam("delimiter", "/")
.queryParam("includeTrailingDelimiter", true)
.queryParam("maxResults", 1)
.when().get("/storage/v1/b/" + BUCKET + "/o")
.then().statusCode(200)
.body("items.name", contains("a/"))
.body("prefixes", contains("a/"))
.body("nextPageToken", org.hamcrest.Matchers.notNullValue());
}

@Test
void versionedTrailingDelimiterGenerationsHaveStablePages() {
String bucket = "versioned-list-filter-bucket";
given().contentType("application/json")
.body(Map.of("name", bucket, "versioning", Map.of("enabled", true)))
.when().post("/storage/v1/b?project=test-project")
.then().statusCode(200);
String firstGeneration = given().contentType("text/plain").body("first")
.queryParam("uploadType", "media").queryParam("name", "versioned/")
.when().post("/upload/storage/v1/b/" + bucket + "/o")
.then().statusCode(200)
.extract().path("generation");
String secondGeneration = given().contentType("text/plain").body("second")
.queryParam("uploadType", "media").queryParam("name", "versioned/")
.when().post("/upload/storage/v1/b/" + bucket + "/o")
.then().statusCode(200)
.extract().path("generation");

String nextPageToken = given().queryParam("delimiter", "/")
.queryParam("versions", true)
.queryParam("includeTrailingDelimiter", true)
.queryParam("maxResults", 1)
.when().get("/storage/v1/b/" + bucket + "/o")
.then().statusCode(200)
.body("items.name", contains("versioned/"))
.body("items.generation", contains(firstGeneration))
.body("prefixes", contains("versioned/"))
.extract().path("nextPageToken");

given().queryParam("delimiter", "/")
.queryParam("versions", true)
.queryParam("includeTrailingDelimiter", true)
.queryParam("maxResults", 1)
.queryParam("pageToken", nextPageToken)
.when().get("/storage/v1/b/" + bucket + "/o")
.then().statusCode(200)
.body("items.name", contains("versioned/"))
.body("items.generation", contains(secondGeneration));
}

@Test
void repeatedDoubleStarsAreCollapsedRatherThanCompounded() {
// "**/**/x" means the same as "**/x"; emitting both groups multiplies the backtracking
Expand Down