|
92 | 92 | import com.google.storage.v2.ListBucketsRequest;
|
93 | 93 | import com.google.storage.v2.ListHmacKeysRequest;
|
94 | 94 | import com.google.storage.v2.ListObjectsRequest;
|
| 95 | +import com.google.storage.v2.ListObjectsResponse; |
95 | 96 | import com.google.storage.v2.LockBucketRetentionPolicyRequest;
|
96 | 97 | import com.google.storage.v2.Object;
|
97 | 98 | import com.google.storage.v2.ObjectAccessControl;
|
|
101 | 102 | import com.google.storage.v2.RewriteObjectRequest;
|
102 | 103 | import com.google.storage.v2.RewriteResponse;
|
103 | 104 | import com.google.storage.v2.StorageClient;
|
104 |
| -import com.google.storage.v2.StorageClient.ListObjectsPage; |
105 |
| -import com.google.storage.v2.StorageClient.ListObjectsPagedResponse; |
106 | 105 | import com.google.storage.v2.UpdateBucketRequest;
|
107 | 106 | import com.google.storage.v2.UpdateHmacKeyRequest;
|
108 | 107 | import com.google.storage.v2.UpdateObjectRequest;
|
|
127 | 126 | import java.nio.file.StandardOpenOption;
|
128 | 127 | import java.util.Arrays;
|
129 | 128 | import java.util.List;
|
| 129 | +import java.util.Map; |
130 | 130 | import java.util.Objects;
|
131 | 131 | import java.util.Optional;
|
132 | 132 | import java.util.Set;
|
@@ -461,22 +461,18 @@ public Page<Bucket> list(BucketListOption... options) {
|
461 | 461 |
|
462 | 462 | @Override
|
463 | 463 | public Page<Blob> list(String bucket, BlobListOption... options) {
|
464 |
| - UnaryCallable<ListObjectsRequest, ListObjectsPagedResponse> listObjectsCallable = |
465 |
| - storageClient.listObjectsPagedCallable(); |
466 | 464 | Opts<ObjectListOpt> opts = Opts.unwrap(options);
|
467 | 465 | GrpcCallContext grpcCallContext =
|
468 | 466 | opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
|
469 | 467 | ListObjectsRequest.Builder builder =
|
470 | 468 | ListObjectsRequest.newBuilder().setParent(bucketNameCodec.encode(bucket));
|
471 | 469 | ListObjectsRequest req = opts.listObjectsRequest().apply(builder).build();
|
472 | 470 | try {
|
473 |
| - ListObjectsPagedResponse call = listObjectsCallable.call(req, grpcCallContext); |
474 |
| - ListObjectsPage page = call.getPage(); |
475 |
| - return new TransformingPageDecorator<>( |
476 |
| - page, |
477 |
| - syntaxDecoders.blob.andThen(opts.clearBlobFields()), |
| 471 | + return Retrying.run( |
478 | 472 | getOptions(),
|
479 |
| - retryAlgorithmManager.getFor(req)); |
| 473 | + retryAlgorithmManager.getFor(req), |
| 474 | + () -> storageClient.listObjectsCallable().call(req, grpcCallContext), |
| 475 | + resp -> new ListObjectsWithSyntheticDirectoriesPage(grpcCallContext, req, resp)); |
480 | 476 | } catch (Exception e) {
|
481 | 477 | throw StorageException.coalesce(e);
|
482 | 478 | }
|
@@ -1440,6 +1436,95 @@ private final class SyntaxDecoders {
|
1440 | 1436 | b -> codecs.bucketInfo().decode(b).asBucket(GrpcStorageImpl.this);
|
1441 | 1437 | }
|
1442 | 1438 |
|
| 1439 | + /** |
| 1440 | + * Today {@link com.google.cloud.storage.spi.v1.HttpStorageRpc#list(String, Map)} creates |
| 1441 | + * synthetic objects to represent {@code prefixes} ("directories") returned as part of a list |
| 1442 | + * objects response. Specifically, a StorageObject with an `isDirectory` attribute added. |
| 1443 | + * |
| 1444 | + * <p>This approach is not sound, and presents an otherwise ephemeral piece of metadata as an |
| 1445 | + * actual piece of data. (A {@code prefix} is not actually an object, and therefor can't be |
| 1446 | + * queried for other object metadata.) |
| 1447 | + * |
| 1448 | + * <p>In an effort to preserve compatibility with the current public API, this class attempts to |
| 1449 | + * encapsulate the process of producing these Synthetic Directory Objects and lifting them into |
| 1450 | + * the Page. |
| 1451 | + * |
| 1452 | + * <p>This behavior should NOT be carried forward to any possible new API for the storage client. |
| 1453 | + */ |
| 1454 | + private final class ListObjectsWithSyntheticDirectoriesPage implements Page<Blob> { |
| 1455 | + |
| 1456 | + private final GrpcCallContext ctx; |
| 1457 | + private final ListObjectsRequest req; |
| 1458 | + private final ListObjectsResponse resp; |
| 1459 | + |
| 1460 | + private ListObjectsWithSyntheticDirectoriesPage( |
| 1461 | + GrpcCallContext ctx, ListObjectsRequest req, ListObjectsResponse resp) { |
| 1462 | + this.ctx = ctx; |
| 1463 | + this.req = req; |
| 1464 | + this.resp = resp; |
| 1465 | + } |
| 1466 | + |
| 1467 | + @Override |
| 1468 | + public boolean hasNextPage() { |
| 1469 | + return !resp.getNextPageToken().isEmpty(); |
| 1470 | + } |
| 1471 | + |
| 1472 | + @Override |
| 1473 | + public String getNextPageToken() { |
| 1474 | + return resp.getNextPageToken(); |
| 1475 | + } |
| 1476 | + |
| 1477 | + @Override |
| 1478 | + public Page<Blob> getNextPage() { |
| 1479 | + ListObjectsRequest nextPageReq = |
| 1480 | + req.toBuilder().setPageToken(resp.getNextPageToken()).build(); |
| 1481 | + try { |
| 1482 | + ListObjectsResponse nextPageResp = |
| 1483 | + Retrying.run( |
| 1484 | + GrpcStorageImpl.this.getOptions(), |
| 1485 | + retryAlgorithmManager.getFor(nextPageReq), |
| 1486 | + () -> storageClient.listObjectsCallable().call(nextPageReq, ctx), |
| 1487 | + Decoder.identity()); |
| 1488 | + return new ListObjectsWithSyntheticDirectoriesPage(ctx, nextPageReq, nextPageResp); |
| 1489 | + } catch (Exception e) { |
| 1490 | + throw StorageException.coalesce(e); |
| 1491 | + } |
| 1492 | + } |
| 1493 | + |
| 1494 | + @Override |
| 1495 | + public Iterable<Blob> iterateAll() { |
| 1496 | + // drop to our interface type to help type inference below with the stream. |
| 1497 | + Page<Blob> curr = this; |
| 1498 | + Predicate<Page<Blob>> exhausted = p -> p != null && p.hasNextPage(); |
| 1499 | + // Create a stream which will attempt to call getNextPage repeatedly until we meet our |
| 1500 | + // condition of exhaustion. By doing this we are able to rely on the retry logic in |
| 1501 | + // getNextPage |
| 1502 | + return () -> |
| 1503 | + streamIterate(curr, exhausted, Page::getNextPage) |
| 1504 | + .filter(Objects::nonNull) |
| 1505 | + .flatMap(p -> StreamSupport.stream(p.getValues().spliterator(), false)) |
| 1506 | + .iterator(); |
| 1507 | + } |
| 1508 | + |
| 1509 | + @Override |
| 1510 | + public Iterable<Blob> getValues() { |
| 1511 | + return () -> { |
| 1512 | + String bucketName = bucketNameCodec.decode(req.getParent()); |
| 1513 | + return Streams.concat( |
| 1514 | + resp.getObjectsList().stream().map(syntaxDecoders.blob::decode), |
| 1515 | + resp.getPrefixesList().stream() |
| 1516 | + .map( |
| 1517 | + prefix -> |
| 1518 | + BlobInfo.newBuilder(bucketName, prefix) |
| 1519 | + .setSize(0L) |
| 1520 | + .setIsDirectory(true) |
| 1521 | + .build()) |
| 1522 | + .map(info -> info.asBlob(GrpcStorageImpl.this))) |
| 1523 | + .iterator(); |
| 1524 | + }; |
| 1525 | + } |
| 1526 | + } |
| 1527 | + |
1443 | 1528 | static final class TransformingPageDecorator<
|
1444 | 1529 | RequestT,
|
1445 | 1530 | ResponseT,
|
@@ -1511,50 +1596,50 @@ public Iterable<ModelT> getValues() {
|
1511 | 1596 | .map(translator::decode)
|
1512 | 1597 | .iterator();
|
1513 | 1598 | }
|
| 1599 | + } |
1514 | 1600 |
|
1515 |
| - private static <T> Stream<T> streamIterate( |
1516 |
| - T seed, Predicate<? super T> shouldComputeNext, UnaryOperator<T> computeNext) { |
1517 |
| - requireNonNull(seed, "seed must be non null"); |
1518 |
| - requireNonNull(shouldComputeNext, "shouldComputeNext must be non null"); |
1519 |
| - requireNonNull(computeNext, "computeNext must be non null"); |
1520 |
| - Spliterator<T> spliterator = |
1521 |
| - new AbstractSpliterator<T>(Long.MAX_VALUE, 0) { |
1522 |
| - T prev; |
1523 |
| - boolean started = false; |
1524 |
| - boolean done = false; |
1525 |
| - |
1526 |
| - @Override |
1527 |
| - public boolean tryAdvance(Consumer<? super T> action) { |
1528 |
| - // if we haven't started, emit our seed and return |
1529 |
| - if (!started) { |
1530 |
| - started = true; |
1531 |
| - action.accept(seed); |
1532 |
| - prev = seed; |
| 1601 | + private static <T> Stream<T> streamIterate( |
| 1602 | + T seed, Predicate<? super T> shouldComputeNext, UnaryOperator<T> computeNext) { |
| 1603 | + requireNonNull(seed, "seed must be non null"); |
| 1604 | + requireNonNull(shouldComputeNext, "shouldComputeNext must be non null"); |
| 1605 | + requireNonNull(computeNext, "computeNext must be non null"); |
| 1606 | + Spliterator<T> spliterator = |
| 1607 | + new AbstractSpliterator<T>(Long.MAX_VALUE, 0) { |
| 1608 | + T prev; |
| 1609 | + boolean started = false; |
| 1610 | + boolean done = false; |
| 1611 | + |
| 1612 | + @Override |
| 1613 | + public boolean tryAdvance(Consumer<? super T> action) { |
| 1614 | + // if we haven't started, emit our seed and return |
| 1615 | + if (!started) { |
| 1616 | + started = true; |
| 1617 | + action.accept(seed); |
| 1618 | + prev = seed; |
| 1619 | + return true; |
| 1620 | + } |
| 1621 | + // if we've previously finished quickly return |
| 1622 | + if (done) { |
| 1623 | + return false; |
| 1624 | + } |
| 1625 | + // test whether we should try and compute the next value |
| 1626 | + if (shouldComputeNext.test(prev)) { |
| 1627 | + // compute the next value and figure out if we can use it |
| 1628 | + T next = computeNext.apply(prev); |
| 1629 | + if (next != null) { |
| 1630 | + action.accept(next); |
| 1631 | + prev = next; |
1533 | 1632 | return true;
|
1534 | 1633 | }
|
1535 |
| - // if we've previously finished quickly return |
1536 |
| - if (done) { |
1537 |
| - return false; |
1538 |
| - } |
1539 |
| - // test whether we should try and compute the next value |
1540 |
| - if (shouldComputeNext.test(prev)) { |
1541 |
| - // compute the next value and figure out if we can use it |
1542 |
| - T next = computeNext.apply(prev); |
1543 |
| - if (next != null) { |
1544 |
| - action.accept(next); |
1545 |
| - prev = next; |
1546 |
| - return true; |
1547 |
| - } |
1548 |
| - } |
1549 |
| - |
1550 |
| - // fallthrough, if we haven't taken an action by now consider the stream done and |
1551 |
| - // return |
1552 |
| - done = true; |
1553 |
| - return false; |
1554 | 1634 | }
|
1555 |
| - }; |
1556 |
| - return StreamSupport.stream(spliterator, false); |
1557 |
| - } |
| 1635 | + |
| 1636 | + // fallthrough, if we haven't taken an action by now consider the stream done and |
| 1637 | + // return |
| 1638 | + done = true; |
| 1639 | + return false; |
| 1640 | + } |
| 1641 | + }; |
| 1642 | + return StreamSupport.stream(spliterator, false); |
1558 | 1643 | }
|
1559 | 1644 |
|
1560 | 1645 | static <T> T throwHttpJsonOnly(String methodName) {
|
|
0 commit comments