-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Feat/#29 s3 upload
- Loading branch information
Showing
16 changed files
with
355 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package com.pyro.yolog.global.config; | ||
|
||
import com.amazonaws.auth.AWSCredentials; | ||
import com.amazonaws.auth.AWSStaticCredentialsProvider; | ||
import com.amazonaws.auth.BasicAWSCredentials; | ||
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.credentials.access-key}") | ||
private String accessKey; | ||
@Value("${cloud.aws.credentials.secret-key}") | ||
private String secretKey; | ||
@Value("${cloud.aws.region.static}") | ||
private String region; | ||
|
||
@Bean | ||
public AmazonS3 amazonS3() { | ||
AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); | ||
|
||
return AmazonS3ClientBuilder | ||
.standard() | ||
.withCredentials(new AWSStaticCredentialsProvider(credentials)) | ||
.withRegion(region) | ||
.build(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package com.pyro.yolog.global.error; | ||
|
||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.HttpStatus; | ||
|
||
@Getter | ||
@RequiredArgsConstructor | ||
public enum ErrorCode { | ||
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "내부 서버에 오류가 발생했습니다."), | ||
|
||
// MEMBER | ||
MEMBER_NOT_FOUND_ERROR(HttpStatus.NOT_FOUND, "회원 정보를 찾지 못했습니다." ), | ||
|
||
// S3 | ||
FILE_UPLOAD_FAILURE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S3 파일 업로드에 실패했습니다."), | ||
INVALID_FILE_EXTENSION_ERROR(HttpStatus.BAD_REQUEST, "잘못된 확장자의 파일 업로드를 시도했습니다."), | ||
FILE_DELETE_FAILURE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S3 파일 삭제에 실패했습니다."); | ||
|
||
private final HttpStatus status; | ||
private final String errorMessage; | ||
} |
19 changes: 19 additions & 0 deletions
19
src/main/java/com/pyro/yolog/global/error/ErrorResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package com.pyro.yolog.global.error; | ||
|
||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.HttpStatus; | ||
|
||
@Getter | ||
@RequiredArgsConstructor | ||
public class ErrorResponse { | ||
private final HttpStatus status; | ||
private final String errorMessage; | ||
|
||
public static ErrorResponse create(final ErrorCode errorCode) { | ||
return new ErrorResponse( | ||
errorCode.getStatus(), | ||
errorCode.getErrorMessage() | ||
); | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
src/main/java/com/pyro/yolog/global/error/GlobalExceptionHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package com.pyro.yolog.global.error; | ||
|
||
import com.pyro.yolog.global.error.exception.BusinessException; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.ExceptionHandler; | ||
import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
|
||
@Slf4j | ||
@RestControllerAdvice | ||
public class GlobalExceptionHandler { | ||
|
||
@ExceptionHandler(BusinessException.class) | ||
protected ResponseEntity<ErrorResponse> handleRuntimeException(BusinessException e) { | ||
final ErrorCode errorCode = e.getErrorCode(); | ||
log.warn(e.getMessage()); | ||
|
||
return ResponseEntity | ||
.status(errorCode.getStatus()) | ||
.body(new ErrorResponse(errorCode.getStatus(), | ||
errorCode.getErrorMessage())); | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
src/main/java/com/pyro/yolog/global/error/advice/AdviceException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package com.pyro.yolog.global.error.advice; | ||
|
||
import com.pyro.yolog.global.jwt.exception.NotFoundTokenException; | ||
import org.springframework.http.HttpEntity; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.ExceptionHandler; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
@RestController | ||
public class AdviceException { | ||
@ExceptionHandler(NotFoundTokenException.class) | ||
public ResponseEntity<HttpEntity> notFoundTokenException() { | ||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
src/main/java/com/pyro/yolog/global/error/exception/BusinessException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.pyro.yolog.global.error.exception; | ||
|
||
import com.pyro.yolog.global.error.ErrorCode; | ||
import lombok.Getter; | ||
|
||
@Getter | ||
public class BusinessException extends RuntimeException { | ||
private final ErrorCode errorCode; | ||
|
||
public BusinessException(ErrorCode errorCode) { | ||
super(errorCode.getErrorMessage()); | ||
this.errorCode = errorCode; | ||
} | ||
} | ||
|
13 changes: 13 additions & 0 deletions
13
src/main/java/com/pyro/yolog/global/error/exception/ExternalApiException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.pyro.yolog.global.error.exception; | ||
|
||
import com.pyro.yolog.global.error.ErrorCode; | ||
import lombok.Getter; | ||
|
||
@Getter | ||
public class ExternalApiException extends RuntimeException { | ||
private final ErrorCode errorCode; | ||
|
||
public ExternalApiException(ErrorCode errorCode) { | ||
this.errorCode = errorCode; | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
src/main/java/com/pyro/yolog/global/jwt/exception/NotFoundTokenException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.pyro.yolog.global.jwt.exception; | ||
|
||
public class NotFoundTokenException extends RuntimeException { | ||
public NotFoundTokenException() { | ||
} | ||
|
||
public NotFoundTokenException(String message) { | ||
super(message); | ||
} | ||
|
||
public NotFoundTokenException(String message, Throwable cause) { | ||
super(message, cause); | ||
} | ||
} | ||
|
38 changes: 38 additions & 0 deletions
38
src/main/java/com/pyro/yolog/global/s3/api/S3ImageApi.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package com.pyro.yolog.global.s3.api; | ||
|
||
import com.pyro.yolog.global.s3.dto.S3ImageDto; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
import io.swagger.v3.oas.annotations.responses.ApiResponses; | ||
import io.swagger.v3.oas.annotations.security.SecurityRequirement; | ||
import org.springframework.web.multipart.MultipartFile; | ||
|
||
public interface S3ImageApi { | ||
@Operation( | ||
summary = "이미지 업로드", | ||
description = "S3에 이미지를 업로드합니다. 해당 이미지의 URL을 반환받습니다.", | ||
security = {@SecurityRequirement(name = "access_token")}, | ||
tags = {"IMAGE"} | ||
) | ||
@ApiResponses(value = { | ||
@ApiResponse( | ||
responseCode = "201", | ||
description = "Created" | ||
) | ||
}) | ||
S3ImageDto uploadImage(MultipartFile image); | ||
|
||
@Operation( | ||
summary = "이미지 삭제", | ||
description = "S3에서 이미지를 삭제합니다.", | ||
security = {@SecurityRequirement(name = "access_token")}, | ||
tags = {"IMAGE"} | ||
) | ||
@ApiResponses(value = { | ||
@ApiResponse( | ||
responseCode = "204", | ||
description = "No Content" | ||
) | ||
}) | ||
void deleteImage(S3ImageDto dto); | ||
} |
29 changes: 29 additions & 0 deletions
29
src/main/java/com/pyro/yolog/global/s3/api/S3ImageImageController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package com.pyro.yolog.global.s3.api; | ||
|
||
import com.pyro.yolog.global.s3.dto.S3ImageDto; | ||
import com.pyro.yolog.global.s3.service.S3ImageService; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.web.bind.annotation.*; | ||
import org.springframework.web.multipart.MultipartFile; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("images") | ||
public class S3ImageImageController implements S3ImageApi { | ||
private final S3ImageService s3ImageService; | ||
|
||
@ResponseStatus(HttpStatus.CREATED) | ||
@PostMapping("") | ||
@Override | ||
public S3ImageDto uploadImage(@RequestPart(value = "image", required = false) MultipartFile image) { | ||
return s3ImageService.uploadImage(image); | ||
} | ||
|
||
@ResponseStatus(HttpStatus.CREATED) | ||
@DeleteMapping("") | ||
@Override | ||
public void deleteImage(@RequestBody final S3ImageDto dto) { | ||
s3ImageService.deleteImage(dto); | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
src/main/java/com/pyro/yolog/global/s3/dto/S3ImageDto.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package com.pyro.yolog.global.s3.dto; | ||
|
||
import lombok.*; | ||
|
||
@Getter | ||
@Setter | ||
@AllArgsConstructor | ||
@NoArgsConstructor | ||
public class S3ImageDto { | ||
String imageUrl; | ||
} |
11 changes: 11 additions & 0 deletions
11
src/main/java/com/pyro/yolog/global/s3/exception/FileDeleteFailureException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package com.pyro.yolog.global.s3.exception; | ||
|
||
import com.pyro.yolog.global.error.ErrorCode; | ||
import com.pyro.yolog.global.error.exception.ExternalApiException; | ||
|
||
public class FileDeleteFailureException extends ExternalApiException { | ||
|
||
public FileDeleteFailureException() { | ||
super(ErrorCode.FILE_DELETE_FAILURE_ERROR); | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
src/main/java/com/pyro/yolog/global/s3/exception/FileUploadFailureException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package com.pyro.yolog.global.s3.exception; | ||
|
||
import com.pyro.yolog.global.error.ErrorCode; | ||
import com.pyro.yolog.global.error.exception.ExternalApiException; | ||
|
||
public class FileUploadFailureException extends ExternalApiException { | ||
|
||
public FileUploadFailureException() { | ||
super(ErrorCode.FILE_UPLOAD_FAILURE_ERROR); | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
src/main/java/com/pyro/yolog/global/s3/exception/InvalidFileExtensionException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package com.pyro.yolog.global.s3.exception; | ||
|
||
import com.pyro.yolog.global.error.ErrorCode; | ||
import com.pyro.yolog.global.error.exception.BusinessException; | ||
|
||
public class InvalidFileExtensionException extends BusinessException { | ||
|
||
public InvalidFileExtensionException() { | ||
super(ErrorCode.INVALID_FILE_EXTENSION_ERROR); | ||
} | ||
} |
89 changes: 89 additions & 0 deletions
89
src/main/java/com/pyro/yolog/global/s3/service/S3ImageService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package com.pyro.yolog.global.s3.service; | ||
|
||
import com.amazonaws.services.s3.AmazonS3; | ||
import com.amazonaws.services.s3.model.CannedAccessControlList; | ||
import com.amazonaws.services.s3.model.DeleteObjectRequest; | ||
import com.amazonaws.services.s3.model.ObjectMetadata; | ||
import com.amazonaws.services.s3.model.PutObjectRequest; | ||
import com.pyro.yolog.global.s3.dto.S3ImageDto; | ||
import com.pyro.yolog.global.s3.exception.FileDeleteFailureException; | ||
import com.pyro.yolog.global.s3.exception.FileUploadFailureException; | ||
import com.pyro.yolog.global.s3.exception.InvalidFileExtensionException; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.web.multipart.MultipartFile; | ||
|
||
import java.io.IOException; | ||
import java.net.MalformedURLException; | ||
import java.net.URL; | ||
import java.net.URLDecoder; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.UUID; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class S3ImageService { | ||
private static final String IMAGE_DIRECTORY = "/image"; | ||
|
||
@Value("${cloud.aws.s3.bucket-name}") | ||
private String BUCKET_NAME; | ||
|
||
private final AmazonS3 amazonS3; | ||
|
||
public S3ImageDto uploadImage(MultipartFile file) { | ||
validateImageExtension(file.getOriginalFilename()); | ||
return new S3ImageDto(uploadImageToS3(file)); | ||
} | ||
|
||
private void validateImageExtension(String fileName) { | ||
int lastDotIndex = getLastDotIndex(fileName); | ||
String extention = fileName.substring(lastDotIndex + 1).toLowerCase(); | ||
List<String> allowedExtentionList = Arrays.asList("jpg", "jpeg", "png", "gif"); | ||
|
||
if (!allowedExtentionList.contains(extention)) { | ||
throw new InvalidFileExtensionException(); | ||
} | ||
} | ||
|
||
private int getLastDotIndex(String fileName) { | ||
int lastDotIndex = fileName.lastIndexOf("."); | ||
if (lastDotIndex == -1) { | ||
throw new InvalidFileExtensionException(); | ||
} | ||
return lastDotIndex; | ||
} | ||
|
||
private String uploadImageToS3(MultipartFile file) { | ||
ObjectMetadata metadata = new ObjectMetadata(); | ||
metadata.setContentType(file.getContentType()); | ||
metadata.setContentLength(file.getSize()); | ||
String s3FileName = UUID.randomUUID().toString().substring(0, 10) | ||
+ file.getOriginalFilename(); | ||
try { | ||
PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME + IMAGE_DIRECTORY, s3FileName, file.getInputStream(), metadata).withCannedAcl(CannedAccessControlList.PublicRead); | ||
amazonS3.putObject(putObjectRequest); | ||
} catch (IOException e) { | ||
throw new FileUploadFailureException(); | ||
} | ||
return amazonS3.getUrl(BUCKET_NAME + IMAGE_DIRECTORY, s3FileName).toString(); | ||
} | ||
|
||
public void deleteImage(S3ImageDto dto) { | ||
String key = getKeyFromImageAddress(dto.getImageUrl()); | ||
amazonS3.deleteObject(new DeleteObjectRequest(BUCKET_NAME, key)); | ||
} | ||
|
||
private String getKeyFromImageAddress(String imageAddress){ | ||
try{ | ||
URL url = new URL(imageAddress); | ||
String decodingKey = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8); | ||
return decodingKey.substring(1); // 맨 앞의 '/' 제거 | ||
}catch (MalformedURLException e){ | ||
throw new FileDeleteFailureException(); | ||
} | ||
} | ||
} |