Skip to content

Commit a6ad1d7

Browse files
authored
blobstore: Implement the DoesBucketExist for Blob (#195)
1 parent 599a538 commit a6ad1d7

File tree

26 files changed

+518
-0
lines changed

26 files changed

+518
-0
lines changed

blob/blob-ali/src/main/java/com/salesforce/multicloudj/blob/ali/AliBlobStore.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,24 @@ protected boolean doDoesObjectExist(String key, String versionId) {
475475
return ossClient.doesObjectExist(transformer.toMetadataRequest(key, versionId));
476476
}
477477

478+
/**
479+
* Determines if the bucket exists
480+
* @return Returns true if the bucket exists. Returns false if it doesn't exist.
481+
*/
482+
@Override
483+
protected boolean doDoesBucketExist() {
484+
try {
485+
return ossClient.doesBucketExist(bucket);
486+
} catch (ServiceException e) {
487+
if ("NoSuchBucket".equals(e.getErrorCode())) {
488+
return false;
489+
}
490+
throw new SubstrateSdkException("Failed to check bucket existence", e);
491+
} catch (ClientException e) {
492+
throw new SubstrateSdkException("Failed to check bucket existence", e);
493+
}
494+
}
495+
478496
/**
479497
* Closes the underlying OSS client and releases any resources.
480498
*/

blob/blob-ali/src/test/java/com/salesforce/multicloudj/blob/ali/AliBlobStoreTest.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.aliyun.oss.OSS;
66
import com.aliyun.oss.OSSClientBuilder;
77
import com.aliyun.oss.OSSException;
8+
import com.aliyun.oss.ServiceException;
89
import com.aliyun.oss.internal.OSSHeaders;
910
import com.aliyun.oss.model.AbortMultipartUploadRequest;
1011
import com.aliyun.oss.model.CompleteMultipartUploadRequest;
@@ -84,12 +85,14 @@
8485
import java.util.stream.IntStream;
8586

8687
import static org.junit.jupiter.api.Assertions.assertEquals;
88+
import static org.junit.jupiter.api.Assertions.assertFalse;
8789
import static org.junit.jupiter.api.Assertions.assertNotNull;
8890
import static org.junit.jupiter.api.Assertions.assertNull;
8991
import static org.junit.jupiter.api.Assertions.assertThrows;
9092
import static org.junit.jupiter.api.Assertions.assertTrue;
9193
import static org.mockito.ArgumentMatchers.any;
9294
import static org.mockito.Mockito.doReturn;
95+
import static org.mockito.Mockito.doThrow;
9396
import static org.mockito.Mockito.mock;
9497
import static org.mockito.Mockito.mockStatic;
9598
import static org.mockito.Mockito.times;
@@ -783,6 +786,57 @@ void testDoDoesObjectExist() {
783786
assertTrue(result);
784787
}
785788

789+
@Test
790+
void testDoDoesBucketExist() {
791+
doReturn(true).when(mockOssClient).doesBucketExist("bucket-1");
792+
793+
boolean result = ali.doDoesBucketExist();
794+
795+
verify(mockOssClient, times(1)).doesBucketExist("bucket-1");
796+
assertTrue(result);
797+
}
798+
799+
@Test
800+
void testDoDoesBucketExist_BucketDoesNotExist() {
801+
doReturn(false).when(mockOssClient).doesBucketExist("bucket-1");
802+
803+
boolean result = ali.doDoesBucketExist();
804+
805+
verify(mockOssClient, times(1)).doesBucketExist("bucket-1");
806+
assertFalse(result);
807+
}
808+
809+
@Test
810+
void testDoDoesBucketExist_ThrowsNoSuchBucketException() {
811+
com.aliyun.oss.ServiceException serviceException = mock(com.aliyun.oss.ServiceException.class);
812+
doReturn("NoSuchBucket").when(serviceException).getErrorCode();
813+
doThrow(serviceException).when(mockOssClient).doesBucketExist("bucket-1");
814+
815+
boolean result = ali.doDoesBucketExist();
816+
817+
verify(mockOssClient, times(1)).doesBucketExist("bucket-1");
818+
assertFalse(result);
819+
}
820+
821+
@Test
822+
void testDoDoesBucketExist_ThrowsOtherServiceException() {
823+
com.aliyun.oss.ServiceException serviceException = mock(com.aliyun.oss.ServiceException.class);
824+
doReturn("AccessDenied").when(serviceException).getErrorCode();
825+
doThrow(serviceException).when(mockOssClient).doesBucketExist("bucket-1");
826+
827+
assertThrows(com.salesforce.multicloudj.common.exceptions.SubstrateSdkException.class, () -> ali.doDoesBucketExist());
828+
verify(mockOssClient, times(1)).doesBucketExist("bucket-1");
829+
}
830+
831+
@Test
832+
void testDoDoesBucketExist_ThrowsClientException() {
833+
ClientException clientException = mock(ClientException.class);
834+
doThrow(clientException).when(mockOssClient).doesBucketExist("bucket-1");
835+
836+
assertThrows(com.salesforce.multicloudj.common.exceptions.SubstrateSdkException.class, () -> ali.doDoesBucketExist());
837+
verify(mockOssClient, times(1)).doesBucketExist("bucket-1");
838+
}
839+
786840
private UploadRequest getTestUploadRequest() {
787841
Map<String, String> metadata = Map.of("key-1", "value-1");
788842
Map<String, String> tags = Map.of("tag-1", "tag-value-1");

blob/blob-aws/src/main/java/com/salesforce/multicloudj/blob/aws/AwsBlobStore.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
5252
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
5353
import software.amazon.awssdk.services.s3.model.GetObjectTaggingResponse;
54+
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
5455
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
5556
import software.amazon.awssdk.services.s3.model.HeadObjectResponse;
5657
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
@@ -488,6 +489,20 @@ protected boolean doDoesObjectExist(String key, String versionId) {
488489
}
489490
}
490491

492+
@Override
493+
protected boolean doDoesBucketExist() {
494+
try {
495+
s3Client.headBucket(builder -> builder.bucket(bucket));
496+
return true;
497+
}
498+
catch(S3Exception e) {
499+
if (e.statusCode() == 404) {
500+
return false;
501+
}
502+
throw e;
503+
}
504+
}
505+
491506
/**
492507
* Closes the underlying S3 client and releases any resources.
493508
*/

blob/blob-aws/src/main/java/com/salesforce/multicloudj/blob/aws/async/AwsAsyncBlobStore.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,20 @@ protected CompletableFuture<Boolean> doDoesObjectExist(String key, String versio
403403
});
404404
}
405405

