diff --git a/build.gradle b/build.gradle index c340f69..e4a3b07 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,9 @@ dependencies { implementation 'org.mindrot:jbcrypt:0.4' implementation 'org.flywaydb:flyway-core' implementation 'org.flywaydb:flyway-mysql' + + implementation "org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE" + testImplementation "org.testcontainers:testcontainers:1.20.1" testImplementation "org.testcontainers:mysql:1.20.1" testImplementation "org.testcontainers:junit-jupiter:1.20.1" diff --git a/src/main/java/gymmi/controller/S3Controller.java b/src/main/java/gymmi/controller/S3Controller.java new file mode 100644 index 0000000..85d1676 --- /dev/null +++ b/src/main/java/gymmi/controller/S3Controller.java @@ -0,0 +1,25 @@ +package gymmi.controller; + +import gymmi.entity.User; +import gymmi.global.Logined; +import gymmi.service.S3Service; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class S3Controller { + + private final S3Service s3Service; + + @GetMapping("/images/workout-proof/presignedUrl") + public ResponseEntity s3( + @Logined User user + ) { + String url = s3Service.getPresignedUrl(); + return ResponseEntity.ok().body(url); + } + +} diff --git a/src/main/java/gymmi/global/S3Config.java b/src/main/java/gymmi/global/S3Config.java new file mode 100644 index 0000000..b4534eb --- /dev/null +++ b/src/main/java/gymmi/global/S3Config.java @@ -0,0 +1,21 @@ +package gymmi.global; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3 amazonS3() { + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .build(); + } +} diff --git a/src/main/java/gymmi/service/ImageUploader.java b/src/main/java/gymmi/service/ImageUploader.java new file mode 100644 index 0000000..ea0d077 --- /dev/null +++ b/src/main/java/gymmi/service/ImageUploader.java @@ -0,0 +1,23 @@ +package gymmi.service; + +import com.amazonaws.services.s3.AmazonS3; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ImageUploader { + + private static final String CACHE_CONTROL_VALUE = "max-age=3153600"; + + private final AmazonS3 s3Client; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Value("${cloud.aws.s3.folder}") + private String folder; + + +} diff --git a/src/main/java/gymmi/service/S3FileService.java b/src/main/java/gymmi/service/S3FileService.java new file mode 100644 index 0000000..bd76084 --- /dev/null +++ b/src/main/java/gymmi/service/S3FileService.java @@ -0,0 +1,136 @@ +package gymmi.service; + +import com.amazonaws.services.s3.AmazonS3; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class S3FileService { + private final AmazonS3 amazonS3; + +// @Value("${cloud.aws.s3.bucket}") +// private String bucketName; +// @Value("${cloud.aws.s3.path}") +// private String s3DefaultPath; +// private static final String BUCKET_DIRECTORY_NAME = "petProfile"; +// private static final String FILE_NAME_PREFIX = "Petudio_"; +// +// +// /** +// * presigned url 발급 +// * +// * @param s3DirectoryPath 파일을 저장할 S3 디렉토리 경로 +// * @param index 이미지 순서 +// * @return presigned url +// */ +//public String getPreSignedUrl(String s3DirectoryPath, int index) { +// try { +// String filePath = createFilePath(s3DirectoryPath, index); +// +// GeneratePresignedUrlRequest generatePresignedUrlRequest = getGeneratePreSignedUrlRequest(bucketName, +// filePath); +// URL url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest); +// return url.toString(); +// } catch (Exception exception) { +// throw new BadGatewayException(ErrorCode.BAD_GATEWAY_EXCEPTION, +// String.format("S3 버킷의 디렉토리(%s/%d) 에 대한 PreSignedURL을 생성하는 과정에서 에러가 발생하였습니다.", s3DirectoryPath, index)); +// } +// } +// +// /** +// * 파일의 전체 경로를 생성 +// * +// * @param index 이미지 순서 +// * @return 파일의 전체 경로 +// */ +// private String createFilePath(String directoryPath, int index) { +// // 경로: {BUCKET_DIRECTORY_NAME}/{memberId}/Petudio_{fileId}/{index} +// return String.format("%s/%d", directoryPath, index); +// } +// +// /** +// * 파일 업로드용(PUT) presigned url 생성 +// * +// * @param bucket 버킷 이름 +// * @param filePath S3 업로드용 파일 경로 +// * @return presigned url +// */ +// private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest(String bucket, String filePath) { +// GeneratePresignedUrlRequest generatePresignedUrlRequest = +// new GeneratePresignedUrlRequest(bucket, filePath) +// .withMethod(HttpMethod.PUT) +// .withExpiration(getPreSignedUrlExpiration()); +// generatePresignedUrlRequest.addRequestParameter( +// Headers.S3_CANNED_ACL, +// CannedAccessControlList.PublicRead.toString()); +// return generatePresignedUrlRequest; +// } +// +// /** +// * presigned url 유효 기간 설정 +// * +// * @return 유효기간 +// */ +// private Date getPreSignedUrlExpiration() { +// Date expiration = new Date(); +// long expTimeMillis = expiration.getTime(); +// expTimeMillis += 1000 * 60 * 2; +// expiration.setTime(expTimeMillis); +// return expiration; +// } +// +// /** +// * 파일을 저장할 디렉토리 경로를 생성 +// * +// * @param memberId 회원 Primary Key +// * @return 파일의 전체 경로 +// */ +// public String createS3DirectoryPath(Long memberId) { +// return String.format("%s/%d/%s", BUCKET_DIRECTORY_NAME, memberId, FILE_NAME_PREFIX + createFileId()); +// } +// +// /** +// * 파일 고유 ID를 생성 +// * +// * @return 36자리의 UUID +// */ +// private String createFileId() { +// return UUID.randomUUID().toString(); +// } +// +// /** +// * s3 파일 접근 URI 생성 +// * +// * @param filePath S3 업로드용 파일 경로 +// * @return s3 파일 접근 URI +// */ +// public String createImageUri(String filePath) { +// return s3DefaultPath + filePath; +// } +// +// +// /** +// * s3 Directory 데이터 삭제 +// * +// * @param s3DirectoryPath 파일 삭제를 위한 S3 디렉토리 경로 +// */ +// public void deleteImagesByS3DirectoryPath(String s3DirectoryPath) { +// try { +// // s3DirectoryPath에 속한 객체들 나열 +// ListObjectsV2Request listObjectsRequest = new ListObjectsV2Request() +// .withBucketName(bucketName) +// .withPrefix(s3DirectoryPath); +// ListObjectsV2Result objectsResult = amazonS3.listObjectsV2(listObjectsRequest); +// +// // s3DirectoryPath 내의 객체들 삭제 +// for (S3ObjectSummary objectSummary : objectsResult.getObjectSummaries()) { +// String key = objectSummary.getKey(); +// amazonS3.deleteObject(new DeleteObjectRequest(bucketName, key)); +// } +// } catch (Exception exception) { +// throw new BadGatewayException(ErrorCode.BAD_GATEWAY_EXCEPTION, +// String.format("S3 버킷의 디렉토리(%s) 내부 데이터를 삭제하는 과정에서 에러가 발생하였습니다.", s3DirectoryPath)); +// } +// } +} diff --git a/src/main/java/gymmi/service/S3Service.java b/src/main/java/gymmi/service/S3Service.java new file mode 100644 index 0000000..ccfe2c4 --- /dev/null +++ b/src/main/java/gymmi/service/S3Service.java @@ -0,0 +1,104 @@ +package gymmi.service; + +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.Headers; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import java.net.URL; +import java.util.Date; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class S3Service { + + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucketName; + @Value("${cloud.aws.s3.path}") + private String s3DefaultPath; + + public String getPresignedUrl() { + return generatePresignedUrl(bucketName, s3DefaultPath); + } + + public String generatePresignedUrl(String bucket, String filePath) { + GeneratePresignedUrlRequest generatePresignedUrlRequest = + new GeneratePresignedUrlRequest(bucket, filePath) + .withMethod(HttpMethod.PUT) + .withExpiration(getPreSignedUrlExpiration()); + generatePresignedUrlRequest.addRequestParameter( + Headers.S3_CANNED_ACL, + CannedAccessControlList.PublicRead.toString()); + URL url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest); + String presignedUrl = url.toString(); + return presignedUrl; + } + + private Date getPreSignedUrlExpiration() { + Date expiration = new Date(); + long expTimeMillis = expiration.getTime(); + expTimeMillis += 1000 * 60 * 2; + expiration.setTime(expTimeMillis); + return expiration; + } + + /** + * 파일을 저장할 디렉토리 경로를 생성 + * + * @param memberId 회원 Primary Key + * @return 파일의 전체 경로 + */ + +// public String createS3DirectoryPath(Long memberId) { +// return String.format("%s/%d/%s", BUCKET_DIRECTORY_NAME, memberId, FILE_NAME_PREFIX + createFileId()); +// } +// +// /** +// * 파일 고유 ID를 생성 +// * +// * @return 36자리의 UUID +// */ +// private String createFileId() { +// return UUID.randomUUID().toString(); +// } +// +// /** +// * s3 파일 접근 URI 생성 +// * +// * @param filePath S3 업로드용 파일 경로 +// * @return s3 파일 접근 URI +// */ +// public String createImageUri(String filePath) { +// return s3DefaultPath + filePath; +// } +// +// +// /** +// * s3 Directory 데이터 삭제 +// * +// * @param s3DirectoryPath 파일 삭제를 위한 S3 디렉토리 경로 +// */ +// public void deleteImagesByS3DirectoryPath(String s3DirectoryPath) { +// try { +// // s3DirectoryPath에 속한 객체들 나열 +// ListObjectsV2Request listObjectsRequest = new ListObjectsV2Request() +// .withBucketName(bucketName) +// .withPrefix(s3DirectoryPath); +// ListObjectsV2Result objectsResult = amazonS3.listObjectsV2(listObjectsRequest); +// +// // s3DirectoryPath 내의 객체들 삭제 +// for (S3ObjectSummary objectSummary : objectsResult.getObjectSummaries()) { +// String key = objectSummary.getKey(); +// amazonS3.deleteObject(new DeleteObjectRequest(bucketName, key)); +// } +// } catch (Exception exception) { +// throw new BadGatewayException(ErrorCode.BAD_GATEWAY_EXCEPTION, +// String.format("S3 버킷의 디렉토리(%s) 내부 데이터를 삭제하는 과정에서 에러가 발생하였습니다.", s3DirectoryPath)); +// } +// } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 916b6c4..d29f96c 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -30,3 +30,14 @@ jwt: file: storage-path: ${FILE_STORAGE_PATH} + + +cloud: + aws: + s3: + bucket: ${BUCKET_NAME} # S3 버킷 이름 + path: ${FILE_PATH} # S3에서 이미지 저장에 활용할 기본 경로 + region: + static: ${REGION} # AWS Region + stack: + auto: false diff --git a/src/test/java/gymmi/workspace/service/IntegrationTest.java b/src/test/java/gymmi/workspace/service/IntegrationTest.java index 250aac8..834ef27 100644 --- a/src/test/java/gymmi/workspace/service/IntegrationTest.java +++ b/src/test/java/gymmi/workspace/service/IntegrationTest.java @@ -3,6 +3,7 @@ import gymmi.helper.Persister; import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; @@ -14,4 +15,5 @@ public class IntegrationTest { @Autowired Persister persister; + }