From 2235656358de6fd6a15510dc500f4f8cfa5069b0 Mon Sep 17 00:00:00 2001 From: psevestre Date: Sun, 8 Dec 2019 17:39:07 -0300 Subject: [PATCH] [BAEL-3562] AWS S3 with Java - Reactive Support (#8309) * [BAEL-3164] Add spring-boot-jdbi module * [BAEL-3164] Remove extra files * [BAEL-3164] Update springboot main dependency * Reset bad commit * [BAEL-3562] Added basic code * [BAEL-3562] Some refatoring * [BAEL-3562] More refatoring * [BAEL-3562] LiveTests --- aws-reactive/images/rective-upload.png | Bin 0 -> 22760 bytes aws-reactive/images/rective-upload.txt | 29 ++ aws-reactive/images/thread-per-client.png | Bin 0 -> 31850 bytes aws-reactive/images/thread-per-client.txt | 28 ++ aws-reactive/pom.xml | 105 ++++++ .../reactive/s3/DownloadFailedException.java | 32 ++ .../aws/reactive/s3/DownloadResource.java | 144 ++++++++ .../reactive/s3/ReactiveS3Application.java | 13 + .../s3/S3ClientConfigurarionProperties.java | 28 ++ .../reactive/s3/S3ClientConfiguration.java | 65 ++++ .../reactive/s3/UploadFailedException.java | 32 ++ .../aws/reactive/s3/UploadResource.java | 308 ++++++++++++++++++ .../aws/reactive/s3/UploadResult.java | 25 ++ .../src/main/resources/application-minio.yml | 15 + .../src/main/resources/application.yml | 16 + .../s3/ReactiveS3ApplicationLiveTest.java | 85 +++++ .../src/test/resources/testimage1.png | Bin 0 -> 4315 bytes .../src/test/resources/testimage2.png | Bin 0 -> 24921 bytes 18 files changed, 925 insertions(+) create mode 100644 aws-reactive/images/rective-upload.png create mode 100644 aws-reactive/images/rective-upload.txt create mode 100644 aws-reactive/images/thread-per-client.png create mode 100644 aws-reactive/images/thread-per-client.txt create mode 100644 aws-reactive/pom.xml create mode 100644 aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/DownloadFailedException.java create mode 100644 aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/DownloadResource.java create mode 100644 aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/ReactiveS3Application.java create mode 100644 aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/S3ClientConfigurarionProperties.java create mode 100644 aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/S3ClientConfiguration.java create mode 100644 aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/UploadFailedException.java create mode 100644 aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/UploadResource.java create mode 100644 aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/UploadResult.java create mode 100644 aws-reactive/src/main/resources/application-minio.yml create mode 100644 aws-reactive/src/main/resources/application.yml create mode 100644 aws-reactive/src/test/java/com/baeldung/aws/reactive/s3/ReactiveS3ApplicationLiveTest.java create mode 100644 aws-reactive/src/test/resources/testimage1.png create mode 100644 aws-reactive/src/test/resources/testimage2.png diff --git a/aws-reactive/images/rective-upload.png b/aws-reactive/images/rective-upload.png new file mode 100644 index 0000000000000000000000000000000000000000..ebe5a7d69c03dad3c07062c3cb654e6f281e95a5 GIT binary patch literal 22760 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 literal 0 HcmV?d00001 diff --git a/aws-reactive/images/rective-upload.txt b/aws-reactive/images/rective-upload.txt new file mode 100644 index 0000000000..fb5177544e --- /dev/null +++ b/aws-reactive/images/rective-upload.txt @@ -0,0 +1,29 @@ +participant "Client 1" as C1 +participant "Client 2" as C2 +participant "Reactive Web App" as RWS +participant "Backend" as S3 +C1 -> RWS: POST +activate C1 +activate RWS +RWS -> S3: Async POST +deactivate RWS +C2 -> RWS: POST +activate C2 +activate RWS +RWS -> S3: Async POST +deactivate RWS +S3 --> RWS: Async Result +activate RWS +RWS -->C2: Result +deactivate RWS +deactivate C2 +// First file EOF +S3 --> RWS: Async Result +activate RWS +RWS -->C1: Result +deactivate RWS +deactivate C1 + + + + diff --git a/aws-reactive/images/thread-per-client.png b/aws-reactive/images/thread-per-client.png new file mode 100644 index 0000000000000000000000000000000000000000..dc75839aa451491fa0071d4dbc09decc348535bf GIT binary patch literal 31850 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 Dzin Controller: POST Data +activate Client 1 +activate Controller +Controller -> Backend: Save Data +activate Backend +note left of Controller #yellow: Controller blocked\nuntil result received +Backend --> Controller: Result +deactivate Backend +Controller --> Client 1: Result +deactivate Client 1 +deactivate Controller +// 2nd Upload +Client 2-> Controller: POST Data +activate Client 2 +activate Controller +Controller -> Backend: Save Data +activate Backend +note left of Controller #yellow: Controller blocket\nuntil result received +Backend --> Controller: Result +deactivate Backend +Controller --> Client 2: Result +deactivate Controller +deactivate Client 2 + diff --git a/aws-reactive/pom.xml b/aws-reactive/pom.xml new file mode 100644 index 0000000000..b3fcb24902 --- /dev/null +++ b/aws-reactive/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + + com.baeldung + parent-modules + 1.0.0-SNAPSHOT + + + aws-reactive + 0.0.1-SNAPSHOT + aws-reactive + AWS Reactive Sample + + + 1.8 + + + + + + + + org.springframework.boot + spring-boot-dependencies + 2.2.1.RELEASE + pom + import + + + + software.amazon.awssdk + bom + 2.10.27 + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + software.amazon.awssdk + s3 + compile + + + + netty-nio-client + software.amazon.awssdk + compile + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + io.projectreactor + reactor-test + test + + + org.springframework.boot + spring-boot-devtools + runtime + + + org.springframework.boot + spring-boot-configuration-processor + + + org.projectlombok + lombok + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/DownloadFailedException.java b/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/DownloadFailedException.java new file mode 100644 index 0000000000..a88e1ab010 --- /dev/null +++ b/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/DownloadFailedException.java @@ -0,0 +1,32 @@ +package com.baeldung.aws.reactive.s3; + +import java.util.Optional; + +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.http.SdkHttpResponse; + +@AllArgsConstructor +public class DownloadFailedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private int statusCode; + private Optional statusText; + + public DownloadFailedException(SdkResponse response) { + + SdkHttpResponse httpResponse = response.sdkHttpResponse(); + if (httpResponse != null) { + this.statusCode = httpResponse.statusCode(); + this.statusText = httpResponse.statusText(); + } else { + this.statusCode = HttpStatus.INTERNAL_SERVER_ERROR.value(); + this.statusText = Optional.of("UNKNOWN"); + } + + } + +} diff --git a/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/DownloadResource.java b/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/DownloadResource.java new file mode 100644 index 0000000000..838ada1685 --- /dev/null +++ b/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/DownloadResource.java @@ -0,0 +1,144 @@ +/** + * + */ +package com.baeldung.aws.reactive.s3; + +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.http.ResponseEntity.BodyBuilder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.core.internal.async.ByteArrayAsyncResponseTransformer; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; + +/** + * @author Philippe + * + */ +@RestController +@RequestMapping("/inbox") +@Slf4j +public class DownloadResource { + + + private final S3AsyncClient s3client; + private final S3ClientConfigurarionProperties s3config; + + public DownloadResource(S3AsyncClient s3client, S3ClientConfigurarionProperties s3config) { + this.s3client = s3client; + this.s3config = s3config; + } + + + @GetMapping(path="/{filekey}") + public Mono>> downloadFile(@PathVariable("filekey") String filekey) { + + GetObjectRequest request = GetObjectRequest.builder() + .bucket(s3config.getBucket()) + .key(filekey) + .build(); + + return Mono.fromFuture(s3client.getObject(request,new FluxResponseProvider())) + .map( (response) -> { + checkResult(response.sdkResponse); + String filename = getMetadataItem(response.sdkResponse,"filename",filekey); + + log.info("[I65] filename={}, length={}",filename, response.sdkResponse.contentLength() ); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_TYPE, response.sdkResponse.contentType()) + .header(HttpHeaders.CONTENT_LENGTH, Long.toString(response.sdkResponse.contentLength())) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"") + .body(response.flux); + }); + } + + /** + * Lookup a metadata key in a case-insensitive way. + * @param sdkResponse + * @param key + * @param defaultValue + * @return + */ + private String getMetadataItem(GetObjectResponse sdkResponse, String key, String defaultValue) { + for( Entry entry : sdkResponse.metadata().entrySet()) { + if ( entry.getKey().equalsIgnoreCase(key)) { + return entry.getValue(); + } + } + return defaultValue; + } + + + // Helper used to check return codes from an API call + private static void checkResult(GetObjectResponse response) { + SdkHttpResponse sdkResponse = response.sdkHttpResponse(); + if ( sdkResponse != null && sdkResponse.isSuccessful()) { + return; + } + + throw new DownloadFailedException(response); + } + + + static class FluxResponseProvider implements AsyncResponseTransformer { + + private FluxResponse response; + + @Override + public CompletableFuture prepare() { + response = new FluxResponse(); + return response.cf; + } + + @Override + public void onResponse(GetObjectResponse sdkResponse) { + this.response.sdkResponse = sdkResponse; + } + + @Override + public void onStream(SdkPublisher publisher) { + response.flux = Flux.from(publisher); + response.cf.complete(response); + } + + @Override + public void exceptionOccurred(Throwable error) { + response.cf.completeExceptionally(error); + } + + } + + /** + * Holds the API response and stream + * @author Philippe + */ + static class FluxResponse { + + final CompletableFuture cf = new CompletableFuture<>(); + GetObjectResponse sdkResponse; + Flux flux; + } + +} diff --git a/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/ReactiveS3Application.java b/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/ReactiveS3Application.java new file mode 100644 index 0000000000..b90c085fc9 --- /dev/null +++ b/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/ReactiveS3Application.java @@ -0,0 +1,13 @@ +package com.baeldung.aws.reactive.s3; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ReactiveS3Application { + + public static void main(String[] args) { + SpringApplication.run(ReactiveS3Application.class, args); + } + +} diff --git a/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/S3ClientConfigurarionProperties.java b/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/S3ClientConfigurarionProperties.java new file mode 100644 index 0000000000..b30bc1e5fa --- /dev/null +++ b/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/S3ClientConfigurarionProperties.java @@ -0,0 +1,28 @@ +package com.baeldung.aws.reactive.s3; + +import java.net.URI; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import lombok.Data; +import software.amazon.awssdk.regions.Region; + +@ConfigurationProperties(prefix = "aws.s3") +@Data +public class S3ClientConfigurarionProperties { + + private Region region = Region.US_EAST_1; + private URI endpoint = null; + + private String accessKeyId; + private String secretAccessKey; + + // Bucket name we'll be using as our backend storage + private String bucket; + + // AWS S3 requires that file parts must have at least 5MB, except + // for the last part. This may change for other S3-compatible services, so let't + // define a configuration property for that + private int multipartMinPartSize = 5*1024*1024; + +} diff --git a/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/S3ClientConfiguration.java b/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/S3ClientConfiguration.java new file mode 100644 index 0000000000..906ea088a1 --- /dev/null +++ b/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/S3ClientConfiguration.java @@ -0,0 +1,65 @@ +package com.baeldung.aws.reactive.s3; + +import java.time.Duration; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.S3AsyncClientBuilder; +import software.amazon.awssdk.services.s3.S3Configuration; +import software.amazon.awssdk.utils.StringUtils; + +@Configuration +@EnableConfigurationProperties(S3ClientConfigurarionProperties.class) +public class S3ClientConfiguration { + + @Bean + public S3AsyncClient s3client(S3ClientConfigurarionProperties s3props, AwsCredentialsProvider credentialsProvider) { + + SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() + .writeTimeout(Duration.ZERO) + .maxConcurrency(64) + .build(); + + S3Configuration serviceConfiguration = S3Configuration.builder() + .checksumValidationEnabled(false) + .chunkedEncodingEnabled(true) + .build(); + + S3AsyncClientBuilder b = S3AsyncClient.builder() + .httpClient(httpClient) + .region(s3props.getRegion()) + .credentialsProvider(credentialsProvider) + .serviceConfiguration(serviceConfiguration); + + if (s3props.getEndpoint() != null) { + b = b.endpointOverride(s3props.getEndpoint()); + } + + return b.build(); + } + + @Bean + public AwsCredentialsProvider awsCredentialsProvider(S3ClientConfigurarionProperties s3props) { + + if (StringUtils.isBlank(s3props.getAccessKeyId())) { + // Return default provider + return DefaultCredentialsProvider.create(); + } + else { + // Return custom credentials provider + return () -> { + AwsCredentials creds = AwsBasicCredentials.create(s3props.getAccessKeyId(), s3props.getSecretAccessKey()); + return creds; + }; + } + } +} diff --git a/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/UploadFailedException.java b/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/UploadFailedException.java new file mode 100644 index 0000000000..0cfebc85d2 --- /dev/null +++ b/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/UploadFailedException.java @@ -0,0 +1,32 @@ +package com.baeldung.aws.reactive.s3; + +import java.util.Optional; + +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.http.SdkHttpResponse; + +@AllArgsConstructor +public class UploadFailedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private int statusCode; + private Optional statusText; + + public UploadFailedException(SdkResponse response) { + + SdkHttpResponse httpResponse = response.sdkHttpResponse(); + if (httpResponse != null) { + this.statusCode = httpResponse.statusCode(); + this.statusText = httpResponse.statusText(); + } else { + this.statusCode = HttpStatus.INTERNAL_SERVER_ERROR.value(); + this.statusText = Optional.of("UNKNOWN"); + } + + } + +} diff --git a/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/UploadResource.java b/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/UploadResource.java new file mode 100644 index 0000000000..fa7bf6a471 --- /dev/null +++ b/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/UploadResource.java @@ -0,0 +1,308 @@ +/** + * + */ +package com.baeldung.aws.reactive.s3; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.codec.multipart.FilePart; +import org.springframework.http.codec.multipart.Part; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest; +import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadResponse; +import software.amazon.awssdk.services.s3.model.CompletedMultipartUpload; +import software.amazon.awssdk.services.s3.model.CompletedMultipartUpload.Builder; +import software.amazon.awssdk.services.s3.model.CompletedPart; +import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest; +import software.amazon.awssdk.services.s3.model.CreateMultipartUploadResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.UploadPartRequest; +import software.amazon.awssdk.services.s3.model.UploadPartResponse; + +/** + * @author Philippe + * + */ +@RestController +@RequestMapping("/inbox") +@Slf4j +public class UploadResource { + + private final S3AsyncClient s3client; + private final S3ClientConfigurarionProperties s3config; + + public UploadResource(S3AsyncClient s3client, S3ClientConfigurarionProperties s3config) { + this.s3client = s3client; + this.s3config = s3config; + } + + /** + * Standard file upload. + */ + @PostMapping + public Mono> uploadHandler(@RequestHeader HttpHeaders headers, @RequestBody Flux body) { + + long length = headers.getContentLength(); + if (length < 0) { + throw new UploadFailedException(HttpStatus.BAD_REQUEST.value(), Optional.of("required header missing: Content-Length")); + } + + String fileKey = UUID.randomUUID().toString(); + Map metadata = new HashMap(); + MediaType mediaType = headers.getContentType(); + + if (mediaType == null) { + mediaType = MediaType.APPLICATION_OCTET_STREAM; + } + + log.info("[I95] uploadHandler: mediaType{}, length={}", mediaType, length); + CompletableFuture future = s3client + .putObject(PutObjectRequest.builder() + .bucket(s3config.getBucket()) + .contentLength(length) + .key(fileKey.toString()) + .contentType(mediaType.toString()) + .metadata(metadata) + .build(), + AsyncRequestBody.fromPublisher(body)); + + return Mono.fromFuture(future) + .map((response) -> { + checkResult(response); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(new UploadResult(HttpStatus.CREATED, new String[] {fileKey})); + }); + } + + + /** + * Multipart file upload + * @param bucket + * @param parts + * @param headers + * @return + */ + @RequestMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, method = {RequestMethod.POST, RequestMethod.PUT}) + public Mono> multipartUploadHandler(@RequestHeader HttpHeaders headers, @RequestBody Flux parts ) { + + return parts + .ofType(FilePart.class) // We'll ignore other data for now + .flatMap((part) -> saveFile(headers, s3config.getBucket(), part)) + .collect(Collectors.toList()) + .map((keys) -> ResponseEntity.status(HttpStatus.CREATED) + .body(new UploadResult(HttpStatus.CREATED,keys))); + } + + + /** + * Save file using a multipart upload. This method does not require any temporary + * storage at the REST service + * @param headers + * @param bucket Bucket name + * @param part Uploaded file + * @return + */ + protected Mono saveFile(HttpHeaders headers,String bucket, FilePart part) { + + // Generate a filekey for this upload + String filekey = UUID.randomUUID().toString(); + + log.info("[I137] saveFile: filekey={}, filename={}", filekey, part.filename()); + + // Gather metadata + Map metadata = new HashMap(); + String filename = part.filename(); + if ( filename == null ) { + filename = filekey; + } + + metadata.put("filename", filename); + + MediaType mt = part.headers().getContentType(); + if ( mt == null ) { + mt = MediaType.APPLICATION_OCTET_STREAM; + } + + // Create multipart upload request + CompletableFuture uploadRequest = s3client + .createMultipartUpload(CreateMultipartUploadRequest.builder() + .contentType(mt.toString()) + .key(filekey) + .metadata(metadata) + .bucket(bucket) + .build()); + + // This variable will hold the upload state that we must keep + // around until all uploads complete + final UploadState uploadState = new UploadState(bucket,filekey); + + return Mono + .fromFuture(uploadRequest) + .flatMapMany((response) -> { + checkResult(response); + uploadState.uploadId = response.uploadId(); + log.info("[I183] uploadId={}", response.uploadId()); + return part.content(); + }) + .bufferUntil((buffer) -> { + uploadState.buffered += buffer.readableByteCount(); + if ( uploadState.buffered >= s3config.getMultipartMinPartSize() ) { + log.info("[I173] bufferUntil: returning true, bufferedBytes={}, partCounter={}, uploadId={}", uploadState.buffered, uploadState.partCounter, uploadState.uploadId); + uploadState.buffered = 0; + return true; + } + else { + return false; + } + }) + .map((buffers) -> concatBuffers(buffers)) + .flatMap((buffer) -> uploadPart(uploadState,buffer)) + .onBackpressureBuffer() + .reduce(uploadState,(state,completedPart) -> { + log.info("[I188] completed: partNumber={}, etag={}", completedPart.partNumber(), completedPart.eTag()); + state.completedParts.put(completedPart.partNumber(), completedPart); + return state; + }) + .flatMap((state) -> completeUpload(state)) + .map((response) -> { + checkResult(response); + return uploadState.filekey; + }); + } + + private static ByteBuffer concatBuffers(List buffers) { + log.info("[I198] creating BytBuffer from {} chunks", buffers.size()); + + int partSize = 0; + for( DataBuffer b : buffers) { + partSize += b.readableByteCount(); + } + + ByteBuffer partData = ByteBuffer.allocate(partSize); + buffers.forEach((buffer) -> { + partData.put(buffer.asByteBuffer()); + }); + + // Reset read pointer to first byte + partData.rewind(); + + log.info("[I208] partData: size={}", partData.capacity()); + return partData; + + } + + /** + * Upload a single file part to the requested bucket + * @param uploadState + * @param buffer + * @return + */ + private Mono uploadPart(UploadState uploadState, ByteBuffer buffer) { + final int partNumber = ++uploadState.partCounter; + log.info("[I218] uploadPart: partNumber={}, contentLength={}",partNumber, buffer.capacity()); + + CompletableFuture request = s3client.uploadPart(UploadPartRequest.builder() + .bucket(uploadState.bucket) + .key(uploadState.filekey) + .partNumber(partNumber) + .uploadId(uploadState.uploadId) + .contentLength((long) buffer.capacity()) + .build(), + AsyncRequestBody.fromPublisher(Mono.just(buffer))); + + return Mono + .fromFuture(request) + .map((uploadPartResult) -> { + checkResult(uploadPartResult); + log.info("[I230] uploadPart complete: part={}, etag={}",partNumber,uploadPartResult.eTag()); + return CompletedPart.builder() + .eTag(uploadPartResult.eTag()) + .partNumber(partNumber) + .build(); + }); + } + + private Mono completeUpload(UploadState state) { + log.info("[I202] completeUpload: bucket={}, filekey={}, completedParts.size={}", state.bucket, state.filekey, state.completedParts.size()); + + CompletedMultipartUpload multipartUpload = CompletedMultipartUpload.builder() + .parts(state.completedParts.values()) + .build(); + + return Mono.fromFuture(s3client.completeMultipartUpload(CompleteMultipartUploadRequest.builder() + .bucket(state.bucket) + .uploadId(state.uploadId) + .multipartUpload(multipartUpload) + .key(state.filekey) + .build())); + } + + + /** + * check result from an API call. + * @param result Result from an API call + */ + private static void checkResult(SdkResponse result) { + if (result.sdkHttpResponse() == null || !result.sdkHttpResponse().isSuccessful()) { + throw new UploadFailedException(result); + } + } + + + /** + * Holds upload state during a multipart upload + */ + static class UploadState { + final String bucket; + final String filekey; + + String uploadId; + int partCounter; + Map completedParts = new HashMap<>(); + int buffered = 0; + + UploadState(String bucket, String filekey) { + this.bucket = bucket; + this.filekey = filekey; + } + } + +} diff --git a/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/UploadResult.java b/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/UploadResult.java new file mode 100644 index 0000000000..642ad426a5 --- /dev/null +++ b/aws-reactive/src/main/java/com/baeldung/aws/reactive/s3/UploadResult.java @@ -0,0 +1,25 @@ +package com.baeldung.aws.reactive.s3; + +import java.util.List; + +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@AllArgsConstructor +@Builder +public class UploadResult { + HttpStatus status; + String[] keys; + + public UploadResult() {} + + public UploadResult(HttpStatus status, List keys) { + this.status = status; + this.keys = keys == null ? new String[] {}: keys.toArray(new String[] {}); + + } +} diff --git a/aws-reactive/src/main/resources/application-minio.yml b/aws-reactive/src/main/resources/application-minio.yml new file mode 100644 index 0000000000..93bc2ff18b --- /dev/null +++ b/aws-reactive/src/main/resources/application-minio.yml @@ -0,0 +1,15 @@ + +# +# Minio profile +# +aws: + s3: + region: sa-east-1 + endpoint: http://localhost:9000 + accessKeyId: 8KLF8U60JER4AP23H0A6 + secretAccessKey: vX4uM7e7nNGPqjcXycVVhceNR7NQkiMQkR9Hoctf + bucket: bucket1 + + + + diff --git a/aws-reactive/src/main/resources/application.yml b/aws-reactive/src/main/resources/application.yml new file mode 100644 index 0000000000..957ebf82c3 --- /dev/null +++ b/aws-reactive/src/main/resources/application.yml @@ -0,0 +1,16 @@ + +# +# Configurações de acesso ao Minio +# +aws: + s3: + region: sa-east-1 +# When using AWS, the library will use one of the available +# credential sources described in the documentation. +# accessKeyId: **** +# secretAccessKey: **** + bucket: dev1.token.com.br + + + + diff --git a/aws-reactive/src/test/java/com/baeldung/aws/reactive/s3/ReactiveS3ApplicationLiveTest.java b/aws-reactive/src/test/java/com/baeldung/aws/reactive/s3/ReactiveS3ApplicationLiveTest.java new file mode 100644 index 0000000000..9e5720225f --- /dev/null +++ b/aws-reactive/src/test/java/com/baeldung/aws/reactive/s3/ReactiveS3ApplicationLiveTest.java @@ -0,0 +1,85 @@ +package com.baeldung.aws.reactive.s3; + +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.core.io.Resource; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ResourceUtils; + +import static org.junit.Assert.*; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ActiveProfiles("minio") +class ReactiveS3ApplicationLiveTest { + + @Autowired + private TestRestTemplate restTemplate; + + @LocalServerPort + private int serverPort; + + + @Test + void whenUploadSingleFile_thenSuccess() throws Exception { + + String url = "http://localhost:" + serverPort + "/inbox"; + byte[] data = Files.readAllBytes(Paths.get("src/test/resources/testimage1.png")); + UploadResult result = restTemplate.postForObject(url, data , UploadResult.class); + + assertEquals("Expected CREATED (202)", result.getStatus(), HttpStatus.CREATED ); + + } + + @Test + void whenUploadMultipleFiles_thenSuccess() throws Exception { + + + MultiValueMap body = new LinkedMultiValueMap<>(); + addFileEntity("f1", body, new File("src/test/resources/testimage1.png")); + addFileEntity("f2", body, new File("src/test/resources/testimage2.png")); + + HttpEntity> requestEntity = new HttpEntity<>(body); + String url = "http://localhost:" + serverPort + "/inbox"; + + ResponseEntity result = restTemplate.postForEntity(url, requestEntity, UploadResult.class); + + assertEquals("Http Code",HttpStatus.CREATED, result.getStatusCode() ); + assertEquals("File keys",2, result.getBody().getKeys().length); + + } + + private void addFileEntity(String name, MultiValueMap body, File file) throws Exception { + + byte[] data = Files.readAllBytes(file.toPath()); + MultiValueMap headers = new LinkedMultiValueMap<>(); + ContentDisposition contentDispositionHeader = ContentDisposition.builder("form-data") + .name(name) + .filename(file.getName()) + .build(); + + headers.add(HttpHeaders.CONTENT_DISPOSITION, contentDispositionHeader.toString()); + + HttpEntity fileEntity = new HttpEntity<>(data, headers); + body.add(name, fileEntity); + } + + +} diff --git a/aws-reactive/src/test/resources/testimage1.png b/aws-reactive/src/test/resources/testimage1.png new file mode 100644 index 0000000000000000000000000000000000000000..c61a9b677f054b5c6bf6e798440d8791ceda6a66 GIT binary patch literal 4315 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