406+
@Override
407+
protected CompletableFuture<Boolean> doDoesBucketExist() {
408+
return client
409+
.headBucket(builder -> builder.bucket(bucket))
410+
.thenApply(response -> true)
411+
.exceptionally(e -> {
412+
if (e.getCause() instanceof S3Exception && ((S3Exception) e.getCause()).statusCode() == 404) {
413+
return false;
414+
} else {
415+
throw new SubstrateSdkException("Request failed. Reason=" + e.getMessage(), e);
416+
}
417+
});
418+
}
419+
406420
/**
407421
* Closes the underlying S3 async client and transfer manager, releasing any resources.
408422
*/

blob/blob-aws/src/test/java/com/salesforce/multicloudj/blob/aws/AwsBlobStoreTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
6060
import software.amazon.awssdk.services.s3.model.GetObjectTaggingRequest;
6161
import software.amazon.awssdk.services.s3.model.GetObjectTaggingResponse;
62+
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
63+
import software.amazon.awssdk.services.s3.model.HeadBucketResponse;
6264
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
6365
import software.amazon.awssdk.services.s3.model.HeadObjectResponse;
6466
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
@@ -1033,6 +1035,25 @@ void testDoDoesObjectExist() {
10331035
assertFalse(result);
10341036
}
10351037

