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