Merge pull request #16462 from ukhan1980/bael-6760-download-file-from-s3

[bael-6760] add code for download files from s3 article
This commit is contained in:
davidmartinezbarua 2024-04-22 19:44:46 -03:00 committed by GitHub
commit b2efaa0112
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 489 additions and 0 deletions

View File

@ -0,0 +1,9 @@
## AWS SpringBoot Rest
This module contains articles about AWS access in Spring boot Rest APIs
### Relevant Articles:

View File

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.baeldung</groupId>
<artifactId>aws-rest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>aws-rest</name>
<description>AWS Rest S3 Sample</description>
<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>${awssdk.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit-jupiter.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.13.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<properties>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
<spring.version>2.2.1.RELEASE</spring.version>
<awssdk.version>2.20.45</awssdk.version>
<lombok.version>1.18.20</lombok.version>
<junit-jupiter.version>5.5.2</junit-jupiter.version>
</properties>
</project>

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<GetObjectResponse> 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);
}
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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<byte[]> 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();
}
}
}

View File

@ -0,0 +1,6 @@
aws:
accessKeyId: YOUR_ACCESS_KEY_ID
secretAccessKey: YOUR_SECRET_ACCESS_KEY

View File

@ -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"));
}
}

View File

@ -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());
}
}

View File

@ -20,6 +20,7 @@
<module>aws-miscellaneous</module>
<module>aws-reactive</module>
<module>aws-s3</module>
<module>aws-rest</module>
</modules>
<dependencies>