Skip to content

Commit 217b887

Browse files
authored
Merge pull request #473 from scribe/custom_delim
Custom Delimiter in S3
2 parents a48cf95 + b3a9014 commit 217b887

File tree

16 files changed

+412
-134
lines changed

16 files changed

+412
-134
lines changed

ds3-sdk-integration/src/test/java/com/spectralogic/ds3client/integration/Iterators_Test.java

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,18 @@
1515

1616
package com.spectralogic.ds3client.integration;
1717

18+
import com.google.common.collect.Lists;
1819
import com.spectralogic.ds3client.Ds3Client;
19-
import com.spectralogic.ds3client.helpers.pagination.GetBucketLoaderFactory;
2020
import com.spectralogic.ds3client.helpers.Ds3ClientHelpers;
21+
import com.spectralogic.ds3client.helpers.pagination.GetBucketKeyLoaderFactory;
2122
import com.spectralogic.ds3client.helpers.pagination.GetObjectsFullDetailsLoaderFactory;
22-
import com.spectralogic.ds3client.utils.collections.LazyIterable;
2323
import com.spectralogic.ds3client.integration.test.helpers.TempStorageIds;
2424
import com.spectralogic.ds3client.integration.test.helpers.TempStorageUtil;
2525
import com.spectralogic.ds3client.models.ChecksumType;
26+
import com.spectralogic.ds3client.models.Contents;
27+
import com.spectralogic.ds3client.models.FileSystemKey;
2628
import com.spectralogic.ds3client.networking.FailedRequestException;
29+
import com.spectralogic.ds3client.utils.collections.LazyIterable;
2730
import org.junit.AfterClass;
2831
import org.junit.BeforeClass;
2932
import org.junit.Test;
@@ -33,10 +36,10 @@
3336
import java.io.IOException;
3437
import java.net.URISyntaxException;
3538
import java.util.Iterator;
39+
import java.util.List;
3640
import java.util.UUID;
3741

38-
import static com.spectralogic.ds3client.integration.Util.deleteAllContents;
39-
import static com.spectralogic.ds3client.integration.Util.loadBookTestData;
42+
import static com.spectralogic.ds3client.integration.Util.*;
4043
import static org.hamcrest.CoreMatchers.*;
4144
import static org.junit.Assert.*;
4245

