From 4abe96958bab71e56443b668034ca3e4b69de842 Mon Sep 17 00:00:00 2001 From: Benoit TELLIER Date: Fri, 21 Aug 2026 16:03:38 +0700 Subject: [PATCH] [FIX] S3: Fallback bucket should not apply bucket prefix --- .../servers/partials/configure/blobstore.adoc | 2 ++ .../helm-chart/james/configs/blob.properties | 4 +++- .../sample-configuration/blob.properties | 4 +++- .../sample-configuration/blob.properties | 4 +++- .../blob.properties | 4 +++- .../sample-configuration/blob.properties | 4 +++- .../objectstorage/aws/S3BlobStoreDAO.java | 6 ++--- .../objectstorage/aws/S3BlobStoreDAOTest.java | 24 +++++++++++++++++++ 8 files changed, 43 insertions(+), 9 deletions(-) diff --git a/docs/modules/servers/partials/configure/blobstore.adoc b/docs/modules/servers/partials/configure/blobstore.adoc index 8d0535ddc28..44d992c36ec 100644 --- a/docs/modules/servers/partials/configure/blobstore.adoc +++ b/docs/modules/servers/partials/configure/blobstore.adoc @@ -166,6 +166,8 @@ Unless a special case like storing blobs of deleted messages. | objectstorage.namespace.read.fallback | BlobStore fallback bucket name. Allows to fallback to a previous used bucket when blob is missing from the default one. It can be useful when migrating blobs to a new bucket for example. +This is a read only fallback: James never writes to, nor deletes from, that bucket. +This bucket name is used as is: `objectstorage.bucketPrefix` is not applied to it. |=== ==== SSE-C Configuration diff --git a/server/apps/distributed-app/helm-chart/james/configs/blob.properties b/server/apps/distributed-app/helm-chart/james/configs/blob.properties index 25cc5793e1f..7de3823583d 100644 --- a/server/apps/distributed-app/helm-chart/james/configs/blob.properties +++ b/server/apps/distributed-app/helm-chart/james/configs/blob.properties @@ -42,7 +42,9 @@ cache.sizeThresholdInBytes=16 KiB objectstorage.namespace=james-${env:JAMES_BUCKET_SUFFIX} # Fallback bucket name -# Optional, read this bucket when default bukcket reads fails if configured +# Optional, read this bucket when reads on the default bucket fail, if configured. +# This is a read only fallback: blobs are never written to nor deleted from this bucket. +# Note: this name is used as is, `objectstorage.bucketPrefix` is not applied to it. # objectstorage.namespace.read.fallback=james-fallback # ========================================= ObjectStorage on S3 ============================================= diff --git a/server/apps/distributed-app/sample-configuration/blob.properties b/server/apps/distributed-app/sample-configuration/blob.properties index 9c21f6e8cee..f5d781b5a36 100644 --- a/server/apps/distributed-app/sample-configuration/blob.properties +++ b/server/apps/distributed-app/sample-configuration/blob.properties @@ -88,7 +88,9 @@ cache.enable=false # objectstorage.namespace=james # Fallback bucket name -# Optional, read this bucket when default bukcket reads fails if configured +# Optional, read this bucket when reads on the default bucket fail, if configured. +# This is a read only fallback: blobs are never written to nor deleted from this bucket. +# Note: this name is used as is, `objectstorage.bucketPrefix` is not applied to it. # objectstorage.namespace.read.fallback=james-fallback # ========================================= ObjectStorage on S3 ============================================= diff --git a/server/apps/distributed-pop3-app/sample-configuration/blob.properties b/server/apps/distributed-pop3-app/sample-configuration/blob.properties index 311103c4f4c..b6037003941 100644 --- a/server/apps/distributed-pop3-app/sample-configuration/blob.properties +++ b/server/apps/distributed-pop3-app/sample-configuration/blob.properties @@ -58,7 +58,9 @@ cache.enable=false # objectstorage.namespace=james # Fallback bucket name -# Optional, read this bucket when default bukcket reads fails if configured +# Optional, read this bucket when reads on the default bucket fail, if configured. +# This is a read only fallback: blobs are never written to nor deleted from this bucket. +# Note: this name is used as is, `objectstorage.bucketPrefix` is not applied to it. # objectstorage.namespace.read.fallback=james-fallback # ========================================= ObjectStorage on S3 ============================================= diff --git a/server/apps/postgres-app/sample-configuration-distributed/blob.properties b/server/apps/postgres-app/sample-configuration-distributed/blob.properties index 3f210e0acbe..25c32dd5828 100644 --- a/server/apps/postgres-app/sample-configuration-distributed/blob.properties +++ b/server/apps/postgres-app/sample-configuration-distributed/blob.properties @@ -50,7 +50,9 @@ encryption.aes.enable=false # objectstorage.namespace=james # Fallback bucket name -# Optional, read this bucket when default bukcket reads fails if configured +# Optional, read this bucket when reads on the default bucket fail, if configured. +# This is a read only fallback: blobs are never written to nor deleted from this bucket. +# Note: this name is used as is, `objectstorage.bucketPrefix` is not applied to it. # objectstorage.namespace.read.fallback=james-fallback # ========================================= ObjectStorage on S3 ============================================= diff --git a/server/apps/scaling-pulsar-smtp/sample-configuration/blob.properties b/server/apps/scaling-pulsar-smtp/sample-configuration/blob.properties index 15549d32856..4a1ed250aca 100644 --- a/server/apps/scaling-pulsar-smtp/sample-configuration/blob.properties +++ b/server/apps/scaling-pulsar-smtp/sample-configuration/blob.properties @@ -68,7 +68,9 @@ cache.enable=false # objectstorage.namespace=james # Fallback bucket name -# Optional, read this bucket when default bukcket reads fails if configured +# Optional, read this bucket when reads on the default bucket fail, if configured. +# This is a read only fallback: blobs are never written to nor deleted from this bucket. +# Note: this name is used as is, `objectstorage.bucketPrefix` is not applied to it. # objectstorage.namespace.read.fallback=james-fallback # ========================================= ObjectStorage on S3 ============================================= 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 5176e474294..f6307d1590d 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 @@ -173,8 +173,7 @@ private Mono getObject(BucketName bucketName, BlobId blobId) { return getObjectFromStore(bucketName, blobId) .onErrorResume(e -> e instanceof NoSuchKeyException || e instanceof NoSuchBucketException, e -> { if (fallbackNamespace.isPresent() && bucketNameResolver.isNameSpace(bucketName)) { - BucketName resolvedFallbackBucketName = bucketNameResolver.resolve(fallbackNamespace.get()); - return getObjectFromStore(resolvedFallbackBucketName, blobId); + return getObjectFromStore(fallbackNamespace.get(), blobId); } return Mono.error(e); }); @@ -229,8 +228,7 @@ private Mono> getObjectBytes(BucketName bucketN return getObjectBytesFromStore(bucketName, blobId) .onErrorResume(e -> e instanceof NoSuchKeyException || e instanceof NoSuchBucketException, e -> { if (fallbackNamespace.isPresent() && bucketNameResolver.isNameSpace(bucketName)) { - BucketName resolvedFallbackBucketName = bucketNameResolver.resolve(fallbackNamespace.get()); - return getObjectBytesFromStore(resolvedFallbackBucketName, blobId); + return getObjectBytesFromStore(fallbackNamespace.get(), blobId); } return Mono.error(e); }); diff --git a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAOTest.java b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAOTest.java index 0c271a9e953..53c22c08a79 100644 --- a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAOTest.java +++ b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAOTest.java @@ -162,6 +162,30 @@ void readBytesShouldFallbackToDefinedBucketWhenFailingOnDefaultOne() { assertThat(bytes).isEqualTo(ELEVEN_KILOBYTES); } + @Test + void fallbackBucketNameShouldNotBeAffectedByBucketPrefix(DockerAwsS3Container dockerAwsS3) { + S3BlobStoreConfiguration prefixedConfiguration = S3BlobStoreConfiguration.builder() + .authConfiguration(AwsS3AuthConfiguration.builder() + .endpoint(dockerAwsS3.getEndpoint()) + .accessKeyId(DockerAwsS3Container.ACCESS_KEY_ID) + .secretKey(DockerAwsS3Container.SECRET_ACCESS_KEY) + .build()) + .region(dockerAwsS3.dockerAwsS3().region()) + .defaultBucketName(BucketName.DEFAULT) + .bucketPrefix("prefix-") + .fallbackBucketName(Optional.of(fallbackBucket)) + .build(); + S3BlobStoreDAO prefixedStore = new S3BlobStoreDAO(s3ClientFactory, prefixedConfiguration, new TestBlobId.Factory(), S3RequestOption.DEFAULT); + + TestBlobId blobId = new TestBlobId("id"); + // Save in the un-prefixed fallback bucket + Mono.from(testee.save(fallbackBucket, blobId, ELEVEN_KILOBYTES)).block(); + + InputStream read = prefixedStore.read(BucketName.DEFAULT, blobId).payload(); + + assertThat(read).hasSameContentAs(ELEVEN_KILOBYTES.asInputStream().payload()); + } + @Test void shouldNotReadOnFallbackBucketWhenNotReadingOnDefaultOne() { BlobStoreDAO store = testee();