2424import java .util .List ;
2525import java .util .Map ;
2626import java .util .Optional ;
27+ import java .util .concurrent .CompletableFuture ;
28+ import java .util .concurrent .CompletionException ;
2729import java .util .function .Consumer ;
2830import java .util .function .Supplier ;
2931import java .util .regex .Matcher ;
4143import software .amazon .awssdk .awscore .internal .defaultsmode .DefaultsModeConfiguration ;
4244import software .amazon .awssdk .core .ClientEndpointProvider ;
4345import software .amazon .awssdk .core .ClientType ;
46+ import software .amazon .awssdk .core .SdkClient ;
4447import software .amazon .awssdk .core .client .config .ClientOverrideConfiguration ;
4548import software .amazon .awssdk .core .client .config .SdkClientConfiguration ;
4649import software .amazon .awssdk .core .client .config .SdkClientOption ;
6871import software .amazon .awssdk .services .s3 .internal .endpoints .UseGlobalEndpointResolver ;
6972import software .amazon .awssdk .services .s3 .model .GetObjectRequest ;
7073import software .amazon .awssdk .services .s3 .model .GetUrlRequest ;
74+ import software .amazon .awssdk .services .s3 .model .HeadBucketRequest ;
75+ import software .amazon .awssdk .services .s3 .model .HeadObjectRequest ;
76+ import software .amazon .awssdk .services .s3 .model .NoSuchBucketException ;
77+ import software .amazon .awssdk .services .s3 .model .NoSuchKeyException ;
7178import software .amazon .awssdk .utils .AttributeMap ;
7279import software .amazon .awssdk .utils .StringUtils ;
7380import software .amazon .awssdk .utils .Validate ;
96103 * URL url = utilities.getUrl(request);
97104 * </pre>
98105 *
99- * Note: This class does not make network calls.
106+ * Note: Most methods in this class do not make network calls. The exceptions are convenience methods like
107+ * {@link #doesObjectExist(String, String)} and {@link #doesBucketExist(String)}, which call S3 on your behalf.
100108 */
101109@ Immutable
102110@ SdkPublicApi
@@ -112,6 +120,7 @@ public final class S3Utilities {
112120 private final boolean fipsEnabled ;
113121 private final ExecutionInterceptorChain interceptorChain ;
114122 private final UseGlobalEndpointResolver useGlobalEndpointResolver ;
123+ private final SdkClient s3Client ;
115124
116125 /**
117126 * SDK currently validates that region is present while constructing {@link S3Utilities} object.
@@ -143,6 +152,8 @@ private S3Utilities(Builder builder) {
143152 this .interceptorChain = createEndpointInterceptorChain ();
144153
145154 this .useGlobalEndpointResolver = createUseGlobalEndpointResolver ();
155+
156+ this .s3Client = builder .s3Client ;
146157 }
147158
148159 private void resolveDualstackSetting (S3Configuration .Builder s3ConfigBuilder , Builder s3UtiltiesBuilder ) {
@@ -177,11 +188,17 @@ public static Builder builder() {
177188 // Used by low-level client
178189 @ SdkInternalApi
179190 static S3Utilities create (SdkClientConfiguration clientConfiguration ) {
191+ return create (clientConfiguration , null );
192+ }
193+
194+ @ SdkInternalApi
195+ static S3Utilities create (SdkClientConfiguration clientConfiguration , SdkClient s3Client ) {
180196 S3Utilities .Builder builder = builder ()
181197 .region (clientConfiguration .option (AwsClientOption .AWS_REGION ))
182198 .s3Configuration ((S3Configuration ) clientConfiguration .option (SdkClientOption .SERVICE_CONFIGURATION ))
183199 .profileFile (clientConfiguration .option (SdkClientOption .PROFILE_FILE_SUPPLIER ))
184- .profileName (clientConfiguration .option (SdkClientOption .PROFILE_NAME ));
200+ .profileName (clientConfiguration .option (SdkClientOption .PROFILE_NAME ))
201+ .s3Client (s3Client );
185202
186203 ClientEndpointProvider clientEndpoint = clientConfiguration .option (SdkClientOption .CLIENT_ENDPOINT_PROVIDER );
187204 if (clientEndpoint .isEndpointOverridden ()) {
@@ -533,6 +550,115 @@ private UseGlobalEndpointResolver createUseGlobalEndpointResolver() {
533550 return new UseGlobalEndpointResolver (config );
534551 }
535552
553+ /**
554+ * Checks whether an object exists in S3. Returns {@code true} if the object exists, {@code false} if it does not.
555+ *
556+ * <p><b>Permissions:</b> This method returns {@code false} only when S3 responds with a 404 (NoSuchKey). If the
557+ * caller does not have {@code s3:ListBucket} permission on the bucket, S3 may return 403 instead of 404 for
558+ * non-existent objects, which will be thrown as an exception.
559+ *
560+ * @param bucket the bucket name
561+ * @param key the object key
562+ * @return {@code true} if the object exists, {@code false} otherwise
563+ * @throws S3Exception if S3 returns an error other than 404
564+ * @throws IllegalStateException if this {@link S3Utilities} was not created via {@link S3Client#utilities()}
565+ */
566+ public boolean doesObjectExist (String bucket , String key ) {
567+ Validate .notEmpty (bucket , "bucket must not be null or empty" );
568+ Validate .notEmpty (key , "key must not be null or empty" );
569+ Validate .validState (s3Client instanceof S3Client ,
570+ "doesObjectExist requires a sync S3Client. Use S3Client.utilities() to create S3Utilities." );
571+ try {
572+ ((S3Client ) s3Client ).headObject (HeadObjectRequest .builder ().bucket (bucket ).key (key ).build ());
573+ return true ;
574+ } catch (NoSuchKeyException e ) {
575+ return false ;
576+ }
577+ }
578+
579+ /**
580+ * Checks whether a bucket exists in S3 and is accessible. Returns {@code true} if the bucket exists and the caller
581+ * has access, {@code false} if the bucket does not exist.
582+ *
583+ * <p><b>Permissions:</b> S3 returns 403 for buckets that exist but the caller does not have access to.
584+ * This means the method cannot distinguish between "bucket does not exist" and "bucket exists but access is denied"
585+ * — the 403 case will be thrown as an exception.
586+ *
587+ * @param bucket the bucket name
588+ * @return {@code true} if the bucket exists and is accessible, {@code false} if it does not exist
589+ * @throws S3Exception if S3 returns an error other than 404
590+ * @throws IllegalStateException if this {@link S3Utilities} was not created via {@link S3Client#utilities()}
591+ */
592+ public boolean doesBucketExist (String bucket ) {
593+ Validate .notEmpty (bucket , "bucket must not be null or empty" );
594+ Validate .validState (s3Client instanceof S3Client ,
595+ "doesBucketExist requires a sync S3Client. Use S3Client.utilities() to create S3Utilities." );
596+ try {
597+ ((S3Client ) s3Client ).headBucket (HeadBucketRequest .builder ().bucket (bucket ).build ());
598+ return true ;
599+ } catch (NoSuchBucketException e ) {
600+ return false ;
601+ }
602+ }
603+
604+ /**
605+ * Async variant of {@link #doesObjectExist(String, String)}.
606+ *
607+ * <p>This method is only available when {@link S3Utilities} is created via {@link S3AsyncClient#utilities()}.
608+ *
609+ * @param bucket the bucket name
610+ * @param key the object key
611+ * @return a {@link CompletableFuture} that completes with {@code true} if the object exists, {@code false} otherwise
612+ * @throws S3Exception if S3 returns an error other than 404
613+ * @throws IllegalStateException if this {@link S3Utilities} was not created via {@link S3AsyncClient#utilities()}
614+ */
615+ public CompletableFuture <Boolean > doesObjectExistAsync (String bucket , String key ) {
616+ Validate .notEmpty (bucket , "bucket must not be null or empty" );
617+ Validate .notEmpty (key , "key must not be null or empty" );
618+ Validate .validState (s3Client instanceof S3AsyncClient ,
619+ "doesObjectExistAsync requires an S3AsyncClient."
620+ + " Use S3AsyncClient.utilities() to create S3Utilities." );
621+ return ((S3AsyncClient ) s3Client ).headObject (HeadObjectRequest .builder ().bucket (bucket ).key (key ).build ())
622+ .thenApply (r -> true )
623+ .exceptionally (t -> {
624+ Throwable cause = t instanceof CompletionException ? t .getCause () : t ;
625+ if (cause instanceof NoSuchKeyException ) {
626+ return false ;
627+ }
628+ throw (cause instanceof RuntimeException )
629+ ? (RuntimeException ) cause
630+ : new RuntimeException (cause );
631+ });
632+ }
633+
634+ /**
635+ * Async variant of {@link #doesBucketExist(String)}.
636+ *
637+ * <p>This method is only available when {@link S3Utilities} is created via {@link S3AsyncClient#utilities()}.
638+ *
639+ * @param bucket the bucket name
640+ * @return a {@link CompletableFuture} that completes with {@code true} if the bucket exists, {@code false} otherwise
641+ * @throws S3Exception if S3 returns an error other than 404
642+ * @throws IllegalStateException if this {@link S3Utilities} was not created via {@link S3AsyncClient#utilities()}
643+ */
644+ public CompletableFuture <Boolean > doesBucketExistAsync (String bucket ) {
645+ Validate .notEmpty (bucket , "bucket must not be null or empty" );
646+ Validate .validState (s3Client instanceof S3AsyncClient ,
647+ "doesBucketExistAsync requires an S3AsyncClient."
648+ + " Use S3AsyncClient.utilities() to create S3Utilities." );
649+ return ((S3AsyncClient ) s3Client ).headBucket (HeadBucketRequest .builder ().bucket (bucket ).build ())
650+ .thenApply (r -> true )
651+ .exceptionally (t -> {
652+ Throwable cause = t instanceof CompletionException ? t .getCause () : t ;
653+ if (cause instanceof NoSuchBucketException ) {
654+ return false ;
655+ }
656+ throw (cause instanceof RuntimeException )
657+ ? (RuntimeException ) cause
658+ : new RuntimeException (cause );
659+ });
660+ }
661+
536662 /**
537663 * Builder class to construct {@link S3Utilities} object
538664 */
@@ -545,6 +671,7 @@ public static final class Builder {
545671 private String profileName ;
546672 private Boolean dualstackEnabled ;
547673 private Boolean fipsEnabled ;
674+ private SdkClient s3Client ;
548675
549676 private Builder () {
550677 }
@@ -646,6 +773,15 @@ private Builder profileName(String profileName) {
646773 return this ;
647774 }
648775
776+ /**
777+ * The S3 client to use for convenience methods like {@link S3Utilities#doesObjectExist}. This is private and
778+ * only used when the utilities is created via {@link S3Client#utilities()}.
779+ */
780+ private Builder s3Client (SdkClient s3Client ) {
781+ this .s3Client = s3Client ;
782+ return this ;
783+ }
784+
649785 /**
650786 * Construct a {@link S3Utilities} object.
651787 */
0 commit comments