Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ dependencies {
testRuntimeOnly 'com.h2database:h2'

// 스웨거
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0'

// 스프링 시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'
Expand All @@ -60,7 +60,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'

// s3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
implementation(platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.1.1"))
implementation("io.awspring.cloud:spring-cloud-aws-starter-s3")

// oauth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public enum ErrorStatus implements BaseErrorCode {
INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "AUTH406", "유효하지 않은 OAuth 제공자입니다."),

AWS_SERVICE_UNAVAILABLE(HttpStatus.BAD_REQUEST, "AWS400", "AWS S3에 파일을 업로드할 수 없습니다."),
AWS_METHOD_NOT_ALLOWED(
HttpStatus.METHOD_NOT_ALLOWED,
"AWS405",
"AWS S3 presigned url에서 해당 method는 허용되지 않습니다."),

MUSIC_NOT_FOUND(HttpStatus.BAD_REQUEST, "MUSIC400", "음원을 찾을 수 없습니다."),

Expand Down
23 changes: 11 additions & 12 deletions src/main/java/umc/codeplay/config/AWSConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
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.presigner.S3Presigner;

@Configuration
public class AWSConfig {
Expand All @@ -22,13 +22,12 @@ public class AWSConfig {
private String region;

@Bean
public AmazonS3Client amazonS3Client() {
final BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);

return (AmazonS3Client)
AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
public S3Presigner s3Presigner() {
return S3Presigner.builder()
.region(Region.of(region))
.credentialsProvider(
StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)))
.build();
}
}
49 changes: 49 additions & 0 deletions src/main/java/umc/codeplay/controller/FileController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package umc.codeplay.controller;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

import lombok.RequiredArgsConstructor;

import io.swagger.v3.oas.annotations.Operation;
import software.amazon.awssdk.http.SdkHttpMethod;
import umc.codeplay.apiPayLoad.ApiResponse;
import umc.codeplay.dto.FileResponseDTO;
import umc.codeplay.service.FileService;

import static umc.codeplay.service.FileService.buildFilename;

@RestController
@RequestMapping("/files")
@RequiredArgsConstructor
public class FileController {

private final FileService fileService;

@Operation(
summary = "Download용 Presigned URL 생성",
description = "다운로드를 위한 Presigned URL 생성 - 유효시간 존재")
@GetMapping("/download")
public ApiResponse<FileResponseDTO.DownloadFile> getUrl(
@RequestParam(value = "fileName") String fileName) {
String downloadUrl = fileService.generatePreSignedUrl(fileName, SdkHttpMethod.GET);
FileResponseDTO.DownloadFile result = new FileResponseDTO.DownloadFile(downloadUrl);

return ApiResponse.onSuccess(result);
}

@Operation(
summary = "Upload용 Presigned URL 생성",
description = "업로드를 위한 Presigned URL 생성 - 유효시간 존재")
@PostMapping("/upload")
public ApiResponse<FileResponseDTO.UploadFile> generateUrl(
@RequestParam(value = "fileName") String fileName) {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
String newFileName = buildFilename(fileName);
Long musicId = fileService.uploadMusic(newFileName, username);

String uploadUrl = fileService.generatePreSignedUrl(newFileName, SdkHttpMethod.PUT);
FileResponseDTO.UploadFile result = new FileResponseDTO.UploadFile(uploadUrl, musicId);
return ApiResponse.onSuccess(result);
}
}
20 changes: 20 additions & 0 deletions src/main/java/umc/codeplay/dto/FileResponseDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package umc.codeplay.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

public class FileResponseDTO {

@Getter
@AllArgsConstructor
public static class DownloadFile {
private String downloadS3Url;
}

@Getter
@AllArgsConstructor
public static class UploadFile {
private String uploadS3Url;
private Long musicId;
}
}
108 changes: 108 additions & 0 deletions src/main/java/umc/codeplay/service/FileService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package umc.codeplay.service;

