diff --git a/aws-modules/aws-rest/README.md b/aws-modules/aws-rest/README.md
new file mode 100644
index 0000000000..2ecc63a918
--- /dev/null
+++ b/aws-modules/aws-rest/README.md
@@ -0,0 +1,9 @@
+## AWS SpringBoot Rest
+
+This module contains articles about AWS access in Spring boot Rest APIs
+
+### Relevant Articles:
+
+
+
+
diff --git a/aws-modules/aws-rest/pom.xml b/aws-modules/aws-rest/pom.xml
new file mode 100644
index 0000000000..1b220ad253
--- /dev/null
+++ b/aws-modules/aws-rest/pom.xml
@@ -0,0 +1,109 @@
+
+
+ 4.0.0
+ com.baeldung
+ aws-rest
+ 0.0.1-SNAPSHOT
+ aws-rest
+ AWS Rest S3 Sample
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring.version}
+ pom
+ import
+
+
+ software.amazon.awssdk
+ bom
+ ${awssdk.version}
+ pom
+ import
+
+
+ org.junit
+ junit-bom
+ ${junit-jupiter.version}
+ pom
+ import
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ software.amazon.awssdk
+ s3
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.junit.vintage
+ junit-vintage-engine
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ commons-io
+ commons-io
+ 2.13.0
+ compile
+
+
+ software.amazon.awssdk
+ s3
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+ 11
+ 11
+ 2.2.1.RELEASE
+ 2.20.45
+ 1.18.20
+ 5.5.2
+
+
+
\ No newline at end of file
diff --git a/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/config/S3Config.java b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/config/S3Config.java
new file mode 100644
index 0000000000..8048c0d4cf
--- /dev/null
+++ b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/config/S3Config.java
@@ -0,0 +1,21 @@
+package com.baeldung.aws.rest.s3.download.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+
+@Configuration
+public class S3Config {
+
+ @Bean
+ public S3Client s3Client() {
+ return S3Client.builder()
+ .region(Region.US_EAST_1)
+ .credentialsProvider(DefaultCredentialsProvider.create())
+ .build();
+ }
+}
+
diff --git a/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/dto/FileData.java b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/dto/FileData.java
new file mode 100644
index 0000000000..3f8bf5a91c
--- /dev/null
+++ b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/dto/FileData.java
@@ -0,0 +1,16 @@
+package com.baeldung.aws.rest.s3.download.dto;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+
+@RequiredArgsConstructor
+@Data
+@Builder
+public class FileData {
+ private final byte[] fileContent;
+ private final String contentType;
+ private final String contentDisposition;
+ private final GetObjectRequest request;
+}
diff --git a/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/dto/FileDownloadResponse.java b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/dto/FileDownloadResponse.java
new file mode 100644
index 0000000000..4d85542d7d
--- /dev/null
+++ b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/dto/FileDownloadResponse.java
@@ -0,0 +1,14 @@
+package com.baeldung.aws.rest.s3.download.dto;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+
+@Builder
+@Data
+@RequiredArgsConstructor
+public class FileDownloadResponse {
+ private final byte[] fileContent;
+ private final String originalFilename;
+ private final String contentType;
+}
diff --git a/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/dto/FileReader.java b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/dto/FileReader.java
new file mode 100644
index 0000000000..ac8836f776
--- /dev/null
+++ b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/dto/FileReader.java
@@ -0,0 +1,7 @@
+package com.baeldung.aws.rest.s3.download.dto;
+
+import java.io.IOException;
+
+public interface FileReader {
+ FileData readResponse(S3ObjectRequest s3ObjectRequest) throws IOException;
+}
diff --git a/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/dto/S3ObjectRequest.java b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/dto/S3ObjectRequest.java
new file mode 100644
index 0000000000..2a2871a1a1
--- /dev/null
+++ b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/dto/S3ObjectRequest.java
@@ -0,0 +1,12 @@
+package com.baeldung.aws.rest.s3.download.dto;
+
+import lombok.Builder;
+import lombok.Data;
+
+@Builder
+@Data
+public class S3ObjectRequest {
+ private final String bucketName;
+ private final String objectKey;
+}
+
diff --git a/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/dto/S3ResponseReader.java b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/dto/S3ResponseReader.java
new file mode 100644
index 0000000000..00e326bad8
--- /dev/null
+++ b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/dto/S3ResponseReader.java
@@ -0,0 +1,44 @@
+package com.baeldung.aws.rest.s3.download.dto;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.springframework.stereotype.Component;
+
+import software.amazon.awssdk.core.ResponseInputStream;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.model.GetObjectResponse;
+
+@Component
+public class S3ResponseReader implements FileReader {
+
+ private final S3Client s3Client;
+
+ public S3ResponseReader(S3Client s3Client) {
+ this.s3Client = s3Client;
+ }
+
+ @Override
+ public FileData readResponse(S3ObjectRequest s3ObjectRequest) throws IOException {
+ GetObjectRequest getObjectRequest = GetObjectRequest.builder()
+ .bucket(s3ObjectRequest.getBucketName())
+ .key(s3ObjectRequest.getObjectKey())
+ .build();
+ try (ResponseInputStream responseInputStream = s3Client.getObject(getObjectRequest)) {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ int bytesRead;
+ while ((bytesRead = responseInputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ byte[] fileContent = outputStream.toByteArray();
+ String contentType = responseInputStream.response()
+ .contentType();
+ String contentDisposition = responseInputStream.response()
+ .contentDisposition();
+ return new FileData(fileContent, contentType, contentDisposition, getObjectRequest);
+ }
+ }
+}
+
diff --git a/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/service/FileService.java b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/service/FileService.java
new file mode 100644
index 0000000000..ddb9738f24
--- /dev/null
+++ b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/service/FileService.java
@@ -0,0 +1,11 @@
+package com.baeldung.aws.rest.s3.download.service;
+
+import java.io.IOException;
+
+import com.baeldung.aws.rest.s3.download.dto.FileDownloadResponse;
+
+import software.amazon.awssdk.services.s3.model.S3Exception;
+
+public interface FileService {
+ FileDownloadResponse downloadFile(String url) throws IOException, S3Exception;
+}
diff --git a/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/service/S3FileServiceImpl.java b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/service/S3FileServiceImpl.java
new file mode 100644
index 0000000000..11a0099b65
--- /dev/null
+++ b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/service/S3FileServiceImpl.java
@@ -0,0 +1,72 @@
+package com.baeldung.aws.rest.s3.download.service;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.springframework.stereotype.Service;
+
+import com.baeldung.aws.rest.s3.download.dto.FileData;
+import com.baeldung.aws.rest.s3.download.dto.FileDownloadResponse;
+import com.baeldung.aws.rest.s3.download.dto.FileReader;
+import com.baeldung.aws.rest.s3.download.dto.S3ObjectRequest;
+
+import lombok.RequiredArgsConstructor;
+import software.amazon.awssdk.services.s3.model.S3Exception;
+
+@Service
+@RequiredArgsConstructor
+public class S3FileServiceImpl implements FileService {
+
+ private final FileReader s3ResponseReader;
+
+ @Override
+ public FileDownloadResponse downloadFile(String s3Url) {
+ try {
+ // Parse the S3 URL
+ URI uri = new URI(s3Url);
+
+ // Extract bucket name and object key
+ String bucketName = uri.getHost();
+ String objectKey = uri.getPath()
+ .substring(1); // Remove leading "/"
+
+ S3ObjectRequest s3Request = S3ObjectRequest.builder()
+ .bucketName(bucketName)
+ .objectKey(objectKey)
+ .build();
+
+ FileData s3Response = s3ResponseReader.readResponse(s3Request);
+
+ // Get object metadata
+ String contentType = s3Response.getContentType();
+ String contentDisposition = s3Response.getContentDisposition();
+ byte[] fileContent = s3Response.getFileContent();
+ String key = s3Response.getRequest()
+ .key();
+ String filename = extractFilenameFromKey(key);
+
+ String originalFilename =
+ contentDisposition == null ? filename : contentDisposition.substring(contentDisposition.indexOf("=") + 1);
+
+ return FileDownloadResponse.builder()
+ .fileContent(fileContent)
+ .originalFilename(originalFilename)
+ .contentType(contentType)
+ .build();
+ } catch (IOException | S3Exception e) {
+ e.printStackTrace();
+ return null;
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String extractFilenameFromKey(String objectKey) {
+ int lastSlashIndex = objectKey.lastIndexOf('/');
+ if (lastSlashIndex != -1 && lastSlashIndex < objectKey.length() - 1) {
+ return objectKey.substring(lastSlashIndex + 1);
+ }
+ return objectKey;
+ }
+}
diff --git a/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/web/FileController.java b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/web/FileController.java
new file mode 100644
index 0000000000..05505f035a
--- /dev/null
+++ b/aws-modules/aws-rest/src/main/java/com/baeldung/aws/rest/s3/download/web/FileController.java
@@ -0,0 +1,55 @@
+package com.baeldung.aws.rest.s3.download.web;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+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 org.springframework.web.util.UriUtils;
+
+import com.baeldung.aws.rest.s3.download.dto.FileDownloadResponse;
+import com.baeldung.aws.rest.s3.download.service.FileService;
+
+import lombok.RequiredArgsConstructor;
+
+@RestController
+@RequestMapping("/api/files")
+@RequiredArgsConstructor
+public class FileController {
+
+ private final FileService fileService;
+
+ @GetMapping(value = "/download/{encodedUrl}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+ public ResponseEntity downloadFile(@PathVariable String encodedUrl) throws IOException {
+ String s3Url = UriUtils.decode(encodedUrl, StandardCharsets.UTF_8);
+ FileDownloadResponse fileDownloadResponse = fileService.downloadFile(s3Url);
+
+ if (fileDownloadResponse != null) {
+ try {
+ byte[] fileContent = fileDownloadResponse.getFileContent();
+ String originalFilename = fileDownloadResponse.getOriginalFilename();
+ String contentType = fileDownloadResponse.getContentType();
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.parseMediaType(contentType));
+ headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + originalFilename);
+
+ return ResponseEntity.ok()
+ .headers(headers)
+ .body(fileContent);
+ } catch (Exception e) {
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .body(null);
+ }
+ } else {
+ return ResponseEntity.notFound()
+ .build();
+ }
+ }
+}
\ No newline at end of file
diff --git a/aws-modules/aws-rest/src/main/resources/application.yml b/aws-modules/aws-rest/src/main/resources/application.yml
new file mode 100644
index 0000000000..7572c0a2f7
--- /dev/null
+++ b/aws-modules/aws-rest/src/main/resources/application.yml
@@ -0,0 +1,6 @@
+
+aws:
+ accessKeyId: YOUR_ACCESS_KEY_ID
+ secretAccessKey: YOUR_SECRET_ACCESS_KEY
+
+
diff --git a/aws-modules/aws-rest/src/test/java/com/baeldung/aws/rest/s3/download/FileControllerUnitTest.java b/aws-modules/aws-rest/src/test/java/com/baeldung/aws/rest/s3/download/FileControllerUnitTest.java
new file mode 100644
index 0000000000..bbed557106
--- /dev/null
+++ b/aws-modules/aws-rest/src/test/java/com/baeldung/aws/rest/s3/download/FileControllerUnitTest.java
@@ -0,0 +1,54 @@
+package com.baeldung.aws.rest.s3.download;
+
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
+
+import com.baeldung.aws.rest.s3.download.dto.FileDownloadResponse;
+import com.baeldung.aws.rest.s3.download.service.S3FileServiceImpl;
+import com.baeldung.aws.rest.s3.download.web.FileController;
+
+@SpringBootTest(classes = { FileController.class, S3FileServiceImpl.class })
+@AutoConfigureMockMvc
+public class FileControllerUnitTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @MockBean
+ private S3FileServiceImpl fileService;
+
+ @Test
+ public void givenAnEncodedS3URL_whenRequestSentToGet_thenReturnsFiletoTheClientAsAttachmentWithTheOriginalNameAndExtension() throws Exception {
+ // Mock FileDownloadResponse
+ byte[] fileContent = "Test content".getBytes();
+ String originalFilename = "test.txt";
+ String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
+
+ FileDownloadResponse mockResponse = new FileDownloadResponse(fileContent, originalFilename, contentType);
+ when(fileService.downloadFile("mocked-s3-url")).thenReturn(mockResponse);
+
+ // Perform request
+ MockHttpServletRequestBuilder requestBuilder = get("/api/files/download/{encodedUrl}", "mocked-s3-url");
+
+ mockMvc.perform(requestBuilder)
+ .andDo(MockMvcResultHandlers.print())
+ .andExpect(status().isOk())
+ .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=test.txt"))
+ .andExpect(header().string(HttpHeaders.CONTENT_TYPE, "application/octet-stream"))
+ .andExpect(content().string("Test content"));
+ }
+}
diff --git a/aws-modules/aws-rest/src/test/java/com/baeldung/aws/rest/s3/download/S3FileServiceImplUnitTest.java b/aws-modules/aws-rest/src/test/java/com/baeldung/aws/rest/s3/download/S3FileServiceImplUnitTest.java
new file mode 100644
index 0000000000..38c7c80c56
--- /dev/null
+++ b/aws-modules/aws-rest/src/test/java/com/baeldung/aws/rest/s3/download/S3FileServiceImplUnitTest.java
@@ -0,0 +1,58 @@
+package com.baeldung.aws.rest.s3.download;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import com.baeldung.aws.rest.s3.download.dto.FileData;
+import com.baeldung.aws.rest.s3.download.dto.FileDownloadResponse;
+import com.baeldung.aws.rest.s3.download.dto.S3ObjectRequest;
+import com.baeldung.aws.rest.s3.download.dto.S3ResponseReader;
+import com.baeldung.aws.rest.s3.download.service.S3FileServiceImpl;
+
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+
+@ExtendWith(SpringExtension.class)
+public class S3FileServiceImplUnitTest {
+
+ @MockBean
+ private S3ResponseReader s3ResponseReader;
+
+ private S3FileServiceImpl fileService;
+
+ @Test
+ public void givenAStringURL_whenDownloadFileService_thenReturnsFileDataAndMetaData() throws IOException {
+ fileService = new S3FileServiceImpl(s3ResponseReader);
+ // Mock S3 URL
+ String s3Url = "s3://my-bucket/test.txt";
+
+ // Mock response from FileReader
+ byte[] expectedFileContent = "Mock file content".getBytes();
+ String expectedContentType = "application/octet-stream";
+ String expectedContentDisposition = "attachment; filename=test.txt";
+
+ FileData mockFileData = new FileData(expectedFileContent, expectedContentType, expectedContentDisposition,
+ GetObjectRequest.builder()
+ .bucket("my-bucket")
+ .key("test.txt")
+ .build());
+ when(s3ResponseReader.readResponse(any(S3ObjectRequest.class))).thenReturn(mockFileData);
+
+ // Perform the download
+ FileDownloadResponse fileDownloadResponse = fileService.downloadFile(s3Url);
+
+ // Assert the file content
+ assertEquals("Mock file content", new String(fileDownloadResponse.getFileContent()));
+
+ // Assert metadata
+ assertEquals(expectedContentType, fileDownloadResponse.getContentType());
+ assertEquals("test.txt", fileDownloadResponse.getOriginalFilename());
+ }
+}
diff --git a/aws-modules/pom.xml b/aws-modules/pom.xml
index c6bf59c1b2..be80bd96cd 100644
--- a/aws-modules/pom.xml
+++ b/aws-modules/pom.xml
@@ -20,6 +20,7 @@
aws-miscellaneous
aws-reactive
aws-s3
+ aws-rest