Skip to content

Commit

Permalink
[MERGE/#59] 사진 업로드를 위한 Presigned URL 구현
Browse files Browse the repository at this point in the history
[FEAT] #59 - 사진 업로드를 위한 Presigned URL 구현
  • Loading branch information
seokbeom00 authored Jul 11, 2024
2 parents 7328887 + a5ddd5f commit f93f5bf
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 22 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ dependencies {

// S3
implementation 'software.amazon.awssdk:s3:2.17.0'
implementation 'com.h2database:h2'
}

ext {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
Expand All @@ -44,25 +41,6 @@ public class GoogleMeetConfig {
@Value("${aws-property.s3-bucket-name}")
private String bucketName;

@Value("${aws-property.access-key}")
private String accessKeyId;

@Value("${aws-property.secret-key}")
private String secretAccessKey;

@Value("${aws-property.aws-region}")
private String awsRegion;

@Bean
public S3Client s3Client() {
return S3Client.builder()
.region(Region.of(awsRegion))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKeyId, secretAccessKey)
))
.build();
}

private static final String USER = "default";

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.sopt.seonyakServer.global.common.external.s3;

import lombok.RequiredArgsConstructor;
import org.sopt.seonyakServer.global.common.external.s3.dto.PreSignedUrlResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class S3Controller {
private final S3Service s3Service;

@GetMapping("/image")
public ResponseEntity<PreSignedUrlResponse> getPreSignedUrl() {
return ResponseEntity.ok(s3Service.getUploadPreSignedUrl());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.sopt.seonyakServer.global.common.external.s3;

import java.net.URL;
import java.time.Duration;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.sopt.seonyakServer.global.common.external.s3.dto.PreSignedUrlResponse;
import org.sopt.seonyakServer.global.exception.enums.ErrorType;
import org.sopt.seonyakServer.global.exception.model.CustomException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;

@Component
@RequiredArgsConstructor
public class S3Service {

@Value("${aws-property.s3-bucket-name}")
private String bucketName;

private final S3Client s3Client;
private final S3Presigner s3Presigner;

// PreSigned URL 만료시간 60분
private static final Long PRE_SIGNED_URL_EXPIRE_MINUTE = 60L;
private final static String imagePath = "profiles/";

public PreSignedUrlResponse getUploadPreSignedUrl() {
try {
// UUID 파일명 생성
String uuidFileName = UUID.randomUUID().toString() + ".jpg";
// 경로 + 파일 이름
String key = imagePath + uuidFileName;

PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();

// S3에서 업로드는 PUT 요청
PutObjectPresignRequest preSignedUrlRequest = PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(PRE_SIGNED_URL_EXPIRE_MINUTE))
.putObjectRequest(putObjectRequest)
.build();

// Persigned URL 생성
URL url = s3Presigner.presignPutObject(preSignedUrlRequest).url();

return PreSignedUrlResponse.of(uuidFileName, url.toString());

} catch (RuntimeException e) {
throw new CustomException(ErrorType.GET_UPLOAD_PRESIGNED_URL_ERROR);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.sopt.seonyakServer.global.common.external.s3.dto;

public record PreSignedUrlResponse(
String fileName,
String url
) {
public static PreSignedUrlResponse of(String fileName, String url) {
return new PreSignedUrlResponse(fileName, url);
}
}
43 changes: 43 additions & 0 deletions src/main/java/org/sopt/seonyakServer/global/config/S3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.sopt.seonyakServer.global.config;

import org.springframework.beans.factory.annotation.Value;
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.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;

@Configuration
public class S3Config {

@Value("${aws-property.access-key}")
private String accessKeyId;

@Value("${aws-property.secret-key}")
private String secretAccessKey;

@Value("${aws-property.aws-region}")
private String awsRegion;

@Bean
public S3Client s3Client() {
return S3Client.builder()
.region(Region.of(awsRegion))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKeyId, secretAccessKey)
))
.build();
}

@Bean
public S3Presigner getS3Presigner() {
return S3Presigner.builder()
.region(Region.of(awsRegion))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKeyId, secretAccessKey)
))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.sopt.seonyakServer.global.common.external.s3;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.sopt.seonyakServer.global.common.external.s3.dto.PreSignedUrlResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@ActiveProfiles("test")
class S3ServiceIntegrationTest {

@Autowired
private S3Service s3Service;

@Test
void testGetUploadPreSignedUrl() throws Exception {
// Pre-Signed URL 생성
PreSignedUrlResponse response = s3Service.getUploadPreSignedUrl();

// Assertions: Pre-Signed URL이 유효한지 확인
assertThat(response).isNotNull();
assertThat(response.fileName()).isNotEmpty();
assertThat(response.url()).isNotEmpty();

// 이미지 파일 읽기
File file = new File("src/test/resources/경희대학교_재학증명서.pdf");
byte[] imageData;
try (InputStream inputStream = new FileInputStream(file)) {
imageData = inputStream.readAllBytes();
}

// Pre-Signed URL로 PUT 요청 보내기
URL url = new URL(response.url());
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("PUT");
connection.setRequestProperty("Content-Type", "application/pdf");
connection.setRequestProperty("Content-Length", String.valueOf(imageData.length));

// 이미지 데이터 전송
try (InputStream inputStream = new ByteArrayInputStream(imageData)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
connection.getOutputStream().write(buffer, 0, bytesRead);
}
}

int responseCode = connection.getResponseCode();
connection.disconnect();

// Assertions: HTTP 응답 코드가 200 (성공)인지 확인
assertThat(responseCode).isEqualTo(200);
}
}

0 comments on commit f93f5bf

Please sign in to comment.