diff --git a/pom.xml b/pom.xml
index 2d8e109..0766dd1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,6 +31,11 @@
org.springframework.boot
spring-boot-starter-log4j2
+
+ software.amazon.awssdk
+ s3
+ 2.25.70
+
org.springframework.boot
spring-boot-starter-test
diff --git a/src/main/java/com/app/config/AwsS3Config.java b/src/main/java/com/app/config/AwsS3Config.java
new file mode 100644
index 0000000..37657fe
--- /dev/null
+++ b/src/main/java/com/app/config/AwsS3Config.java
@@ -0,0 +1,46 @@
+package com.app.config;
+
+
+import lombok.extern.log4j.Log4j2;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+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.StaticCredentialsProvider;
+import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3AsyncClient;
+
+@Configuration
+@ConditionalOnProperty(name = "aws.s3.enabled", havingValue = "true")
+@Log4j2
+public class AwsS3Config {
+
+ @Value("${aws.s3.region:us-east-1}")
+ private String awsRegion;
+ @Value("${aws.s3.accessKeyId:us-east-1}")
+ private String accessKeyId;
+ @Value("${aws.s3.secretAccessKey}")
+ private String secretAccessKey;
+
+ @Bean
+ public S3AsyncClient s3Client() {
+ try {
+ log.info("Trying to S3Client create.");
+
+ return S3AsyncClient.builder()
+ .region(Region.of(awsRegion))
+ .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKeyId, secretAccessKey)))
+ .overrideConfiguration(ClientOverrideConfiguration.builder()
+ .retryPolicy(r -> r.numRetries(3))
+ .build())
+ .build();
+ } catch (Exception e) {
+ log.error("Failed to create S3AsyncClient.", e);
+ throw e;
+ } finally {
+ log.info("S3AsyncClient created successfully.");
+ }
+ }
+}
diff --git a/src/main/java/com/app/controller/S3Controller.java b/src/main/java/com/app/controller/S3Controller.java
new file mode 100644
index 0000000..f395121
--- /dev/null
+++ b/src/main/java/com/app/controller/S3Controller.java
@@ -0,0 +1,62 @@
+package com.app.controller;
+
+import com.app.service.S3Service;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.concurrent.CompletableFuture;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/files")
+public class S3Controller {
+
+ private final S3Service s3Service;
+
+ @PostMapping("/upload")
+ public ResponseEntity uploadFile(@RequestPart("file") MultipartFile file,
+ @RequestParam(value = "isReadPublicly", defaultValue = "false") boolean isReadPublicly) {
+ boolean isUploaded = s3Service.uploadFile(file, isReadPublicly);
+ if (isUploaded) {
+ return ResponseEntity.ok("File uploaded successfully: " + file.getOriginalFilename());
+ } else {
+ return ResponseEntity.status(500).body("Failed to upload file: " + file.getOriginalFilename());
+ }
+ }
+
+ @GetMapping("/download/{key}")
+ public StreamingResponseBody downloadFile(@PathVariable String key, HttpServletResponse httpResponse) {
+
+ httpResponse.setContentType("application/octet-stream");
+ httpResponse.setHeader("Content-Disposition", String.format("inline; filename=\"%s\"", key));
+
+ CompletableFuture byteArrayInputStreamCompletableFuture = s3Service.downloadFileAsStream(key);
+
+ return outputStream -> {
+ ByteArrayInputStream byteArrayInputStream = byteArrayInputStreamCompletableFuture.join();
+ if (byteArrayInputStream != null) {
+ byte[] buffer = new byte[8192];
+ int bytesRead;
+ while ((bytesRead = byteArrayInputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ outputStream.flush();
+ } else {
+ // Handle the case where the stream is null
+ httpResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ String errorMessage = "Failed to download the key. Please try again later.";
+ outputStream.write(errorMessage.getBytes());
+ outputStream.flush();
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/app/service/S3Service.java b/src/main/java/com/app/service/S3Service.java
new file mode 100644
index 0000000..775dd0f
--- /dev/null
+++ b/src/main/java/com/app/service/S3Service.java
@@ -0,0 +1,14 @@
+package com.app.service;
+
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.ByteArrayInputStream;
+import java.util.concurrent.CompletableFuture;
+
+public interface S3Service {
+
+
+ boolean uploadFile(MultipartFile file, boolean isReadPublicly);
+
+ CompletableFuture downloadFileAsStream(String key);
+}
diff --git a/src/main/java/com/app/service/S3ServiceImpl.java b/src/main/java/com/app/service/S3ServiceImpl.java
new file mode 100644
index 0000000..d47fd12
--- /dev/null
+++ b/src/main/java/com/app/service/S3ServiceImpl.java
@@ -0,0 +1,95 @@
+package com.app.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+import software.amazon.awssdk.core.async.AsyncRequestBody;
+import software.amazon.awssdk.core.async.AsyncResponseTransformer;
+import software.amazon.awssdk.services.s3.S3AsyncClient;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.transfer.s3.S3TransferManager;
+import software.amazon.awssdk.transfer.s3.model.CompletedUpload;
+import software.amazon.awssdk.transfer.s3.model.Upload;
+import software.amazon.awssdk.transfer.s3.model.UploadRequest;
+import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.concurrent.CompletableFuture;
+
+@Service
+@Log4j2
+@RequiredArgsConstructor
+public class S3ServiceImpl implements S3Service {
+
+ private final S3AsyncClient s3AsyncClient;
+
+ @Value("${aws.s3.bucket-name}")
+ private String bucketName;
+
+
+ private S3TransferManager createTransferManager() {
+ return S3TransferManager.builder()
+ .s3Client(s3AsyncClient)
+ .build();
+ }
+
+
+ @Override
+ public boolean uploadFile(MultipartFile file, boolean isReadPublicly) {
+ log.info("Started uploading file '{}' to S3 Bucket '{}'", file.getOriginalFilename(), bucketName);
+ try (S3TransferManager transferManager = createTransferManager()) {
+ UploadRequest uploadRequest;
+ if (isReadPublicly) {
+ uploadRequest = UploadRequest.builder()
+ .putObjectRequest(builder -> builder.bucket(bucketName).key(file.getOriginalFilename()).acl("public-read"))
+ .requestBody(AsyncRequestBody.fromBytes(file.getBytes()))
+ .addTransferListener(LoggingTransferListener.create()) // For logging progress
+ .build();
+ } else {
+ uploadRequest = UploadRequest.builder()
+ .putObjectRequest(builder -> builder.bucket(bucketName).key(file.getOriginalFilename()))
+ .requestBody(AsyncRequestBody.fromBytes(file.getBytes()))
+ .addTransferListener(LoggingTransferListener.create()) // For logging progress
+ .build();
+ }
+ // Start the file upload
+ Upload upload = transferManager.upload(uploadRequest);
+
+ // Wait for the upload to complete
+ CompletableFuture uploadCompletion = upload.completionFuture();
+ uploadCompletion.join();
+ log.info("Successfully uploaded file to S3. Bucket: {}, Key: {}", bucketName, file.getOriginalFilename());
+ return true;
+ } catch (Exception e) {
+ log.error("Failed to upload file to S3. Bucket: {}, Key: {}", bucketName, file.getOriginalFilename(), e);
+ return false;
+ }
+ }
+
+ @Override
+ public CompletableFuture downloadFileAsStream(String key) {
+ GetObjectRequest getObjectRequest = GetObjectRequest.builder()
+ .bucket(bucketName)
+ .key(key)
+ .build();
+
+ // Download the file directly into a ByteArrayOutputStream
+ return s3AsyncClient.getObject(getObjectRequest, AsyncResponseTransformer.toBytes())
+ .thenApply(response -> {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ try {
+ byteArrayOutputStream.write(response.asByteArray());
+ } catch (Exception e) {
+ log.error("Failed to write response to ByteArrayOutputStream. Bucket: {}, Key: {}", bucketName, key, e);
+ }
+ return new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
+ })
+ .exceptionally(e -> {
+ log.error("Failed to download file from S3. Bucket: {}, Key: {}", bucketName, key, e);
+ return null;
+ });
+ }
+}
diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml
index d2f8b2d..11b1e06 100644
--- a/src/main/resources/application-local.yml
+++ b/src/main/resources/application-local.yml
@@ -2,3 +2,10 @@ app:
logs:
path: C:/${spring.application.name}/logs
+aws:
+ s3:
+ enabled: true
+ region: us-east-1
+ accessKeyId: <>
+ secretAccessKey: <>
+ bucket-name: <>
\ No newline at end of file