Skip to content

Commit

Permalink
feat: s3 presigned url service
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeongh00 committed Sep 11, 2024
1 parent e0bd142 commit a786556
Show file tree
Hide file tree
Showing 10 changed files with 318 additions and 0 deletions.
25 changes: 25 additions & 0 deletions core/src/main/java/com/pocket/core/image/S3TestController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.pocket.core.image;

import com.pocket.core.exception.common.ApiResponse;
import com.pocket.core.image.service.AwsS3Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1/test")
public class S3TestController {

private final AwsS3Service awsS3Service;

@PostMapping(value = "/uploadFile", consumes = "multipart/form-data")
public ApiResponse<String> uploadFile(@RequestPart(value = "file", required = false) MultipartFile file) {
return ApiResponse.onSuccess(awsS3Service.uploadFile(file));
}
}
35 changes: 35 additions & 0 deletions core/src/main/java/com/pocket/core/image/config/AwsS3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.pocket.core.image.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 lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@RequiredArgsConstructor
public class AwsS3Config {

@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 generateS3client() {
AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.pocket.core.image.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
@Schema(description = "프리사인드 URL 응답 DTO")
public class PresignedUrlResponse {

@Schema(description = "생성된 프리사인드 URL", example = "https://example.com/presigned-url")
private final String url;

@Schema(description = "파일 경로", example = "images/example.txt")
private final String filePath;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.pocket.core.image.exception;

import com.pocket.core.exception.common.BaseErrorCode;

public class FileDeleteException extends ImageException {
public FileDeleteException(BaseErrorCode errorCode) {
super(errorCode);
}

public FileDeleteException(BaseErrorCode errorCode, Throwable cause) {
super(errorCode, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.pocket.core.image.exception;

import com.pocket.core.exception.common.BaseErrorCode;

public class FileExtensionException extends ImageException {
public FileExtensionException(BaseErrorCode errorCode) {
super(errorCode);
}

public FileExtensionException(BaseErrorCode errorCode, Throwable cause) {
super(errorCode, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.pocket.core.image.exception;

import com.pocket.core.exception.common.BaseErrorCode;

public class FileUploadException extends ImageException {
public FileUploadException(BaseErrorCode errorCode) {
super(errorCode);
}

public FileUploadException(BaseErrorCode errorCode, Throwable cause) {
super(errorCode, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.pocket.core.image.exception;

import com.pocket.core.exception.common.ApiResponse;
import com.pocket.core.exception.common.BaseErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum ImageErrorCode implements BaseErrorCode {

FILE_UPLOAD_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "3000", "파일 업로드에 실패했습니다."),
FILE_DELETE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "3000", "파일 삭제에 실패했습니다."),
WRONG_FILE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR, "3000", "파일 타입이 올바르지 않습니다."),
;

private final HttpStatus httpStatus;
private final String code;
private final String message;


@Override
public ApiResponse<Void> getErrorResponse() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.pocket.core.image.exception;

import com.pocket.core.exception.common.BaseErrorCode;
import lombok.Getter;

@Getter
public class ImageException extends RuntimeException {

private final BaseErrorCode errorCode;

private final Throwable cause;

public ImageException(BaseErrorCode errorCode) {
this.errorCode = errorCode;
this.cause = null;
}

public ImageException(BaseErrorCode errorCode, Throwable cause) {
this.errorCode = errorCode;
this.cause = cause;
}
}
81 changes: 81 additions & 0 deletions core/src/main/java/com/pocket/core/image/service/AwsS3Service.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.pocket.core.image.service;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.pocket.core.image.exception.FileDeleteException;
import com.pocket.core.image.exception.FileExtensionException;
import com.pocket.core.image.exception.FileUploadException;
import com.pocket.core.image.exception.ImageErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.UUID;

@Service
@RequiredArgsConstructor
public class AwsS3Service {

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

/**
* file upload
*/
public String uploadFile(MultipartFile multipartFile) {
if (Objects.isNull(multipartFile)) return null;
if (multipartFile.isEmpty()) return null;

String fileName = createFileName(multipartFile.getOriginalFilename());

ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType(multipartFile.getContentType());

try (InputStream inputStream = multipartFile.getInputStream()) {
amazonS3.putObject(new PutObjectRequest(bucketName, fileName, inputStream, objectMetadata));
} catch (IOException e) {
throw new FileUploadException(ImageErrorCode.FILE_UPLOAD_FAIL);
}

return amazonS3.getUrl(bucketName, fileName).toString();
}


/**
* 파일 삭제 메서드
*/
public void deleteFile(String fileUrl) {
if (fileUrl == null) return;
try {
amazonS3.deleteObject(bucketName, fileUrl);
} catch (AmazonServiceException e) {
throw new FileDeleteException(ImageErrorCode.FILE_DELETE_FAIL);
}
}

/**
* 파일 업로드 시에 파일명을 난수화하는 메서드
*/
private String createFileName(String fileName) {
return UUID.randomUUID().toString().concat(getFileExtension(fileName));
}

/**
* 파일 확장자 가져오는 메서드
*/
private String getFileExtension(String fileName) {
try {
return fileName.substring(fileName.lastIndexOf("."));
} catch (StringIndexOutOfBoundsException e) {
throw new FileExtensionException(ImageErrorCode.WRONG_FILE_FORMAT);
}
}

}
72 changes: 72 additions & 0 deletions core/src/main/java/com/pocket/core/image/service/FileService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.pocket.core.image.service;

import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import com.pocket.core.image.dto.PresignedUrlResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;

@Service
@RequiredArgsConstructor
public class FileService {

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

@Value("${cloud.aws.s3.expTime}")
private Long expTime;


private final AmazonS3 amazonS3;

public PresignedUrlResponse getUploadPresignedUrl(String prefix, String originalFileName) {
String filePath = createPath(prefix, originalFileName);
GeneratePresignedUrlRequest generatePresignedUrlRequest = getGeneratePresignedUrlRequest(bucket, filePath, HttpMethod.PUT);
URL url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest);

return new PresignedUrlResponse(url.toString(), filePath);
}

public String getDownloadPresignedUrl(String filePath) {
if (filePath != null && filePath.startsWith("images/")) {
GeneratePresignedUrlRequest generatePresignedUrlRequest = getGeneratePresignedUrlRequest(bucket, filePath, HttpMethod.GET);
URL url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest);
return url.toString();
}

return filePath;
}

private GeneratePresignedUrlRequest getGeneratePresignedUrlRequest(String bucket, String fileName, HttpMethod method) {

return new GeneratePresignedUrlRequest(bucket, fileName)
.withMethod(method)
.withExpiration(getPresignedUrlExpiration());
}

private Date getPresignedUrlExpiration() {
Date expiration = new Date();
long expTimeMillis = expiration.getTime();
expTimeMillis += expTime;
expiration.setTime(expTimeMillis);

return expiration;
}

private String createFileId() {
return UUID.randomUUID().toString();
}

private String createPath(String prefix, String fileName) {
String fileId = createFileId();
String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
return String.format("%s/%s-%s-%s", prefix, timestamp, fileId, fileName);
}
}

0 comments on commit a786556

Please sign in to comment.