package de.otto.edison.togglz.s3.testsupport; import org.slf4j.Logger; import software.amazon.awssdk.awscore.exception.AwsErrorDetails; import software.amazon.awssdk.core.ResponseInputStream; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.http.Abortable; import software.amazon.awssdk.http.AbortableInputStream; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.*; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; import static de.otto.edison.togglz.s3.testsupport.BucketItem.bucketItemBuilder; import static org.slf4j.LoggerFactory.getLogger; import static software.amazon.awssdk.utils.IoUtils.toByteArray; public class LocalS3Client implements S3Client { private static final Logger LOG = getLogger(LocalS3Client.class); private static final Instant BUCKET_DEFAULT_CREATION_DATE = Instant.parse("2017-01-01T10:00:00.00Z"); private Map<String, Map<String, BucketItem>> bucketsWithContents; public LocalS3Client() { this.bucketsWithContents = new HashMap<>(); } @Override public ListObjectsV2Response listObjectsV2(final ListObjectsV2Request listObjectsV2Request) throws S3Exception { final Collection<S3Object> s3Objects = bucketsWithContents.get(listObjectsV2Request.bucket()) .values() .stream() .map(bucketItem -> S3Object.builder() .key(bucketItem.getName()) .size((long) bucketItem.getData().length) .lastModified(bucketItem.getLastModified()) .build()) .collect(Collectors.toList()); return ListObjectsV2Response.builder() .contents(s3Objects) .keyCount(s3Objects.size()) .build(); } @Override public CreateBucketResponse createBucket(final CreateBucketRequest createBucketRequest) throws S3Exception { bucketsWithContents.put(createBucketRequest.bucket(), new HashMap<>()); return CreateBucketResponse.builder().build(); } @Override public PutObjectResponse putObject(final PutObjectRequest putObjectRequest, final RequestBody requestBody) throws S3Exception { try { bucketsWithContents.get(putObjectRequest.bucket()).put(putObjectRequest.key(), bucketItemBuilder() .withName(putObjectRequest.key()) .withData(toByteArray(requestBody.contentStreamProvider().newStream())) .withLastModifiedNow() .build()); return PutObjectResponse.builder().build(); } catch (IOException e) { throw new RuntimeException(e); } } @Override public DeleteObjectsResponse deleteObjects(final DeleteObjectsRequest deleteObjectsRequest) throws S3Exception { final Map<String, BucketItem> bucketItemMap = bucketsWithContents.get(deleteObjectsRequest.bucket()); deleteObjectsRequest.delete().objects() .stream() .map(ObjectIdentifier::key) .forEach(bucketItemMap::remove); return DeleteObjectsResponse.builder().build(); } @Override public ListBucketsResponse listBuckets(final ListBucketsRequest listBucketsRequest) throws S3Exception { return ListBucketsResponse.builder() .buckets(bucketsWithContents.keySet().stream() .map(name -> Bucket.builder() .creationDate(BUCKET_DEFAULT_CREATION_DATE) .name(name) .build()) .collect(Collectors.toList())).build(); } @Override public GetObjectResponse getObject(final GetObjectRequest getObjectRequest, final Path filePath) throws S3Exception { final Map<String, BucketItem> bucketItemMap = bucketsWithContents.get(getObjectRequest.bucket()); final BucketItem bucketItem = bucketItemMap.get(getObjectRequest.key()); try { Files.write(filePath, bucketItem.getData()); } catch (IOException e) { throw SdkClientException.create("", e); } return GetObjectResponse.builder().build(); } @SuppressWarnings("unchecked") @Override public ResponseInputStream<GetObjectResponse> getObject(final GetObjectRequest getObjectRequest) throws S3Exception { final Map<String, BucketItem> bucketItemMap = bucketsWithContents.get(getObjectRequest.bucket()); final BucketItem bucketItem = bucketItemMap.get(getObjectRequest.key()); if (bucketItem != null) { try { return new ResponseInputStream<>(GetObjectResponse.builder().build(), toAbortableInputStream(bucketItem)); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) { throw SdkClientException.create("", e); } } else { throw NoSuchKeyException.builder().message("NoSuchKey").awsErrorDetails(AwsErrorDetails.builder().errorCode("NoSuchKey").build()).build(); } } private AbortableInputStream toAbortableInputStream(final BucketItem bucketItem) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { final Constructor<AbortableInputStream> constructor = AbortableInputStream.class.getDeclaredConstructor(InputStream.class, Abortable.class); constructor.setAccessible(true); return constructor.newInstance( new ByteArrayInputStream(bucketItem.getData()), (Abortable) () -> {} ); } @Override public void close() { LOG.debug("s3 closing..."); } @Override public String serviceName() { return "s3"; } }