1038+
@Test
1039+
void testDoDoesBucketExist() {
1040+
HeadBucketResponse mockResponse = mock(HeadBucketResponse.class);
1041+
when(mockS3Client.headBucket(ArgumentMatchers.<java.util.function.Consumer<HeadBucketRequest.Builder>>any())).thenReturn(mockResponse);
1042+
1043+
boolean result = aws.doDoesBucketExist();
1044+
1045+
verify(mockS3Client, times(1)).headBucket(ArgumentMatchers.<java.util.function.Consumer<HeadBucketRequest.Builder>>any());
1046+
assertTrue(result);
1047+
1048+
// Verify the error state - bucket doesn't exist (404)
1049+
S3Exception mockException = mock(S3Exception.class);
1050+
doReturn(404).when(mockException).statusCode();
1051+
doThrow(mockException).when(mockS3Client).headBucket(ArgumentMatchers.<java.util.function.Consumer<HeadBucketRequest.Builder>>any());
1052+
1053+
result = aws.doDoesBucketExist();
1054+
assertFalse(result);
1055+
}
1056+
10361057
private S3Object mockObject(int index) {
10371058
S3Object mockS3 = mock(S3Object.class);
10381059
when(mockS3.key()).thenReturn("key-" + index);

blob/blob-aws/src/test/java/com/salesforce/multicloudj/blob/aws/async/AwsAsyncBlobStoreTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@
7070
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
7171
import software.amazon.awssdk.services.s3.model.GetObjectTaggingRequest;
7272
import software.amazon.awssdk.services.s3.model.GetObjectTaggingResponse;
73+
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
74+
import software.amazon.awssdk.services.s3.model.HeadBucketResponse;
7375
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
7476
import software.amazon.awssdk.services.s3.model.HeadObjectResponse;
7577
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
@@ -136,6 +138,7 @@
136138
import static org.junit.jupiter.api.Assertions.assertThrows;
137139
import static org.junit.jupiter.api.Assertions.assertTrue;
138140
import static org.mockito.ArgumentMatchers.any;
141+
import org.mockito.ArgumentMatchers;
139142
import static org.mockito.Mockito.doAnswer;
140143
import static org.mockito.Mockito.doReturn;
141144
import static org.mockito.Mockito.mock;
@@ -1133,6 +1136,30 @@ void testDoDoesObjectExist() throws ExecutionException, InterruptedException {
11331136
assertInstanceOf(SubstrateSdkException.class, assertThrows(ExecutionException.class, exceptionalResult::get).getCause());
11341137
}
11351138

1139+
@Test
1140+
void testDoDoesBucketExist() throws ExecutionException, InterruptedException {
1141+
HeadBucketResponse mockResponse = mock(HeadBucketResponse.class);
1142+
doReturn(future(mockResponse)).when(mockS3Client).headBucket(ArgumentMatchers.<java.util.function.Consumer<HeadBucketRequest.Builder>>any());
1143+
1144+
boolean result = aws.doDoesBucketExist().get();
1145+
1146+
verify(mockS3Client, times(1)).headBucket(ArgumentMatchers.<java.util.function.Consumer<HeadBucketRequest.Builder>>any());
1147+
assertTrue(result);
1148+
1149+
// Verify the error state - bucket doesn't exist (404)
1150+
S3Exception mockException = mock(S3Exception.class);
1151+
doReturn(404).when(mockException).statusCode();
1152+
doReturn(CompletableFuture.failedFuture(mockException)).when(mockS3Client).headBucket(ArgumentMatchers.<java.util.function.Consumer<HeadBucketRequest.Builder>>any());
1153+
result = aws.doDoesBucketExist().get();
1154+
assertFalse(result);
1155+
1156+
// Verify the unexpected error state
1157+
doReturn(CompletableFuture.failedFuture(mock(RuntimeException.class))).when(mockS3Client).headBucket(ArgumentMatchers.<java.util.function.Consumer<HeadBucketRequest.Builder>>any());
1158+
var exceptionalResult = aws.doDoesBucketExist();
1159+
assertTrue(exceptionalResult.isCompletedExceptionally());
1160+
assertInstanceOf(SubstrateSdkException.class, assertThrows(ExecutionException.class, exceptionalResult::get).getCause());
1161+
}
1162+
11361163
@Test
11371164
void doDownloadDirectory() throws ExecutionException, InterruptedException {
11381165

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"id" : "6c44e456-44f3-477e-9866-b4713335e4cb",
3+
"name" : "java-bucket-does-not-exist",
4+
"request" : {
5+
"url" : "/java-bucket-does-not-exist",
6+
"method" : "HEAD"
7+
},
8+
"response" : {
9+
"status" : 404,
10+
"headers" : {
11+
"Server" : "AmazonS3",
12+
"x-amz-request-id" : "S25MYAZ5WTF51AJ8",
13+
"x-amz-id-2" : "4yw9URr1CZ7CQpeqEif9BaRFIRyp/WxIQiYWQ6eDdZRoT8CJXtnoUhRf3u6WRkF8OYZTtjXLm/g6owSCjFfRE7F/0Ny35DW1LumP474xCbk=",
14+
"Date" : "Wed, 10 Dec 2025 17:44:21 GMT",
15+
"Content-Type" : "application/xml"
16+
}
17+
},
18+
"uuid" : "6c44e456-44f3-477e-9866-b4713335e4cb",
19+
"persistent" : true,
20+
"insertionIndex" : 595
21+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"id" : "5abebd0a-9030-4e7b-91aa-1409334a4b6b",
3+
"name" : "chameleon-jcloud",
4+
"request" : {
5+
"url" : "/chameleon-jcloud",
6+
"method" : "HEAD"
7+
},
8+
"response" : {
9+
"status" : 200,
10+
"headers" : {
11+
"Server" : "AmazonS3",
12+
"x-amz-access-point-alias" : "false",
13+
"x-amz-bucket-arn" : "arn:aws:s3:::chameleon-jcloud",
14+
"x-amz-request-id" : "JTCB60A5F2ERKBDM",
15+
"x-amz-id-2" : "VBMYlZ3W9WACjGa0VL6cjK5tNsgDdM6at9KorprY5u9IPJg1A+z4PCWxSDjzdkWQME2f9Sh/NK4=",
16+
"x-amz-bucket-region" : "us-west-2",
17+
"Date" : "Wed, 10 Dec 2025 17:43:54 GMT",
18+
"Content-Type" : "application/xml"
19+
}
20+
},
21+
"uuid" : "5abebd0a-9030-4e7b-91aa-1409334a4b6b",
22+
"persistent" : true,
23+
"insertionIndex" : 594
24+
}

blob/blob-client/src/main/java/com/salesforce/multicloudj/blob/async/client/AsyncBucketClient.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,17 @@ public CompletableFuture<Boolean> doesObjectExist(String key, String versionId)
387387
.exceptionally(this::handleException);
388388
}
389389

