Merge pull request #546 from chibenwa/JAMES-3544-list-buckets
JAMES-3544 Blob store should allow to list bucket
diff --git a/server/blob/blob-aes/src/main/java/org/apache/james/blob/aes/AESBlobStoreDAO.java b/server/blob/blob-aes/src/main/java/org/apache/james/blob/aes/AESBlobStoreDAO.java
index 8f45e0f..aa4a6b6 100644
--- a/server/blob/blob-aes/src/main/java/org/apache/james/blob/aes/AESBlobStoreDAO.java
+++ b/server/blob/blob-aes/src/main/java/org/apache/james/blob/aes/AESBlobStoreDAO.java
@@ -131,4 +131,9 @@
public Publisher<Void> deleteBucket(BucketName bucketName) {
return underlying.deleteBucket(bucketName);
}
+
+ @Override
+ public Publisher<BucketName> listBuckets() {
+ return underlying.listBuckets();
+ }
}
diff --git a/server/blob/blob-aes/src/test/java/org/apache/james/blob/aes/AESBlobStoreDAOTest.java b/server/blob/blob-aes/src/test/java/org/apache/james/blob/aes/AESBlobStoreDAOTest.java
index 8be3728..ad58cd0 100644
--- a/server/blob/blob-aes/src/test/java/org/apache/james/blob/aes/AESBlobStoreDAOTest.java
+++ b/server/blob/blob-aes/src/test/java/org/apache/james/blob/aes/AESBlobStoreDAOTest.java
@@ -30,6 +30,7 @@
import org.apache.james.blob.api.BlobStoreDAOContract;
import org.apache.james.blob.memory.MemoryBlobStoreDAO;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import com.google.common.io.ByteSource;
@@ -83,4 +84,10 @@
assertThat(bytes).isNotEqualTo(SHORT_BYTEARRAY);
}
+
+ @Override
+ @Disabled("Not supported by the Memory blob store")
+ public void listBucketsShouldReturnBucketsWithNoBlob() {
+
+ }
}
\ No newline at end of file
diff --git a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStore.java b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStore.java
index 3f7cd67..cf315d2 100644
--- a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStore.java
+++ b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStore.java
@@ -58,6 +58,8 @@
BucketName getDefaultBucketName();
+ Publisher<BucketName> listBuckets();
+
Publisher<Void> deleteBucket(BucketName bucketName);
Publisher<Boolean> delete(BucketName bucketName, BlobId blobId);
diff --git a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java
index f0e2844..5288284 100644
--- a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java
+++ b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java
@@ -99,4 +99,6 @@
* otherwise an IOObjectStoreException in its error channel
*/
Publisher<Void> deleteBucket(BucketName bucketName);
+
+ Publisher<BucketName> listBuckets();
}
diff --git a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/MetricableBlobStore.java b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/MetricableBlobStore.java
index 8c57fec..4b104ac 100644
--- a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/MetricableBlobStore.java
+++ b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/MetricableBlobStore.java
@@ -102,4 +102,9 @@
return metricFactory.decoratePublisherWithTimerMetric(DELETE_TIMER_NAME, blobStoreImpl.delete(bucketName, blobId));
}
+
+ @Override
+ public Publisher<BucketName> listBuckets() {
+ return blobStoreImpl.listBuckets();
+ }
}
diff --git a/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreContract.java b/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreContract.java
index 75b0706..e616ee2 100644
--- a/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreContract.java
+++ b/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreContract.java
@@ -31,6 +31,7 @@
import org.apache.james.util.concurrency.ConcurrentTestRunner;
import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface BucketBlobStoreContract {
@@ -168,4 +169,33 @@
.operationCount(10)
.runSuccessfullyWithin(Duration.ofMinutes(1));
}
+
+ @Test
+ default void listBucketsShouldReturnDefaultBucket() {
+ BlobStore store = testee();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .containsOnly(store.getDefaultBucketName());
+ }
+
+ @Test
+ default void listBucketsShouldReturnACustomBucket() {
+ BlobStore store = testee();
+
+ Mono.from(store.save(CUSTOM, SHORT_BYTEARRAY, LOW_COST)).block();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .containsOnly(store.getDefaultBucketName(), CUSTOM);
+ }
+
+ @Test
+ default void listBucketsShouldNotReturnADeletedBucket() {
+ BlobStore store = testee();
+
+ Mono.from(store.save(CUSTOM, SHORT_BYTEARRAY, LOW_COST)).block();
+ Mono.from(store.deleteBucket(CUSTOM)).block();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .containsOnly(store.getDefaultBucketName());
+ }
}
diff --git a/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreDAOContract.java b/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreDAOContract.java
index acb4fe8..eb71a37 100644
--- a/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreDAOContract.java
+++ b/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreDAOContract.java
@@ -35,6 +35,7 @@
import org.apache.james.util.concurrency.ConcurrentTestRunner;
import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface BucketBlobStoreDAOContract {
@@ -172,4 +173,68 @@
.operationCount(10)
.runSuccessfullyWithin(Duration.ofMinutes(1));
}
+
+ @Test
+ default void listBucketsShouldReturnEmptyWhenNone() {
+ BlobStoreDAO store = testee();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .isEmpty();
+ }
+
+ @Test
+ default void listBucketsShouldReturnBucketInUse() {
+ BlobStoreDAO store = testee();
+
+ Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .containsOnly(TEST_BUCKET_NAME);
+ }
+
+ @Test
+ default void listBucketsShouldNotReturnDuplicates() {
+ BlobStoreDAO store = testee();
+
+ Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+ Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .hasSize(1);
+ }
+
+ @Test
+ default void listBucketsShouldReturnAllBucketsInUse() {
+ BlobStoreDAO store = testee();
+
+ Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+ Mono.from(store.save(CUSTOM_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .containsOnly(TEST_BUCKET_NAME, CUSTOM_BUCKET_NAME);
+ }
+
+ @Test
+ default void listBucketsShouldNotReturnDeletedBuckets() {
+ BlobStoreDAO store = testee();
+
+ Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+ Mono.from(store.deleteBucket(TEST_BUCKET_NAME)).block();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .isEmpty();
+ }
+
+ @Test
+ default void listBucketsShouldReturnBucketsWithNoBlob() {
+ BlobStoreDAO store = testee();
+
+ Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+ Mono.from(store.delete(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .containsOnly(TEST_BUCKET_NAME);
+ }
}
diff --git a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAO.java b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAO.java
index ffcc179..b27ec29 100644
--- a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAO.java
+++ b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAO.java
@@ -42,6 +42,7 @@
import org.apache.james.metrics.api.MetricFactory;
import org.apache.james.util.DataChunker;
import org.apache.james.util.ReactorUtils;
+import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -69,7 +70,6 @@
private final CassandraConfiguration configuration;
private final BucketName defaultBucket;
- private final MetricFactory metricFactory;
private final Metric metricClOneHitCount;
private final Metric metricClOneMissCount;
@@ -84,7 +84,6 @@
this.bucketDAO = bucketDAO;
this.configuration = cassandraConfiguration;
this.defaultBucket = defaultBucket;
- this.metricFactory = metricFactory;
this.metricClOneMissCount = metricFactory.generate(CASSANDRA_BLOBSTORE_CL_ONE_MISS_COUNT_METRIC_NAME);
this.metricClOneHitCount = metricFactory.generate(CASSANDRA_BLOBSTORE_CL_ONE_HIT_COUNT_METRIC_NAME);
@@ -269,4 +268,11 @@
.reduce(ByteBuffer.allocate(targetSize), ByteBuffer::put)
.array();
}
+
+ @Override
+ public Publisher<BucketName> listBuckets() {
+ return bucketDAO.listAll()
+ .map(Pair::getLeft)
+ .distinct();
+ }
}
diff --git a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CachedBlobStore.java b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CachedBlobStore.java
index 9b6d0b9..fb74113 100644
--- a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CachedBlobStore.java
+++ b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CachedBlobStore.java
@@ -322,4 +322,9 @@
return Mono.from(metricFactory.decoratePublisherWithTimerMetric(BLOBSTORE_BACKEND_LATENCY_METRIC_NAME,
backend.readBytes(bucketName, blobId)));
}
+
+ @Override
+ public Publisher<BucketName> listBuckets() {
+ return backend.listBuckets();
+ }
}
diff --git a/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAOTest.java b/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAOTest.java
index 6b2ad1d..159d5a1 100644
--- a/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAOTest.java
+++ b/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAOTest.java
@@ -28,11 +28,11 @@
import org.apache.james.blob.api.HashBlobId;
import org.apache.james.metrics.tests.RecordingMetricFactory;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.extension.RegisterExtension;
public class CassandraBlobStoreDAOTest implements BlobStoreDAOContract {
private static final int CHUNK_SIZE = 10240;
- private static final int MULTIPLE_CHUNK_SIZE = 3;
@RegisterExtension
static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension(CassandraBlobModule.MODULE);
@@ -60,4 +60,9 @@
return testee;
}
+ @Override
+ @Disabled("Not supported by the Cassandra blob store")
+ public void listBucketsShouldReturnBucketsWithNoBlob() {
+
+ }
}
\ No newline at end of file
diff --git a/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java b/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java
index f66cc1f..95b256f 100644
--- a/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java
+++ b/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java
@@ -29,12 +29,14 @@
import org.apache.james.blob.api.BucketName;
import org.apache.james.blob.api.ObjectNotFoundException;
import org.apache.james.blob.api.ObjectStoreIOException;
+import org.reactivestreams.Publisher;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.google.common.io.ByteSource;
+import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public class MemoryBlobStoreDAO implements BlobStoreDAO {
@@ -110,4 +112,9 @@
}
});
}
+
+ @Override
+ public Publisher<BucketName> listBuckets() {
+ return Flux.fromIterable(blobs.rowKeySet());
+ }
}
diff --git a/server/blob/blob-memory/src/test/java/org/apache/james/blob/memory/MemoryBlobStoreDAOTest.java b/server/blob/blob-memory/src/test/java/org/apache/james/blob/memory/MemoryBlobStoreDAOTest.java
index 3225301..83748a7 100644
--- a/server/blob/blob-memory/src/test/java/org/apache/james/blob/memory/MemoryBlobStoreDAOTest.java
+++ b/server/blob/blob-memory/src/test/java/org/apache/james/blob/memory/MemoryBlobStoreDAOTest.java
@@ -22,6 +22,7 @@
import org.apache.james.blob.api.BlobStoreDAO;
import org.apache.james.blob.api.BlobStoreDAOContract;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
class MemoryBlobStoreDAOTest implements BlobStoreDAOContract {
@@ -36,4 +37,10 @@
public BlobStoreDAO testee() {
return blobStore;
}
+
+ @Override
+ @Disabled("Not supported")
+ public void listBucketsShouldReturnBucketsWithNoBlob() {
+
+ }
}
diff --git a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/BucketNameResolver.java b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/BucketNameResolver.java
index a0fcb71..7b13ad3 100644
--- a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/BucketNameResolver.java
+++ b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/BucketNameResolver.java
@@ -95,6 +95,19 @@
.orElse(bucketName);
}
+ Optional<BucketName> unresolve(BucketName bucketName) {
+ if (isNameSpace(bucketName)) {
+ return Optional.of(bucketName);
+ }
+
+ return prefix.map(p -> {
+ if (bucketName.asString().startsWith(p)) {
+ return Optional.of(BucketName.of(bucketName.asString().substring(p.length())));
+ }
+ return Optional.<BucketName>empty();
+ }).orElse(Optional.of(bucketName));
+ }
+
private boolean isNameSpace(BucketName bucketName) {
return namespace
.map(existingNamespace -> existingNamespace.equals(bucketName))
diff --git a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
index 5849bf9..24a35a4 100644
--- a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
+++ b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
@@ -41,6 +41,7 @@
import org.apache.james.lifecycle.api.Startable;
import org.apache.james.util.DataChunker;
import org.apache.james.util.ReactorUtils;
+import org.reactivestreams.Publisher;
import com.github.fge.lambdas.Throwing;
import com.github.steveash.guavate.Guavate;
@@ -63,11 +64,11 @@
import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.S3Configuration;
+import software.amazon.awssdk.services.s3.model.Bucket;
import software.amazon.awssdk.services.s3.model.BucketAlreadyOwnedByYouException;
import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.ListBucketsResponse;
-import software.amazon.awssdk.services.s3.model.ListObjectsResponse;
import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
import software.amazon.awssdk.services.s3.model.ObjectIdentifier;
@@ -279,10 +280,13 @@
private Mono<BucketName> emptyBucket(BucketName bucketName) {
return Mono.fromFuture(() -> client.listObjects(builder -> builder.bucket(bucketName.asString())))
- .flatMapIterable(ListObjectsResponse::contents)
- .window(EMPTY_BUCKET_BATCH_SIZE)
- .flatMap(this::buildListForBatch, DEFAULT_CONCURRENCY)
- .flatMap(identifiers -> deleteObjects(bucketName, identifiers), DEFAULT_CONCURRENCY)
+ .flatMap(response -> Flux.fromIterable(response.contents())
+ .window(EMPTY_BUCKET_BATCH_SIZE)
+ .flatMap(this::buildListForBatch, DEFAULT_CONCURRENCY)
+ .flatMap(identifiers -> deleteObjects(bucketName, identifiers), DEFAULT_CONCURRENCY)
+ .then(Mono.just(response)))
+ .flux()
+ .takeUntil(list -> !list.isTruncated())
.then(Mono.just(bucketName));
}
@@ -305,4 +309,13 @@
.flatMap(bucket -> deleteResolvedBucket(BucketName.of(bucket.name())), DEFAULT_CONCURRENCY)
.then();
}
+
+ @Override
+ public Publisher<BucketName> listBuckets() {
+ return Mono.fromFuture(client::listBuckets)
+ .flatMapIterable(ListBucketsResponse::buckets)
+ .map(Bucket::name)
+ .handle((bucket, sink) -> bucketNameResolver.unresolve(BucketName.of(bucket))
+ .ifPresent(sink::next));
+ }
}
diff --git a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/BucketNameResolverTest.java b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/BucketNameResolverTest.java
index 43e9e84..178830d 100644
--- a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/BucketNameResolverTest.java
+++ b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/BucketNameResolverTest.java
@@ -25,10 +25,25 @@
import org.apache.james.blob.api.BucketName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
class BucketNameResolverTest {
@Nested
class EmptyPrefix {
+ @ParameterizedTest
+ @ValueSource(strings = {"namespace", "any", "bucketPrefix-aaa", "bucketPrefix-"})
+ void withShouldAddNewValuesInSet(String bucketNameString) {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .noPrefix()
+ .namespace(BucketName.of("namespace"))
+ .build();
+
+ BucketName bucketName = BucketName.of(bucketNameString);
+ assertThat(resolver.unresolve(resolver.resolve(bucketName)))
+ .contains(bucketName);
+ }
+
@Test
void resolveShouldReturnPassedValue() {
BucketNameResolver resolver = BucketNameResolver.builder()
@@ -50,10 +65,45 @@
assertThat(resolver.resolve(BucketName.of("namespace")))
.isEqualTo(BucketName.of("namespace"));
}
+
+ @Test
+ void unresolveShouldReturnPassedValue() {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .noPrefix()
+ .namespace(BucketName.of("namespace"))
+ .build();
+
+ assertThat(resolver.unresolve(BucketName.of("bucketName")))
+ .contains(BucketName.of("bucketName"));
+ }
+
+ @Test
+ void unresolveShouldReturnValueWhenNamespace() {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .noPrefix()
+ .namespace(BucketName.of("namespace"))
+ .build();
+
+ assertThat(resolver.unresolve(BucketName.of("namespace")))
+ .contains(BucketName.of("namespace"));
+ }
}
@Nested
class EmptyNamespace {
+ @ParameterizedTest
+ @ValueSource(strings = {"namespace", "any", "bucketPrefix-aaa", "bucketPrefix-"})
+ void withShouldAddNewValuesInSet(String bucketNameString) {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .prefix("bucketPrefix-")
+ .noNamespace()
+ .build();
+
+ BucketName bucketName = BucketName.of(bucketNameString);
+ assertThat(resolver.unresolve(resolver.resolve(bucketName)))
+ .contains(bucketName);
+ }
+
@Test
void resolveShouldReturnPassedValueWithPrefix() {
BucketNameResolver resolver = BucketNameResolver.builder()
@@ -64,10 +114,45 @@
assertThat(resolver.resolve(BucketName.of("bucketName")))
.isEqualTo(BucketName.of("bucketPrefix-bucketName"));
}
+
+ @Test
+ void unresolveShouldReturnPassedValueWithPrefix() {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .prefix("bucketPrefix-")
+ .noNamespace()
+ .build();
+
+ assertThat(resolver.unresolve(BucketName.of("bucketPrefix-bucketName")))
+ .contains(BucketName.of("bucketName"));
+ }
+
+ @Test
+ void unresolveShouldFilterValuesWithoutPrefix() {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .prefix("bucketPrefix-")
+ .noNamespace()
+ .build();
+
+ assertThat(resolver.unresolve(BucketName.of("bucketName")))
+ .isEmpty();
+ }
}
@Nested
class BothAreEmpty {
+ @ParameterizedTest
+ @ValueSource(strings = {"namespace", "any", "bucketPrefix-aaa", "bucketPrefix-"})
+ void withShouldAddNewValuesInSet(String bucketNameString) {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .noPrefix()
+ .noNamespace()
+ .build();
+
+ BucketName bucketName = BucketName.of(bucketNameString);
+ assertThat(resolver.unresolve(resolver.resolve(bucketName)))
+ .contains(bucketName);
+ }
+
@Test
void resolveShouldReturnPassedValue() {
BucketNameResolver resolver = BucketNameResolver.builder()
@@ -78,10 +163,35 @@
assertThat(resolver.resolve(BucketName.of("bucketName")))
.isEqualTo(BucketName.of("bucketName"));
}
+
+ @Test
+ void unresolveShouldReturnPassedValue() {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .noPrefix()
+ .noNamespace()
+ .build();
+
+ assertThat(resolver.unresolve(BucketName.of("bucketName")))
+ .contains(BucketName.of("bucketName"));
+ }
}
@Nested
class BothArePresent {
+
+ @ParameterizedTest
+ @ValueSource(strings = {"namespace", "any", "bucketPrefix-aaa", "bucketPrefix-"})
+ void withShouldAddNewValuesInSet(String bucketNameString) {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .prefix("bucketPrefix-")
+ .namespace(BucketName.of("namespace"))
+ .build();
+
+ BucketName bucketName = BucketName.of(bucketNameString);
+ assertThat(resolver.unresolve(resolver.resolve(bucketName)))
+ .contains(bucketName);
+ }
+
@Test
void resolveShouldReturnPassedValueWithPrefix() {
BucketNameResolver resolver = BucketNameResolver.builder()
@@ -103,6 +213,39 @@
assertThat(resolver.resolve(BucketName.of("namespace")))
.isEqualTo(BucketName.of("namespace"));
}
+
+ @Test
+ void unresolveShouldFilterValuesWithoutPrefix() {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .prefix("bucketPrefix-")
+ .namespace(BucketName.of("namespace"))
+ .build();
+
+ assertThat(resolver.unresolve(BucketName.of("bucketName")))
+ .isEmpty();
+ }
+
+ @Test
+ void unresolveShouldRemovePrefix() {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .prefix("bucketPrefix-")
+ .namespace(BucketName.of("namespace"))
+ .build();
+
+ assertThat(resolver.unresolve(BucketName.of("bucketPrefix-bucketName")))
+ .contains(BucketName.of("bucketName"));
+ }
+
+ @Test
+ void unresolveShouldReturnNamespaceWhenPassingNamespace() {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .prefix("bucketPrefix-")
+ .namespace(BucketName.of("namespace"))
+ .build();
+
+ assertThat(resolver.unresolve(BucketName.of("namespace")))
+ .contains(BucketName.of("namespace"));
+ }
}
diff --git a/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/DeDuplicationBlobStore.scala b/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/DeDuplicationBlobStore.scala
index 618beb4..15148c2 100644
--- a/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/DeDuplicationBlobStore.scala
+++ b/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/DeDuplicationBlobStore.scala
@@ -29,7 +29,7 @@
import org.apache.commons.io.IOUtils
import org.apache.james.blob.api.{BlobId, BlobStore, BlobStoreDAO, BucketName}
import org.reactivestreams.Publisher
-import reactor.core.publisher.Mono
+import reactor.core.publisher.{Flux, Mono}
import reactor.core.scala.publisher.SMono
import reactor.util.function.{Tuple2, Tuples}
@@ -112,4 +112,6 @@
SMono.just(Boolean.box(false))
}
+
+ override def listBuckets(): Publisher[BucketName] = Flux.concat(blobStoreDAO.listBuckets(), Flux.just(defaultBucketName)).distinct()
}
diff --git a/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/PassThroughBlobStore.scala b/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/PassThroughBlobStore.scala
index 6cb73e4..faf4a5d 100644
--- a/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/PassThroughBlobStore.scala
+++ b/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/PassThroughBlobStore.scala
@@ -26,6 +26,7 @@
import javax.inject.{Inject, Named}
import org.apache.james.blob.api.{BlobId, BlobStore, BlobStoreDAO, BucketName}
import org.reactivestreams.Publisher
+import reactor.core.publisher.Flux
import reactor.core.scala.publisher.SMono
@@ -86,4 +87,6 @@
SMono.fromPublisher(blobStoreDAO.delete(bucketName, blobId))
.`then`(SMono.just(Boolean.box(true)))
}
+
+ override def listBuckets(): Publisher[BucketName] = Flux.concat(blobStoreDAO.listBuckets(), Flux.just(defaultBucketName)).distinct()
}