Skip to content

Commit

Permalink
[merge] S3 설정 추가
Browse files Browse the repository at this point in the history
[feat] S3 설정 추가
  • Loading branch information
sebbbin authored Jul 9, 2024
2 parents e5571a8 + 3da197e commit cfd9466
Show file tree
Hide file tree
Showing 15 changed files with 449 additions and 4 deletions.
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ dependencies {
implementation platform("org.springframework.cloud:spring-cloud-dependencies:2023.0.1")
implementation "org.springframework.cloud:spring-cloud-starter-openfeign"

//Multipart file
implementation("software.amazon.awssdk:bom:2.21.0")
implementation("software.amazon.awssdk:s3:2.21.0")
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

// QUERYDSL
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
implementation 'com.querydsl:querydsl-apt:5.0.0:jakarta'
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/org/recordy/server/common/message/ErrorMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ public enum ErrorMessage {
VALIDATION_REQUEST_MISSING_EXCEPTION(HttpStatus.BAD_REQUEST, "요청 값이 유효하지 않습니다."),
INVALID_REQUEST_TERM(HttpStatus.BAD_REQUEST, "필수 동의항목에 모두 동의해주세요"),

/**
* EXTERNAL
*/
INVALID_FILE_SIZE(HttpStatus.BAD_REQUEST,"파일 사이즈는 100MB를 넘을 수 없습니다."),
INVALID_FILE_TYPE(HttpStatus.BAD_REQUEST,"이미지 확장자는 jpg, png, webp만, 비디오 확장자는 mp4, mov, quicktime만 가능합니다."),

/**
* AUTH
*/
Expand Down Expand Up @@ -49,6 +55,12 @@ public enum ErrorMessage {
DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "이미 존재하는 닉네임입니다."),
DUPLICATE_USER(HttpStatus.CONFLICT, "이미 존재하는 회원입니다."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."),

/**
* USER
*/
FAILED_TO_UPLOAD_TO_S3(HttpStatus.INTERNAL_SERVER_ERROR, "S3에 업로드를 실패했습니다."),

;

private final HttpStatus httpStatus;
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/org/recordy/server/external/config/S3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.recordy.server.external.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;

@Configuration
public class S3Config {

private final String accessKey;
private final String secretKey;
private final String regionString;

public S3Config(@Value("${aws-property.access-key}") final String accessKey,
@Value("${aws-property.secret-key}") final String secretKey,
@Value("${aws-property.aws-region}") final String regionString) {
this.accessKey = accessKey;
this.secretKey = secretKey;
this.regionString = regionString;
}

@Bean
public S3Client getS3Client() {
AwsBasicCredentials awsCreds = AwsBasicCredentials.create(accessKey, secretKey);
return S3Client.builder()
.region(Region.of(regionString))
.credentialsProvider(StaticCredentialsProvider.create(awsCreds))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.recordy.server.external.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@Tag(name = "S3 테스트 관련 API")
public interface S3TestApi {

@Operation(
summary = "이미지 업로드 테스트 API",
description = "S3에 이미지가 업로드 되는지 테스트하는 API입니다. 이미지 사이즈는 5MB를 넘을 수 없습니다. 이미지 확장자는 jpg, png, webp만 가능합니다.",
responses = {
@ApiResponse(
responseCode = "200",
description = "성공",
content = @Content(
mediaType = MediaType.MULTIPART_FORM_DATA_VALUE
)
)
}
)
public String uploadTest(@RequestPart MultipartFile file) throws IOException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.recordy.server.external.controller;

import lombok.RequiredArgsConstructor;
import org.recordy.server.external.service.impl.S3ServiceImpl;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;


@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1")
public class S3TestController implements S3TestApi {

private final S3ServiceImpl s3ServiceImpl;

@Override
@PostMapping(path = "/uploadTest", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String uploadTest(@RequestPart MultipartFile file) throws IOException {
return this.s3ServiceImpl.uploadFile(file);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.recordy.server.external.exception;

import org.recordy.server.common.exception.RecordyException;
import org.recordy.server.common.message.ErrorMessage;

public class ExternalException extends RecordyException {

public ExternalException(ErrorMessage errorMessage) {
super(errorMessage);
}
}
16 changes: 16 additions & 0 deletions src/main/java/org/recordy/server/external/service/S3Service.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.recordy.server.external.service;

import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

public interface S3Service {

// command
public String uploadFile(MultipartFile file) throws IOException;
void deleteFile(String key) throws IOException;

// query


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package org.recordy.server.external.service.impl;

import org.recordy.server.common.message.ErrorMessage;
import org.recordy.server.external.config.S3Config;
import org.recordy.server.external.exception.ExternalException;
import org.recordy.server.external.service.S3Service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

@Component
public class S3ServiceImpl implements S3Service {

private String bucketName;
private S3Config s3Config;
private S3Client s3Client;
private static final List<String> FILE_EXTENSIONS = Arrays.asList("image/jpeg", "image/png", "image/jpg", "image/webp", "video/mp4", "video/mov", "video/quicktime");
private static final Long MAX_SIZE = 100 * 1024 * 1024L; // 100MB

public S3ServiceImpl(@Value("${aws-property.s3-bucket-name}") final String bucketName, S3Config s3Config) {
this.bucketName = bucketName;
this.s3Config = s3Config;
this.s3Client = s3Config.getS3Client();
}

@Override
public String uploadFile(MultipartFile file) throws IOException {
validateFileExtension(file);
validateFileSize(file);
final String url = getFileExtension(file);
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucketName)
.key(url)
.contentType(file.getContentType())
.contentDisposition("inline")
.build();
RequestBody requestBody = RequestBody.fromBytes(file.getBytes());
s3Client.putObject(request, requestBody);
return url;
}

public String getFileExtension(MultipartFile file) {
return UUID.randomUUID() + switch (Objects.requireNonNull(file.getContentType())) {
case "image/jpeg", "image/jpg" -> ".jpg";
case "image/png" -> ".png";
case "image/webp" -> ".webp";
case "video/mp4" -> ".mp4";
case "video/mov", "video/quicktime" -> ".mov";
default -> throw new ExternalException(ErrorMessage.INVALID_FILE_TYPE);
};
}

@Override
public void deleteFile(String key) throws IOException {
s3Client.deleteObject(DeleteObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build());
}

public void setS3Client(S3Client s3Client) {
this.s3Client = s3Client;
}

private String generateImageFileName() {
return UUID.randomUUID() + ".jpg";
}

private String generateVideoFileName() {
return UUID.randomUUID() + ".mp4";
}

public void validateFileExtension(MultipartFile file) {
String contentType = file.getContentType();
if (!FILE_EXTENSIONS.contains(contentType)) {
throw new ExternalException(ErrorMessage.INVALID_FILE_TYPE);
}
}

public void validateFileSize(MultipartFile file) {
if (file.getSize() > MAX_SIZE) {
throw new ExternalException(ErrorMessage.INVALID_FILE_SIZE);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.recordy.server.record.exception;

import org.recordy.server.common.exception.RecordyException;
import org.recordy.server.common.message.ErrorMessage;

public class RecordException extends RecordyException {

public RecordException(ErrorMessage errorMessage) {
super(errorMessage);
}

}
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
package org.recordy.server.record.service.impl;

import lombok.RequiredArgsConstructor;
import org.recordy.server.common.message.ErrorMessage;
import org.recordy.server.external.service.S3Service;
import org.recordy.server.record.exception.RecordException;
import org.recordy.server.record.service.FileService;
import org.recordy.server.record.domain.File;
import org.recordy.server.record.service.dto.FileUrl;
import org.springframework.stereotype.Service;

import java.io.IOException;

@RequiredArgsConstructor
@Service
public class FileServiceImpl implements FileService {

private final S3Service s3Service;

@Override
public FileUrl save(File file) {
try {
String videoUrl = s3Service.uploadFile(file.video());
String thumbnailUrl = s3Service.uploadFile(file.thumbnail());

/*
* FFmpeg ㅏㅇ넘ㄹ;ㅣㄴ아ㅓ리;ㅁㄴ아ㅓㄹㅁ니;ㅏㄹㅁ너
* */
return new FileUrl(videoUrl, thumbnailUrl);
} catch (IOException e) {
throw new RecordException(ErrorMessage.FAILED_TO_UPLOAD_TO_S3);
}

return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@

@Tag(name = "유저 관련 API")
public interface UserApi {

@Operation(
summary = "유저 회원 가입 API",
description = "유저가 회원 가입하는 API입니다.",
description = "유저가 회원 가입하는 API입니다. ",
responses = {
@ApiResponse(
responseCode = "200",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public ResponseEntity<Void> signUp(
build();
}

//쿼리 파라미터로
@Override
@GetMapping("/check-nickname")
public ResponseEntity<Void> checkDuplicateNickname(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.recordy.server.external.controller;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.recordy.server.external.service.impl.S3ServiceImpl;
import org.recordy.server.mock.FakeContainer;
import org.springframework.mock.web.MockMultipartFile;

import java.io.IOException;

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


public class S3TestControllerTest {

private S3TestController s3TestController;
private S3ServiceImpl s3Service;

@BeforeEach
void init() {
FakeContainer fakeContainer = new FakeContainer();
s3Service = (S3ServiceImpl) fakeContainer.s3Service;
s3TestController = new S3TestController(s3Service);
}

@Test
void uploadImageTest_이미지_업로드_성공() throws IOException {
// given
MockMultipartFile image = new MockMultipartFile("image", "test.jpg", "image/jpeg", new byte[1024]);

// when
String result = s3TestController.uploadTest(image);

// then
assertThat(result).isNotNull();
assertThat(result).contains("recordy/file/");
}

@Test
void uploadVideoTest_비디오_업로드_성공() throws IOException {
// given
MockMultipartFile video = new MockMultipartFile("video", "test.mp4", "video/mp4", new byte[1024 * 50]);

// when
String result = s3TestController.uploadTest(video);

// then
assertThat(result).isNotNull();
assertThat(result).contains("recordy/file/");
}
}

Loading

0 comments on commit cfd9466

Please sign in to comment.