390+
/**
391+
* Determines if the bucket exists
392+
* @return Returns true if the bucket exists. Returns false if it doesn't exist.
393+
* @throws SubstrateSdkException Thrown if the operation fails
394+
*/
395+
public CompletableFuture<Boolean> doesBucketExist() {
396+
return blobStore
397+
.doesBucketExist()
398+
.exceptionally(this::handleException);
399+
}
400+
390401
/**
391402
* Uploads the directory content to substrate-specific Blob storage
392403
* Note: Specifying the contentLength in the UploadRequest can dramatically improve upload efficiency

blob/blob-client/src/main/java/com/salesforce/multicloudj/blob/async/driver/AbstractAsyncBlobStore.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,14 @@ public CompletableFuture<Boolean> doesObjectExist(String key, String versionId)
280280
return doDoesObjectExist(key, versionId);
281281
}
282282

283+
/**
284+
* {@inheritDoc}
285+
*/
286+
@Override
287+
public CompletableFuture<Boolean> doesBucketExist() {
288+
return doDoesBucketExist();
289+
}
290+
283291
/**
284292
* {@inheritDoc}
285293
*/
@@ -352,6 +360,8 @@ public CompletableFuture<Void> deleteDirectory(String prefix) {
352360

353361
protected abstract CompletableFuture<Boolean> doDoesObjectExist(String key, String versionId);
354362

363+
protected abstract CompletableFuture<Boolean> doDoesBucketExist();
364+
355365
protected abstract CompletableFuture<DirectoryDownloadResponse> doDownloadDirectory(DirectoryDownloadRequest directoryDownloadRequest);
356366

357367
protected abstract CompletableFuture<DirectoryUploadResponse> doUploadDirectory(DirectoryUploadRequest directoryUploadRequest);

0 commit comments

Comments
 (0)