@@ -47,6 +50,7 @@ public class Iterators_Test {
4750
private static final Ds3ClientHelpers HELPERS = Ds3ClientHelpers.wrap(CLIENT);
4851
private static final String TEST_ENV_NAME = "lazy_iterator_test";
4952
private static final int RETRIES = 5;
53+
private static final String TEST_DELIMITER = null;
5054

5155
private static TempStorageIds envStorageIds;
5256
private static UUID envDataPolicyId;
@@ -67,21 +71,39 @@ public static void teardown() throws IOException {
6771
LOG.info("Finished test teardown...");
6872
}
6973

74+
@Test
75+
public void iterateWithDelimiter() throws IOException, URISyntaxException {
76+
try {
77+
HELPERS.ensureBucketExists(TEST_ENV_NAME, envDataPolicyId);
78+
loadBookTestDataWithPrefix(CLIENT, TEST_ENV_NAME, "books/");
79+
final Iterable<FileSystemKey> fileSystemKeysIterator = Ds3ClientHelpers.wrap(CLIENT).remoteListDirectory(TEST_ENV_NAME, "books/", null, 100);
80+
final List<FileSystemKey> fileSystemKeys = Lists.newArrayList(fileSystemKeysIterator);
81+
assertThat(fileSystemKeys.size(), is(4));
82+
83+
loadBookTestDataWithPrefix(CLIENT, TEST_ENV_NAME, "books/more/");
84+
final Iterable<FileSystemKey> anotherFileSystemKeysIterator = Ds3ClientHelpers.wrap(CLIENT).remoteListDirectory(TEST_ENV_NAME, "books/", null, 100);
85+
final List<FileSystemKey> anotherFileSystemKeys = Lists.newArrayList(anotherFileSystemKeysIterator);
86+
assertThat(anotherFileSystemKeys.size(), is(5));
87+
} finally {
88+
deleteAllContents(CLIENT, TEST_ENV_NAME);
89+
}
90+
}
91+
7092
@Test
7193
public void emptyGetBucket() throws IOException {
7294
final String prefix = "";
7395
final String nextMarker = null;
7496
final int maxKeys = 100;
7597

76-
emptyTest(new GetBucketLoaderFactory(CLIENT, TEST_ENV_NAME, prefix, nextMarker, maxKeys, RETRIES));
98+
emptyTest(new GetBucketKeyLoaderFactory<Contents>(CLIENT, TEST_ENV_NAME, prefix, TEST_DELIMITER, nextMarker, maxKeys, RETRIES, GetBucketKeyLoaderFactory.contentsFunction));
7799
}
78100

79101
@Test
80102
public void singlePageGetBucket() throws IOException, URISyntaxException {
81103
final String prefix = "";
82104
final String nextMarker = null;
83105
final int maxKeys = 100;
84-
paginate(new GetBucketLoaderFactory(CLIENT, TEST_ENV_NAME, prefix, nextMarker, maxKeys, RETRIES));
106+
paginate(new GetBucketKeyLoaderFactory<Contents>(CLIENT, TEST_ENV_NAME, prefix, TEST_DELIMITER, nextMarker, maxKeys, RETRIES, GetBucketKeyLoaderFactory.contentsFunction));
85107
}
86108

87109

@@ -90,13 +112,13 @@ public void multiPageGetBucket() throws IOException, URISyntaxException {
90112
final String prefix = "";
91113
final String nextMarker = null;
92114
final int maxKeys = 2;
93-
paginate(new GetBucketLoaderFactory(CLIENT, TEST_ENV_NAME, prefix, nextMarker, maxKeys, RETRIES));
115+
paginate(new GetBucketKeyLoaderFactory<Contents>(CLIENT, TEST_ENV_NAME, prefix, nextMarker, TEST_DELIMITER, maxKeys, RETRIES, GetBucketKeyLoaderFactory.contentsFunction));
94116

95117
}
96118

97119
@Test
98120
public void failedRequestGetBucket() {
99-
testFailedRequest(new GetBucketLoaderFactory(CLIENT, "Unknown_Bucket",null, null, 1000, 5));
121+
testFailedRequest(new GetBucketKeyLoaderFactory<Contents>(CLIENT, "Unknown_Bucket",null, TEST_DELIMITER,null, 1000, 5, GetBucketKeyLoaderFactory.contentsFunction));
100122
}
101123

102124
@Test

ds3-sdk-integration/src/test/java/com/spectralogic/ds3client/integration/test/helpers/TempStorageIds.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
* These IDs are stored for teardown of testing environment.
2424
*/
2525
public class TempStorageIds {
26-
private UUID storageDomainMemberId;
27-
private UUID dataPersistenceRuleId;
26+
private final UUID storageDomainMemberId;
27+
private final UUID dataPersistenceRuleId;
2828

2929
public TempStorageIds(final UUID storageDomainMemberId, final UUID dataPersistenceRuleId) {
3030
this.storageDomainMemberId = storageDomainMemberId;

ds3-sdk/src/main/java/com/spectralogic/ds3client/commands/interfaces/AbstractPaginationResponse.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919

2020
public abstract class AbstractPaginationResponse extends AbstractResponse implements PaginationResponse {
2121

22-
private Integer pagingTruncated;
23-
private Integer pagingTotalResultCount;
22+
private final Integer pagingTruncated;
23+
private final Integer pagingTotalResultCount;
2424

2525
public AbstractPaginationResponse(final Integer pagingTotalResultCount, final Integer pagingTruncated, final String checksum, final ChecksumType.Type checksumType) {
2626
super(checksum, checksumType);

ds3-sdk/src/main/java/com/spectralogic/ds3client/helpers/Ds3ClientHelpers.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.spectralogic.ds3client.helpers.options.WriteJobOptions;
2323
import com.spectralogic.ds3client.helpers.strategy.transferstrategy.TransferStrategy;
2424
import com.spectralogic.ds3client.models.Contents;
25+
import com.spectralogic.ds3client.models.FileSystemKey;
2526
import com.spectralogic.ds3client.models.bulk.Ds3Object;
2627
import com.spectralogic.ds3client.utils.Predicate;
2728

@@ -467,6 +468,46 @@ public abstract Iterable<Contents> listObjects(final String bucket, final String
467468
*/
468469
public abstract Iterable<Ds3Object> listObjectsForDirectory(final Path directory) throws IOException;
469470

471+
472+
/**
473+
* The '/' character is traditionally used as a path delimiter, these methods provide '/' as a default delimiter.
474+
* An S3 object like foo/bar/baz with the prefix of foo/ and the delimiter of '/' will act similar to a traditional file system
475+
* proving a common prefix for each 'directory' descending from the current folder of the prefix.
476+
* Returns an Iterable of {@link FileSystemKey}
477+
* @param bucket The bucket
478+
* @param keyPrefix Limits the response to keys that begin with the specified prefix
479+
*/
480+
public abstract Iterable<FileSystemKey> remoteListDirectory(final String bucket, final String keyPrefix) throws IOException;
481+
482+
/**
483+
* Defaults to the path delimiter of '/'
484+
* Returns an Iterable of {@link FileSystemKey}
485+
* @param bucket The bucket
486+
* @param keyPrefix Limits the response to keys that begin with the specified prefix
487+
* @param nextMarker Specifies the key to start with
488+
*/
489+
public abstract Iterable<FileSystemKey> remoteListDirectory(final String bucket, final String keyPrefix, final String nextMarker) throws IOException;
490+
491+
/**
492+
* Defaults to the path delimiter of '/'
493+
* Returns an Iterable of {@link FileSystemKey}
494+
* @param bucket The bucket
495+
* @param keyPrefix Limits the response to keys that begin with the specified prefix
496+
* @param nextMarker Specifies the key to start with when listing objects
497+
* @param maxKeys Sets the maximum number of keys returned in the response body.
498+
*/
499+
public abstract Iterable<FileSystemKey> remoteListDirectory(final String bucket, final String keyPrefix, final String nextMarker, final int maxKeys) throws IOException;
500+
501+
/**
502+
* Returns an Iterable of {@link FileSystemKey}
503+
* @param bucket The bucket
504+
* @param keyPrefix Limits the response to keys that begin with the specified prefix
505+
* @param delimiter Specifies a path delimiter for the S3 query
506+
* @param nextMarker Specifies the key to start with when listing objects
507+
* @param maxKeys Sets the maximum number of keys returned in the response body.
508+
*/
509+
public abstract Iterable<FileSystemKey> remoteListDirectory(final String bucket, final String keyPrefix, final String delimiter, final String nextMarker, final int maxKeys) throws IOException;
510+
470511
/**
471512
* Returns an Iterable of {@link Ds3Object} that have a prefix added.
472513
*/

ds3-sdk/src/main/java/com/spectralogic/ds3client/helpers/Ds3ClientHelpersImpl.java

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,23 @@
2222
import com.google.common.collect.ImmutableMultimap;
2323
import com.google.common.collect.Lists;
2424
import com.spectralogic.ds3client.Ds3Client;
25+
import com.spectralogic.ds3client.commands.HeadBucketRequest;
26+
import com.spectralogic.ds3client.commands.HeadBucketResponse;
27+
import com.spectralogic.ds3client.commands.PutBucketRequest;
28+
import com.spectralogic.ds3client.commands.spectrads3.*;
2529
import com.spectralogic.ds3client.helpers.events.EventRunner;
2630
import com.spectralogic.ds3client.helpers.events.SameThreadEventRunner;
27-
import com.spectralogic.ds3client.helpers.pagination.GetBucketLoaderFactory;
28-
import com.spectralogic.ds3client.commands.*;
29-
import com.spectralogic.ds3client.commands.spectrads3.*;
3031
import com.spectralogic.ds3client.helpers.options.ReadJobOptions;
3132
import com.spectralogic.ds3client.helpers.options.WriteJobOptions;
33+
import com.spectralogic.ds3client.helpers.pagination.GetBucketKeyLoaderFactory;
3234
import com.spectralogic.ds3client.helpers.strategy.transferstrategy.EventDispatcher;
3335
import com.spectralogic.ds3client.helpers.strategy.transferstrategy.EventDispatcherImpl;
3436
import com.spectralogic.ds3client.helpers.strategy.transferstrategy.TransferStrategy;
3537
import com.spectralogic.ds3client.helpers.strategy.transferstrategy.TransferStrategyBuilder;
3638
import com.spectralogic.ds3client.helpers.util.PartialObjectHelpers;
3739
import com.spectralogic.ds3client.models.*;
40+
import com.spectralogic.ds3client.models.bulk.Ds3Object;
3841
import com.spectralogic.ds3client.models.common.Range;
39-
import com.spectralogic.ds3client.models.bulk.*;
4042
import com.spectralogic.ds3client.networking.FailedRequestException;
4143
import com.spectralogic.ds3client.utils.collections.LazyIterable;
4244
import org.slf4j.Logger;
@@ -56,6 +58,7 @@
5658
class Ds3ClientHelpersImpl extends Ds3ClientHelpers {
5759
private final static Logger LOG = LoggerFactory.getLogger(Ds3ClientHelpersImpl.class);
5860
private final static int DEFAULT_LIST_OBJECTS_RETRIES = 5;
61+
public final static String DEFAULT_DELIMITER = null;
5962

6063
private final Ds3Client client;
6164
private final int maxChunkAttempts;
@@ -505,13 +508,13 @@ public Iterable<Contents> listObjects(final String bucket, final String keyPrefi
505508
@Override
506509
public Iterable<Contents> listObjects(final String bucket, final String keyPrefix, final String nextMarker, final int maxKeys) {
507510

508-
return new LazyIterable<>(new GetBucketLoaderFactory(client, bucket, keyPrefix, nextMarker, maxKeys, DEFAULT_LIST_OBJECTS_RETRIES));
511+
return this.listObjects(bucket, keyPrefix, nextMarker, maxKeys, DEFAULT_LIST_OBJECTS_RETRIES);
509512
}
510513

511514
@Override
512515
public Iterable<Contents> listObjects(final String bucket, final String keyPrefix, final String nextMarker, final int maxKeys, final int retries) {
513516

514-
return new LazyIterable<>(new GetBucketLoaderFactory(client, bucket, keyPrefix, nextMarker, maxKeys, retries));
517+
return new LazyIterable<>(new GetBucketKeyLoaderFactory<Contents>(client, bucket, keyPrefix, DEFAULT_DELIMITER, nextMarker, maxKeys, retries, GetBucketKeyLoaderFactory.contentsFunction));
515518
}
516519

517520
@Override
@@ -527,6 +530,24 @@ public FileVisitResult visitFile(final Path file, final BasicFileAttributes attr
527530
return objects.build();
528531
}
529532

533+
@Override
534+
public Iterable<FileSystemKey> remoteListDirectory(final String bucket, final String keyPrefix) throws IOException {
535+
return this.remoteListDirectory(bucket, keyPrefix, null);
536+
}
537+
538+
@Override
539+
public Iterable<FileSystemKey> remoteListDirectory(final String bucket, final String keyPrefix, final String nextMarker) throws IOException {
540+
return this.remoteListDirectory(bucket, keyPrefix, nextMarker,1000);
541+
}
542+
@Override
543+
public Iterable<FileSystemKey> remoteListDirectory(final String bucket, final String keyPrefix, final String nextMarker, final int maxKeys) throws IOException {
544+
return remoteListDirectory(bucket,keyPrefix,"/", keyPrefix, maxKeys);
545+
}
546+
547+
public Iterable<FileSystemKey> remoteListDirectory(final String bucket, final String keyPrefix, final String delimiter, final String nextMarker, final int maxKeys) throws IOException {
548+
return new LazyIterable<>(new GetBucketKeyLoaderFactory<FileSystemKey>(client, bucket, keyPrefix, delimiter, nextMarker, maxKeys, DEFAULT_LIST_OBJECTS_RETRIES, GetBucketKeyLoaderFactory.getFileSystemKeysFunction));
549+
}
550+
530551
public Iterable<Ds3Object> addPrefixToDs3ObjectsList(final Iterable<Ds3Object> objectsList, final String prefix) {
531552
final FluentIterable<Ds3Object> objectIterable = FluentIterable.from(objectsList);
532553

ds3-sdk/src/main/java/com/spectralogic/ds3client/helpers/pagination/GetBucketLoader.java renamed to ds3-sdk/src/main/java/com/spectralogic/ds3client/helpers/pagination/GetBucketKeyLoader.java

Lines changed: 36 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@
1212
* specific language governing permissions and limitations under the License.
1313
* ****************************************************************************
1414
*/
15-
1615
package com.spectralogic.ds3client.helpers.pagination;
1716

17+
import com.google.common.base.Function;
1818
import com.spectralogic.ds3client.Ds3Client;
1919
import com.spectralogic.ds3client.commands.GetBucketRequest;
2020
import com.spectralogic.ds3client.commands.GetBucketResponse;
21-
import com.spectralogic.ds3client.models.Contents;
2221
import com.spectralogic.ds3client.models.ListBucketResult;
2322
import com.spectralogic.ds3client.networking.FailedRequestException;
23+
import com.spectralogic.ds3client.networking.FailedToGetBucketException;
2424
import com.spectralogic.ds3client.networking.TooManyRetriesException;
2525
import com.spectralogic.ds3client.utils.Guard;
2626
import com.spectralogic.ds3client.utils.collections.LazyIterable;
@@ -29,69 +29,66 @@
2929
import java.util.Collections;
3030
import java.util.List;
3131

32-
public class GetBucketLoader implements LazyIterable.LazyLoader<Contents> {
32+
public class GetBucketKeyLoader<T> implements LazyIterable.LazyLoader<T> {
3333
private static final int DEFAULT_MAX_KEYS = 1000;
34-
34+
private final List<T> emptyList = Collections.emptyList();
3535
private final Ds3Client client;
3636
private final String bucket;
3737
private final String prefix;
38+
private final String delimiter;
3839
private final int maxKeys;
3940
private final int retryCount;
40-
41+
private final Function<ListBucketResult, Iterable<T>> function;
4142
private String nextMarker;
4243
private boolean truncated;
4344
private boolean endOfInput = false;
4445

45-
public GetBucketLoader(final Ds3Client client, final String bucket, final String prefix, final String nextMarker, final int maxKeys, final int retryCount) {
46+
GetBucketKeyLoader(final Ds3Client client, final String bucket, final String prefix, final String delimiter, final String nextMarker, final int maxKeys, final int retryCount, final Function<ListBucketResult, Iterable<T>> function) {
4647
this.client = client;
4748
this.bucket = bucket;
4849
this.prefix = prefix;
49-
this.maxKeys = maxKeys;
50+
this.delimiter = delimiter;
51+
this.nextMarker = nextMarker;
52+
this.maxKeys = Math.min(maxKeys, DEFAULT_MAX_KEYS);
5053
this.retryCount = retryCount;
51-
5254
this.nextMarker = nextMarker;
5355
this.truncated = nextMarker != null;
56+
this.function = function;
57+
}
58+
59+
private GetBucketRequest prepRequest() {
60+
final GetBucketRequest request = new GetBucketRequest(bucket);
61+
if (prefix != null) { request.withPrefix(prefix); }
62+
if (truncated) { request.withMarker(nextMarker); }
63+
if (delimiter != null) { request.withDelimiter(delimiter); }
64+
request.withMaxKeys(maxKeys);
65+
return request;
5466
}
5567

5668
@Override
57-
public List<Contents> getNextValues() {
58-
if (endOfInput) {
59-
return Collections.emptyList();
60-
}
69+
public Iterable<T> getNextValues() {
70+
if (endOfInput) { return emptyList; }
6171
int retryAttempt = 0;
62-
while(true) {
63-
final GetBucketRequest request = new GetBucketRequest(bucket);
64-
request.withMaxKeys(Math.min(maxKeys, DEFAULT_MAX_KEYS));
65-
if (prefix != null) {
66-
request.withPrefix(prefix);
67-
}
68-
if (truncated) {
69-
request.withMarker(nextMarker);
70-
}
71-
72+
while (retryCount > retryAttempt) {
73+
final GetBucketRequest request = prepRequest();
74+
final ListBucketResult result;
7275
final GetBucketResponse response;
7376
try {
7477
response = this.client.getBucket(request);
75-
final ListBucketResult result = response.getListBucketResult();
76-
77-
truncated = result.getTruncated();
78-
this.nextMarker = result.getNextMarker();
79-
80-
if (Guard.isStringNullOrEmpty(nextMarker) && !truncated) {
81-
endOfInput = true;
82-
}
83-
84-
return result.getObjects();
8578
} catch (final FailedRequestException e) {
86-
throw new RuntimeException("Failed to get the list of objects due to a failed request", e);
79+
throw new FailedToGetBucketException("Failed to get the list of objects due to a failed request", e);
8780
} catch (final IOException e) {
88-
if (retryAttempt >= retryCount) {
89-
//TODO need a proxied test to validate this retry logic
90-
throw new TooManyRetriesException("Failed to get the next set of objects from the getBucket request after " + retryCount + " retries", e);
91-
}
9281
retryAttempt++;
82+
continue;
9383
}
84+
result = response.getListBucketResult();
85+
this.truncated = result.getTruncated();
86+
this.nextMarker = result.getNextMarker();
87+
if (Guard.isStringNullOrEmpty(nextMarker) && !truncated) {
88+
endOfInput = true;
89+
}
90+
return function.apply(result);
9491
}
95-
92+
throw new TooManyRetriesException("Failed to get the next set of objects from the getBucketKey request after " + retryCount + " retries");
9693
}
97-
}
94+
}

0 commit comments

Comments
 (0)