import java.text.Normalizer;
import java.time.Duration;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
import umc.codeplay.apiPayLoad.code.status.ErrorStatus;
import umc.codeplay.apiPayLoad.exception.handler.GeneralHandler;
import umc.codeplay.domain.Member;
import umc.codeplay.domain.Music;
import umc.codeplay.repository.MemberRepository;
import umc.codeplay.repository.MusicRepository;

@Service
@RequiredArgsConstructor
public class FileService {

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

@Value("${cloud.aws.region.static}")
private String region;

private final S3Presigner s3Presigner;
private final MusicRepository musicRepository;
private final MemberRepository memberRepository;

// 타임스탬프_파일명 형식으로 파일 이름 저장
public static String buildFilename(String filename) {
return String.format("%s_%s", System.currentTimeMillis(), sanitizeFileName(filename));
}

// 특수 문자나 공백 등을 정리
private static String sanitizeFileName(String fileName) {
String normalizedFileName = Normalizer.normalize(fileName, Normalizer.Form.NFC);
return normalizedFileName.replaceAll("\\s+", "_").replaceAll("[^a-zA-Z0-9.\\-_]", "");
}

// 파일 업로드(HTTP PUT) 또는 다운로드(HTTP GET)를 위한 Presigned URL 생성
public String generatePreSignedUrl(String fileName, SdkHttpMethod method) {

return switch (method) {
case GET -> generateGetPresignedUrl(fileName);
case PUT -> generatePutPresignedUrl(fileName);
default -> throw new GeneralHandler(ErrorStatus.AWS_SERVICE_UNAVAILABLE);
};
}

// S3에서 파일을 다운로드할 수 있는 Presigned URL 생성
private String generateGetPresignedUrl(String fileName) {
GetObjectRequest getObjectRequest =
GetObjectRequest.builder().bucket(bucketName).key(fileName).build();

GetObjectPresignRequest presignRequest =
GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(60))
.getObjectRequest(getObjectRequest)
.build();

PresignedGetObjectRequest presignedRequest = s3Presigner.presignGetObject(presignRequest);
return presignedRequest.url().toString();
}

// S3에 파일을 업로드할 수 있는 Presigned URL 생성
private String generatePutPresignedUrl(String fileName) {
PutObjectRequest putObjectRequest =
PutObjectRequest.builder().bucket(bucketName).key(fileName).build();

PutObjectPresignRequest presignRequest =
PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(60))
.putObjectRequest(putObjectRequest)
.build();

PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(presignRequest);
return presignedRequest.url().toString();
}

// music 레포지토리에 업로드
public Long uploadMusic(String newFileName, String userEmail) {
Member member =
memberRepository
.findByEmail(userEmail)
.orElseThrow(() -> new GeneralHandler(ErrorStatus.MEMBER_NOT_FOUND));

// 저장하는 url은 유효시간이 없는 public
// TODO: 업로드에만 presigned 사용할지 아님 다운로드시에도 사용할지에 따라 변경해야함.
String s3Url =
String.format("https://%s.s3.%s.amazonaws.com/%s", bucketName, region, newFileName);
Music newMusic = Music.builder().title(newFileName).musicUrl(s3Url).member(member).build();

return musicRepository.save(newMusic).getId();
}

// TODO: 필요시 직접 업로드 방법 구현 필요
}
87 changes: 0 additions & 87 deletions src/main/java/umc/codeplay/service/S3Service.java

This file was deleted.

4 changes: 2 additions & 2 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ cloud:
aws:
s3:
bucket: ${S3_BUCKET}
stack.auto: false
region.static: ${AWS_DEFAULT_REGION}
region:
static: ${AWS_DEFAULT_REGION}
credentials:
accessKey: ${AWS_ACCESS_KEY_ID}
secretKey: ${AWS_SECRET_ACCESS_KEY}
Expand Down
4 changes: 2 additions & 2 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ cloud:
aws:
s3:
bucket: ${S3_BUCKET}
stack.auto: false
region.static: ${AWS_DEFAULT_REGION}
region:
static: ${AWS_DEFAULT_REGION}
credentials:
accessKey: ${AWS_ACCESS_KEY_ID}
secretKey: ${AWS_SECRET_ACCESS_KEY}
Expand Down
Loading