001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.db.storage.s3; 029 030import org.opencms.db.storage.CmsStorageBlobNotFoundException; 031import org.opencms.db.storage.CmsStorageException; 032import org.opencms.db.storage.Messages; 033import org.opencms.file.I_CmsFileContentStreamHandler; 034import org.opencms.main.OpenCms; 035import org.opencms.security.CmsDefaultCredentialsResolver; 036import org.opencms.security.I_CmsCredentialsResolver; 037import org.opencms.util.CmsFileUtil; 038 039import java.io.OutputStream; 040import java.net.URI; 041import java.time.Duration; 042import java.time.Instant; 043import java.util.ArrayList; 044import java.util.LinkedHashMap; 045import java.util.List; 046import java.util.Map; 047 048import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; 049import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; 050import software.amazon.awssdk.core.ResponseBytes; 051import software.amazon.awssdk.core.ResponseInputStream; 052import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; 053import software.amazon.awssdk.core.retry.RetryPolicy; 054import software.amazon.awssdk.core.sync.RequestBody; 055import software.amazon.awssdk.http.apache.ApacheHttpClient; 056import software.amazon.awssdk.regions.Region; 057import software.amazon.awssdk.services.s3.S3Client; 058import software.amazon.awssdk.services.s3.model.CopyObjectRequest; 059import software.amazon.awssdk.services.s3.model.Delete; 060import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; 061import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; 062import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; 063import software.amazon.awssdk.services.s3.model.GetObjectRequest; 064import software.amazon.awssdk.services.s3.model.GetObjectResponse; 065import software.amazon.awssdk.services.s3.model.HeadBucketRequest; 066import software.amazon.awssdk.services.s3.model.HeadObjectRequest; 067import software.amazon.awssdk.services.s3.model.HeadObjectResponse; 068import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; 069import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; 070import software.amazon.awssdk.services.s3.model.MetadataDirective; 071import software.amazon.awssdk.services.s3.model.NoSuchKeyException; 072import software.amazon.awssdk.services.s3.model.ObjectIdentifier; 073import software.amazon.awssdk.services.s3.model.PutObjectRequest; 074import software.amazon.awssdk.services.s3.model.S3Error; 075import software.amazon.awssdk.services.s3.model.S3Exception; 076import software.amazon.awssdk.services.s3.model.S3Object; 077 078/** 079 * S3 client using AWS SDK v2 optimized for a local object storage.<p> 080 * 081 * Should work with most products such as SeaweedFS, MinIO, Garage, Ceph.<p> 082 */ 083public class CmsGenericS3Client implements I_CmsS3Client { 084 085 /** Maximum number of keys accepted by the S3 DeleteObjects API. */ 086 public static final int MAX_DELETE_OBJECTS = 1000; 087 088 /** Default complete API call timeout in milliseconds. */ 089 public static final int DEFAULT_API_CALL_TIMEOUT = CmsS3ClientConfiguration.DEFAULT_API_CALL_TIMEOUT; 090 091 /** Default single API call attempt timeout in milliseconds. */ 092 public static final int DEFAULT_API_CALL_ATTEMPT_TIMEOUT = CmsS3ClientConfiguration.DEFAULT_API_CALL_ATTEMPT_TIMEOUT; 093 094 /** Default connection timeout in milliseconds. */ 095 public static final int DEFAULT_CONNECTION_TIMEOUT = CmsS3ClientConfiguration.DEFAULT_CONNECTION_TIMEOUT; 096 097 /** Default timeout for acquiring a pooled connection in milliseconds. */ 098 public static final int DEFAULT_CONNECTION_ACQUISITION_TIMEOUT = CmsS3ClientConfiguration.DEFAULT_CONNECTION_ACQUISITION_TIMEOUT; 099 100 /** Default maximum number of pooled connections. */ 101 public static final int DEFAULT_MAX_CONNECTIONS = CmsS3ClientConfiguration.DEFAULT_MAX_CONNECTIONS; 102 103 /** Default maximum number of retries. */ 104 public static final int DEFAULT_MAX_RETRIES = CmsS3ClientConfiguration.DEFAULT_MAX_RETRIES; 105 106 /** Default AWS region. */ 107 public static final String DEFAULT_REGION = CmsS3ClientConfiguration.DEFAULT_REGION; 108 109 /** Default socket timeout in milliseconds. */ 110 public static final int DEFAULT_SOCKET_TIMEOUT = CmsS3ClientConfiguration.DEFAULT_SOCKET_TIMEOUT; 111 112 /** The S3 client. */ 113 private final S3Client m_s3Client; 114 115 /** The client configuration. */ 116 private final CmsS3ClientConfiguration m_configuration; 117 118 /** 119 * Creates a new local S3 client.<p> 120 * 121 * @param configuration the S3 client configuration 122 */ 123 public CmsGenericS3Client(CmsS3ClientConfiguration configuration) { 124 125 m_configuration = resolveCredentials(configuration); 126 ApacheHttpClient.Builder httpClientBuilder = ApacheHttpClient.builder().connectionTimeout( 127 Duration.ofMillis(m_configuration.getConnectionTimeout())).connectionAcquisitionTimeout( 128 Duration.ofMillis(m_configuration.getConnectionAcquisitionTimeout())).maxConnections( 129 m_configuration.getMaxConnections()).socketTimeout( 130 Duration.ofMillis(m_configuration.getSocketTimeout())); 131 ClientOverrideConfiguration overrideConfiguration = ClientOverrideConfiguration.builder().apiCallAttemptTimeout( 132 Duration.ofMillis(m_configuration.getApiCallAttemptTimeout())).apiCallTimeout( 133 Duration.ofMillis(m_configuration.getApiCallTimeout())).retryPolicy( 134 RetryPolicy.builder().numRetries(m_configuration.getMaxRetries()).build()).build(); 135 m_s3Client = S3Client.builder().endpointOverride(URI.create(m_configuration.getEndpoint())).credentialsProvider( 136 StaticCredentialsProvider.create( 137 AwsBasicCredentials.create(m_configuration.getAccessKey(), m_configuration.getSecretKey()))).region( 138 Region.of(m_configuration.getRegion())).forcePathStyle( 139 m_configuration.isPathStyle()).httpClientBuilder(httpClientBuilder).overrideConfiguration( 140 overrideConfiguration).build(); 141 } 142 143 /** 144 * Creates a new local S3 client.<p> 145 * 146 * @param endpoint the URL of the S3 server (e.g., "http://localhost:8080") 147 * @param bucketName the bucket name 148 * @param accessKey the access key 149 * @param secretKey the secret key 150 * @param pathStyle whether path-style access should be used 151 */ 152 public CmsGenericS3Client( 153 String endpoint, 154 String bucketName, 155 String accessKey, 156 String secretKey, 157 boolean pathStyle) { 158 159 this(CmsS3ClientConfiguration.createDefault(endpoint, bucketName, accessKey, secretKey, pathStyle)); 160 } 161 162 /** 163 * Creates a new local S3 client.<p> 164 * 165 * @param endpoint the URL of the S3 server (e.g., "http://localhost:8080") 166 * @param bucketName the bucket name 167 * @param accessKey the access key 168 * @param secretKey the secret key 169 * @param pathStyle whether path-style access should be used 170 * @param connectionTimeout the connection timeout in milliseconds 171 * @param socketTimeout the socket timeout in milliseconds 172 * @param apiCallAttemptTimeout the timeout for a single API call attempt in milliseconds 173 * @param apiCallTimeout the timeout for the complete API call in milliseconds 174 * @param maxRetries the maximum number of retries 175 */ 176 public CmsGenericS3Client( 177 String endpoint, 178 String bucketName, 179 String accessKey, 180 String secretKey, 181 boolean pathStyle, 182 int connectionTimeout, 183 int socketTimeout, 184 int apiCallAttemptTimeout, 185 int apiCallTimeout, 186 int maxRetries) { 187 188 this( 189 new CmsS3ClientConfiguration( 190 endpoint, 191 bucketName, 192 accessKey, 193 secretKey, 194 pathStyle, 195 DEFAULT_REGION, 196 connectionTimeout, 197 socketTimeout, 198 apiCallAttemptTimeout, 199 apiCallTimeout, 200 maxRetries)); 201 } 202 203 /** 204 * Resolves credentials in the S3 client configuration with the OpenCms credentials resolver.<p> 205 * 206 * @param configuration the original S3 client configuration 207 * @return the S3 client configuration with resolved credentials 208 */ 209 public static CmsS3ClientConfiguration resolveCredentials(CmsS3ClientConfiguration configuration) { 210 211 return resolveCredentials(configuration, getCredentialsResolver()); 212 } 213 214 /** 215 * Resolves credentials in the S3 client configuration.<p> 216 * 217 * @param configuration the original S3 client configuration 218 * @param credentialsResolver the credentials resolver 219 * @return the S3 client configuration with resolved credentials 220 */ 221 public static CmsS3ClientConfiguration resolveCredentials( 222 CmsS3ClientConfiguration configuration, 223 I_CmsCredentialsResolver credentialsResolver) { 224 225 String accessKey = credentialsResolver.resolveCredential( 226 I_CmsCredentialsResolver.S3_ACCESS_KEY, 227 configuration.getAccessKey()); 228 String secretKey = credentialsResolver.resolveCredential( 229 I_CmsCredentialsResolver.S3_SECRET_KEY, 230 configuration.getSecretKey()); 231 return new CmsS3ClientConfiguration( 232 configuration.getEndpoint(), 233 configuration.getBucketName(), 234 accessKey, 235 secretKey, 236 configuration.isPathStyle(), 237 configuration.getRegion(), 238 configuration.getConnectionTimeout(), 239 configuration.getSocketTimeout(), 240 configuration.getApiCallAttemptTimeout(), 241 configuration.getApiCallTimeout(), 242 configuration.getMaxRetries(), 243 configuration.getMaxConnections(), 244 configuration.getConnectionAcquisitionTimeout()); 245 } 246 247 /** 248 * Returns the OpenCms credentials resolver, or the default resolver if OpenCms is not initialized far enough.<p> 249 * 250 * @return the credentials resolver 251 */ 252 private static I_CmsCredentialsResolver getCredentialsResolver() { 253 254 try { 255 return OpenCms.getCredentialsResolver(); 256 } catch (RuntimeException e) { 257 return new CmsDefaultCredentialsResolver(); 258 } 259 } 260 261 @Override 262 public void close() throws Exception { 263 264 m_s3Client.close(); 265 } 266 267 @Override 268 public void deleteObject(String key) throws Exception { 269 270 try { 271 DeleteObjectRequest request = DeleteObjectRequest.builder().bucket(m_configuration.getBucketName()).key( 272 key).build(); 273 m_s3Client.deleteObject(request); 274 } catch (S3Exception e) { 275 throw createStorageException("DELETE", key, e); 276 } catch (RuntimeException e) { 277 throw createStorageException("DELETE", key, e); 278 } 279 } 280 281 /** 282 * @see org.opencms.db.storage.s3.I_CmsS3Client#deleteObjects(java.util.List) 283 */ 284 @Override 285 public CmsS3DeleteResult deleteObjects(List<String> keys) throws Exception { 286 287 if (keys.isEmpty()) { 288 return new CmsS3DeleteResult(keys, java.util.Collections.emptyMap()); 289 } 290 if (keys.size() > MAX_DELETE_OBJECTS) { 291 throw new IllegalArgumentException( 292 "S3 multi-object delete supports at most " + MAX_DELETE_OBJECTS + " keys per request."); 293 } 294 List<ObjectIdentifier> objectIdentifiers = new ArrayList<ObjectIdentifier>(keys.size()); 295 for (String key : keys) { 296 objectIdentifiers.add(ObjectIdentifier.builder().key(key).build()); 297 } 298 try { 299 Delete delete = Delete.builder().objects(objectIdentifiers).quiet(Boolean.FALSE).build(); 300 DeleteObjectsRequest request = DeleteObjectsRequest.builder().bucket( 301 m_configuration.getBucketName()).delete(delete).build(); 302 DeleteObjectsResponse response = m_s3Client.deleteObjects(request); 303 Map<String, Exception> failures = new LinkedHashMap<String, Exception>(); 304 for (S3Error error : response.errors()) { 305 failures.put( 306 error.key(), 307 new CmsStorageException( 308 "S3 multi-object delete failed. operation=DELETE_OBJECTS, endpoint=" 309 + m_configuration.getEndpoint() 310 + ", bucket=" 311 + m_configuration.getBucketName() 312 + ", key=" 313 + error.key() 314 + ", errorCode=" 315 + error.code() 316 + ", message=" 317 + error.message())); 318 } 319 return new CmsS3DeleteResult(keys, failures); 320 } catch (S3Exception e) { 321 throw createStorageException("DELETE_OBJECTS", null, e); 322 } catch (RuntimeException e) { 323 throw createStorageException("DELETE_OBJECTS", null, e); 324 } 325 } 326 327 @Override 328 public boolean exists(String key) throws Exception { 329 330 try { 331 HeadObjectRequest request = HeadObjectRequest.builder().bucket(m_configuration.getBucketName()).key( 332 key).build(); 333 m_s3Client.headObject(request); 334 return true; 335 } catch (NoSuchKeyException e) { 336 return false; 337 } catch (S3Exception e) { 338 if (e.statusCode() == 404) { 339 return false; 340 } 341 throw createStorageException("HEAD", key, e); 342 } catch (RuntimeException e) { 343 throw createStorageException("HEAD", key, e); 344 } 345 } 346 347 @Override 348 public byte[] getObject(String key) throws Exception { 349 350 try { 351 GetObjectRequest request = GetObjectRequest.builder().bucket(m_configuration.getBucketName()).key( 352 key).build(); 353 ResponseBytes<GetObjectResponse> objectBytes = m_s3Client.getObjectAsBytes(request); 354 return objectBytes.asByteArray(); 355 } catch (NoSuchKeyException e) { 356 throw createBlobNotFoundException("GET", key, e); 357 } catch (S3Exception e) { 358 if (e.statusCode() == 404) { 359 throw createBlobNotFoundException("GET", key, e); 360 } 361 throw createStorageException("GET", key, e); 362 } catch (RuntimeException e) { 363 throw createStorageException("GET", key, e); 364 } 365 } 366 367 @Override 368 public long getObjectLength(String key) throws Exception { 369 370 try { 371 HeadObjectRequest request = HeadObjectRequest.builder().bucket(m_configuration.getBucketName()).key( 372 key).build(); 373 return m_s3Client.headObject(request).contentLength(); 374 } catch (NoSuchKeyException e) { 375 throw new CmsStorageBlobNotFoundException(key, e); 376 } catch (S3Exception e) { 377 if (e.statusCode() == 404) { 378 throw new CmsStorageBlobNotFoundException(key, e); 379 } 380 throw createStorageException("HEAD", key, e); 381 } catch (RuntimeException e) { 382 throw createStorageException("HEAD", key, e); 383 } 384 } 385 386 @Override 387 public long getObjectLengthIfExists(String key) throws Exception { 388 389 try { 390 HeadObjectRequest request = HeadObjectRequest.builder().bucket(m_configuration.getBucketName()).key( 391 key).build(); 392 return m_s3Client.headObject(request).contentLength(); 393 } catch (NoSuchKeyException e) { 394 return -1; 395 } catch (S3Exception e) { 396 if (e.statusCode() == 404) { 397 return -1; 398 } 399 throw createStorageException("HEAD", key, e); 400 } catch (RuntimeException e) { 401 throw createStorageException("HEAD", key, e); 402 } 403 } 404 405 /** 406 * @see org.opencms.db.storage.s3.I_CmsS3Client#getObjectMetadata(java.lang.String) 407 */ 408 @Override 409 public CmsS3ObjectMetadata getObjectMetadata(String key) throws Exception { 410 411 try { 412 HeadObjectRequest request = HeadObjectRequest.builder().bucket(m_configuration.getBucketName()).key( 413 key).build(); 414 HeadObjectResponse response = m_s3Client.headObject(request); 415 return new CmsS3ObjectMetadata(key, response.contentLength(), response.lastModified(), response.eTag()); 416 } catch (NoSuchKeyException e) { 417 throw new CmsStorageBlobNotFoundException(key, e); 418 } catch (S3Exception e) { 419 if (e.statusCode() == 404) { 420 throw new CmsStorageBlobNotFoundException(key, e); 421 } 422 throw createStorageException("HEAD", key, e); 423 } catch (RuntimeException e) { 424 throw createStorageException("HEAD", key, e); 425 } 426 } 427 428 @Override 429 public void putObject(String key, byte[] content) throws Exception { 430 431 try { 432 PutObjectRequest request = PutObjectRequest.builder().bucket(m_configuration.getBucketName()).key( 433 key).build(); 434 m_s3Client.putObject(request, RequestBody.fromBytes(content)); 435 } catch (S3Exception e) { 436 throw createStorageException("PUT", key, e); 437 } catch (RuntimeException e) { 438 throw createStorageException("PUT", key, e); 439 } 440 } 441 442 /** 443 * @see I_CmsS3Client#readObjectFrom(String, I_CmsFileContentStreamHandler) 444 */ 445 @Override 446 public void readObjectFrom(String key, I_CmsFileContentStreamHandler handler) throws Exception { 447 448 try { 449 GetObjectRequest request = GetObjectRequest.builder().bucket(m_configuration.getBucketName()).key( 450 key).build(); 451 try (ResponseInputStream<GetObjectResponse> in = m_s3Client.getObject(request)) { 452 handler.read(in); 453 } 454 } catch (NoSuchKeyException e) { 455 throw createBlobNotFoundException("GET", key, e); 456 } catch (S3Exception e) { 457 if (e.statusCode() == 404) { 458 throw createBlobNotFoundException("GET", key, e); 459 } 460 throw createStorageException("GET", key, e); 461 } catch (RuntimeException e) { 462 throw createStorageException("GET", key, e); 463 } 464 } 465 466 /** 467 * @see org.opencms.db.storage.s3.I_CmsS3Client#renewObject(java.lang.String, java.lang.String, java.time.Instant) 468 */ 469 @Override 470 public boolean renewObject(String key, String expectedRevision, Instant renewalTime) throws Exception { 471 472 try { 473 // S3 requires REPLACE when copying an object onto itself; image cache writes no custom metadata. 474 CopyObjectRequest request = CopyObjectRequest.builder().sourceBucket( 475 m_configuration.getBucketName()).sourceKey(key).destinationBucket( 476 m_configuration.getBucketName()).destinationKey(key).copySourceIfMatch( 477 expectedRevision).metadataDirective(MetadataDirective.REPLACE).build(); 478 m_s3Client.copyObject(request); 479 return true; 480 } catch (NoSuchKeyException e) { 481 return false; 482 } catch (S3Exception e) { 483 if ((e.statusCode() == 404) || (e.statusCode() == 412)) { 484 return false; 485 } 486 throw createStorageException("RENEW", key, e); 487 } catch (RuntimeException e) { 488 throw createStorageException("RENEW", key, e); 489 } 490 } 491 492 @Override 493 public void validateBucketAccess() throws Exception { 494 495 try { 496 HeadBucketRequest request = HeadBucketRequest.builder().bucket(m_configuration.getBucketName()).build(); 497 m_s3Client.headBucket(request); 498 } catch (S3Exception e) { 499 throw createStorageException("HEAD_BUCKET", null, e); 500 } catch (RuntimeException e) { 501 throw createStorageException("HEAD_BUCKET", null, e); 502 } 503 } 504 505 @Override 506 public void visitObjectKeys(I_CmsS3ObjectKeyVisitor visitor) throws Exception { 507 508 visitObjects(null, metadata -> visitor.visit(metadata.getKey())); 509 } 510 511 @Override 512 public void visitObjects(I_CmsS3ObjectVisitor visitor) throws Exception { 513 514 visitObjects(null, metadata -> visitor.visit(metadata.getKey(), metadata.getLength())); 515 } 516 517 /** 518 * @see org.opencms.db.storage.s3.I_CmsS3Client#visitObjects(java.lang.String, org.opencms.db.storage.s3.I_CmsS3Client.I_CmsS3ObjectMetadataVisitor) 519 */ 520 @Override 521 public void visitObjects(String prefix, I_CmsS3ObjectMetadataVisitor visitor) throws Exception { 522 523 try { 524 ListObjectsV2Request.Builder requestBuilder = ListObjectsV2Request.builder().bucket( 525 m_configuration.getBucketName()); 526 if (prefix != null) { 527 requestBuilder.prefix(prefix); 528 } 529 ListObjectsV2Request request = requestBuilder.build(); 530 for (ListObjectsV2Response page : m_s3Client.listObjectsV2Paginator(request)) { 531 for (S3Object object : page.contents()) { 532 visitor.visit( 533 new CmsS3ObjectMetadata(object.key(), object.size(), object.lastModified(), object.eTag())); 534 } 535 } 536 } catch (S3Exception e) { 537 throw createStorageException("LIST", null, e); 538 } catch (RuntimeException e) { 539 throw createStorageException("LIST", null, e); 540 } 541 } 542 543 /** 544 * @see I_CmsS3Client#writeObjectRangeTo(String, long, long, OutputStream) 545 */ 546 @Override 547 public void writeObjectRangeTo(String key, long start, long length, OutputStream out) throws Exception { 548 549 writeObjectRangeToWithMetadata(key, start, length, out); 550 } 551 552 /** 553 * @see I_CmsS3Client#writeObjectRangeToWithMetadata(String, long, long, OutputStream) 554 */ 555 @Override 556 public CmsS3ObjectMetadata writeObjectRangeToWithMetadata(String key, long start, long length, OutputStream out) 557 throws Exception { 558 559 if ((start < 0) || (length < 1) || ((Long.MAX_VALUE - start) < length)) { 560 throw new IllegalArgumentException("Invalid byte range: start=" + start + ", length=" + length); 561 } 562 long end = start + length - 1; 563 try { 564 GetObjectRequest request = GetObjectRequest.builder().bucket(m_configuration.getBucketName()).key( 565 key).range("bytes=" + start + "-" + end).build(); 566 try (ResponseInputStream<GetObjectResponse> in = m_s3Client.getObject(request)) { 567 CmsFileUtil.copy(in, out); 568 GetObjectResponse response = in.response(); 569 return new CmsS3ObjectMetadata(key, response.contentLength(), response.lastModified(), response.eTag()); 570 } 571 } catch (NoSuchKeyException e) { 572 throw createBlobNotFoundException("GET", key, e); 573 } catch (S3Exception e) { 574 if (e.statusCode() == 404) { 575 throw createBlobNotFoundException("GET", key, e); 576 } 577 throw createStorageException("GET", key, e); 578 } catch (RuntimeException e) { 579 throw createStorageException("GET", key, e); 580 } 581 } 582 583 @Override 584 public void writeObjectTo(String key, OutputStream out) throws Exception { 585 586 writeObjectToWithMetadata(key, out); 587 } 588 589 /** 590 * @see I_CmsS3Client#writeObjectToWithMetadata(String, OutputStream) 591 */ 592 @Override 593 public CmsS3ObjectMetadata writeObjectToWithMetadata(String key, OutputStream out) throws Exception { 594 595 try { 596 GetObjectRequest request = GetObjectRequest.builder().bucket(m_configuration.getBucketName()).key( 597 key).build(); 598 try (ResponseInputStream<GetObjectResponse> in = m_s3Client.getObject(request)) { 599 CmsFileUtil.copy(in, out); 600 GetObjectResponse response = in.response(); 601 return new CmsS3ObjectMetadata(key, response.contentLength(), response.lastModified(), response.eTag()); 602 } 603 } catch (NoSuchKeyException e) { 604 throw createBlobNotFoundException("GET", key, e); 605 } catch (S3Exception e) { 606 if (e.statusCode() == 404) { 607 throw createBlobNotFoundException("GET", key, e); 608 } 609 throw createStorageException("GET", key, e); 610 } catch (RuntimeException e) { 611 throw createStorageException("GET", key, e); 612 } 613 } 614 615 /** 616 * Creates a missing blob exception with S3 context.<p> 617 * 618 * @param operation the S3 operation 619 * @param key the S3 key 620 * @param cause the cause 621 * @return the missing blob exception 622 */ 623 private CmsStorageBlobNotFoundException createBlobNotFoundException(String operation, String key, Exception cause) { 624 625 return new CmsStorageBlobNotFoundException( 626 Messages.get().getBundle().key(Messages.ERR_STORAGE_BLOB_MISSING_2, m_configuration.getBucketName(), key) 627 + " " 628 + createContext(operation, key, cause), 629 cause); 630 } 631 632 /** 633 * Creates the S3 diagnostic context.<p> 634 * 635 * @param operation the S3 operation 636 * @param key the S3 key 637 * @param cause the cause 638 * @return the context 639 */ 640 private String createContext(String operation, String key, Exception cause) { 641 642 StringBuilder result = new StringBuilder(192); 643 result.append("operation=").append(operation); 644 result.append(", endpoint=").append(m_configuration.getEndpoint()); 645 result.append(", bucket=").append(m_configuration.getBucketName()); 646 if (key != null) { 647 result.append(", key=").append(key); 648 } 649 result.append(", region=").append(m_configuration.getRegion()); 650 result.append(", pathStyle=").append(m_configuration.isPathStyle()); 651 if (cause instanceof S3Exception) { 652 S3Exception s3Exception = (S3Exception)cause; 653 result.append(", statusCode=").append(s3Exception.statusCode()); 654 if (s3Exception.awsErrorDetails() != null) { 655 result.append(", errorCode=").append(s3Exception.awsErrorDetails().errorCode()); 656 } 657 result.append(", requestId=").append(s3Exception.requestId()); 658 } 659 return result.toString(); 660 } 661 662 /** 663 * Creates an exception with S3 context.<p> 664 * 665 * @param operation the S3 operation 666 * @param key the S3 key 667 * @param cause the cause 668 * @return the storage exception 669 */ 670 private CmsStorageException createStorageException(String operation, String key, Exception cause) { 671 672 return new CmsStorageException("S3 storage operation failed. " + createContext(operation, key, cause), cause); 673 } 674}