-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 미사용 s3 객체 제거 스케쥴러 #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 2 commits
5061ccc
c9ff3cf
be90f1f
5f4ecdd
9f3cc22
704f920
d0ddd8c
e21f191
d979767
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package be.dash.dashserver.api.shceduler; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import org.springframework.context.annotation.Profile; | ||
| import org.springframework.scheduling.annotation.Scheduled; | ||
| import org.springframework.stereotype.Component; | ||
| import be.dash.dashserver.core.exception.ImageStorageException; | ||
| import be.dash.dashserver.core.image.ImageService; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| @Profile("dev") | ||
| @Component | ||
| @Slf4j | ||
| @RequiredArgsConstructor | ||
| public class S3CleanUpScheduler { | ||
|
|
||
| private final ImageService imageService; | ||
|
|
||
| @Scheduled(cron = "0 0 3 * * *") | ||
| public void cleanUp() { | ||
| log.info("S3 정리 작업 시작 {}", LocalDateTime.now()); | ||
| try { | ||
| imageService.cleanUpUnusedProfileImages(); | ||
| } catch (ImageStorageException e) { | ||
| log.error("S3 정리 작업 실패: {}", e.getMessage()); | ||
| if (!e.getFailedKeys().isEmpty()) { | ||
| log.error("삭제 실패한 키: {}", e.getFailedKeys()); | ||
| } | ||
| } | ||
| log.info("S3 정리 작업 끝 {}", LocalDateTime.now()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package be.dash.dashserver.core.exception; | ||
|
|
||
| import java.util.List; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| public class ImageStorageException extends RuntimeException { | ||
| private List<String> failedKeys; | ||
| public ImageStorageException(String message) { | ||
| super(message); | ||
| } | ||
|
|
||
| public ImageStorageException(String message, List<String> failedKeys) { | ||
| super(message); | ||
| this.failedKeys = failedKeys; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,11 @@ | ||
| package be.dash.dashserver.core.image; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.List; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| public interface ImageUploader { | ||
| public interface ImageManager { | ||
| String uploadImage(MultipartFile file) throws IOException; | ||
| List<String> getAllKeys(); | ||
| void deleteAllByKeys(List<String> keysToDelete); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| package be.dash.dashserver.external.s3; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import java.util.UUID; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
| import be.dash.dashserver.core.exception.BadRequestException; | ||
| import be.dash.dashserver.core.exception.ImageStorageException; | ||
| import be.dash.dashserver.core.image.ImageManager; | ||
| import be.dash.dashserver.external.config.s3.S3Config; | ||
| import be.dash.dashserver.external.config.s3.S3Properties; | ||
| import lombok.RequiredArgsConstructor; | ||
| import software.amazon.awssdk.awscore.exception.AwsServiceException; | ||
| import software.amazon.awssdk.core.exception.SdkClientException; | ||
| import software.amazon.awssdk.core.sync.RequestBody; | ||
| import software.amazon.awssdk.services.s3.S3Client; | ||
| import software.amazon.awssdk.services.s3.model.Delete; | ||
| import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; | ||
| import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; | ||
| import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; | ||
| import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; | ||
| import software.amazon.awssdk.services.s3.model.ObjectIdentifier; | ||
| import software.amazon.awssdk.services.s3.model.PutObjectRequest; | ||
| import software.amazon.awssdk.services.s3.model.S3Error; | ||
| import software.amazon.awssdk.services.s3.model.S3Object; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class S3ImageManager implements ImageManager { | ||
| private final S3Properties s3Properties; | ||
| private final S3Config s3Config; | ||
| private static final List<String> IMAGE_EXTENSIONS = Arrays.asList("image/jpeg", "image/png", "image/jpg", "image/webp", "image/heic", "image/heif"); | ||
|
|
||
| @Override | ||
| public String uploadImage(MultipartFile image) throws IOException { | ||
| final String key = generateImageFileName(); | ||
| final S3Client s3Client = s3Config.getS3Client(); | ||
|
|
||
| validateExtension(image); | ||
|
|
||
| PutObjectRequest request = PutObjectRequest.builder() | ||
| .bucket(s3Properties.s3BucketName()) | ||
| .key(key) | ||
| .contentType(image.getContentType()) | ||
| .contentDisposition("inline") | ||
| .build(); | ||
|
|
||
| RequestBody requestBody = RequestBody.fromBytes(image.getBytes()); | ||
| s3Client.putObject(request, requestBody); | ||
|
||
| return s3Properties.s3Endpoint() + key; | ||
| } | ||
|
|
||
| @Override | ||
| public List<String> getAllKeys() { | ||
| //여기도 retrey붙여야 되나? 어차피 실패하면 내일 해버리면 떙 아닌감.. | ||
|
||
| List<String> keys = new ArrayList<>(); | ||
| ListObjectsV2Request request = ListObjectsV2Request.builder() | ||
| .bucket(s3Properties.s3BucketName()) | ||
| .build(); | ||
|
|
||
| ListObjectsV2Response response; | ||
| S3Client s3Client = s3Config.getS3Client(); | ||
|
|
||
| do { | ||
| response = s3Client.listObjectsV2(request); | ||
| keys.addAll( | ||
| response.contents() | ||
| .stream() | ||
| .map(S3Object::key) | ||
| .toList() | ||
| ); | ||
| request = request.toBuilder() | ||
| .continuationToken(response.nextContinuationToken()) | ||
| .build(); | ||
| } while (response.isTruncated()); | ||
| return keys; | ||
| } | ||
|
|
||
| @Override | ||
| public void deleteAllByKeys(List<String> keysToDelete) { | ||
| S3Client s3Client = s3Config.getS3Client(); | ||
|
|
||
| List<ObjectIdentifier> list = keysToDelete.stream() | ||
| .map(key -> ObjectIdentifier.builder().key(key).build()).toList(); | ||
| DeleteObjectsRequest deleteObjectRequest = DeleteObjectsRequest.builder() | ||
| .bucket(s3Properties.s3BucketName()) | ||
| .delete(Delete.builder().objects(list).build()) | ||
| .build(); | ||
| DeleteObjectsResponse response; | ||
| try { | ||
| response = s3Client.deleteObjects(deleteObjectRequest); | ||
| } catch (AwsServiceException | SdkClientException e) { | ||
| throw new ImageStorageException("S3 객체 삭제에 실패했습니다."); | ||
| } | ||
|
|
||
| List<S3Error> errors = response.errors(); | ||
| if(!errors.isEmpty()) { | ||
| throw new ImageStorageException("이미지 삭제에 실패했습니다.", errors.stream().map(S3Error::key).toList()); | ||
| } | ||
| } | ||
|
|
||
| private String generateImageFileName() { | ||
| return UUID.randomUUID() + ".jpg"; | ||
| } | ||
|
|
||
| private void validateExtension(MultipartFile image) { | ||
| String contentType = image.getContentType(); | ||
| if (!IMAGE_EXTENSIONS.contains(contentType)) { | ||
| throw new BadRequestException("이미지 확장자는 jpg, png, webp, heic, heif만 가능합니다."); | ||
| } | ||
| } | ||
| } | ||
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
전역 핸들러에서
BadGatewayException을 잡고있는 부분이 없기에, 아래와 같이 새롭게 정의한ImageStorageException으로 처리해도 괜찮을 것 같습니다!전역 예외 핸들러에도 해당 예외를 추가해주세요!