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:
commit
b2efaa0112
|
@ -0,0 +1,9 @@
|
|||
## AWS SpringBoot Rest
|
||||
|
||||
This module contains articles about AWS access in Spring boot Rest APIs
|
||||
|
||||
### Relevant Articles:
|
||||
|
||||
|
||||
|
||||
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
aws:
|
||||
accessKeyId: YOUR_ACCESS_KEY_ID
|
||||
secretAccessKey: YOUR_SECRET_ACCESS_KEY
|
||||
|
||||
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@
|
|||
<module>aws-miscellaneous</module>
|
||||
<module>aws-reactive</module>
|
||||
<module>aws-s3</module>
|
||||
<module>aws-rest</module>
|
||||
</modules>
|
||||
|
||||
<dependencies>
|
||||
|
|
Loading…
Reference in New Issue