Skip to content

Commit

Permalink
Merge pull request #44 from DSM-Repo/file-upload
Browse files Browse the repository at this point in the history
pr :: image upload 관련 api 추가
  • Loading branch information
dkflfkd53 authored Aug 7, 2024
2 parents ee43257 + c36e849 commit 3846291
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.whopper.domain.file.api;

import com.example.whopper.domain.file.application.usecase.SaveImageUseCase;
import com.example.whopper.domain.file.dto.response.ImagePathResponse;
import com.example.whopper.domain.file.type.ImageType;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

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

private final SaveImageUseCase saveImageUseCase;

@PostMapping(value = "/image", consumes = {
MediaType.MULTIPART_FORM_DATA_VALUE
})
public ImagePathResponse uploadImage(@RequestPart("file") MultipartFile filePart, @RequestParam("type") ImageType imageType) {
String path = saveImageUseCase.saveImage(filePart, imageType);
return new ImagePathResponse(path, saveImageUseCase.getFileBaseUrl());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.example.whopper.domain.file.application.impl;

import com.example.whopper.domain.file.application.usecase.SaveImageUseCase;
import com.example.whopper.infra.s3.AwsS3Properties;
import com.example.whopper.infra.s3.AwsS3FileType;
import com.example.whopper.domain.file.type.ImageType;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.async.AsyncRequestBody;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.transfer.s3.S3TransferManager;
import software.amazon.awssdk.transfer.s3.model.UploadRequest;

import java.util.*;
import java.util.concurrent.Executors;

@Component
@RequiredArgsConstructor
public class SaveImageService implements SaveImageUseCase {
private static final Set<String> VALID_EXTENSIONS = Set.of(".jpg", ".jpeg", ".png", ".heic", ".svg", ".webp", ".gif");

private final S3TransferManager s3TransferManager;
private final AwsS3Properties awsS3Properties;

public String saveImage(MultipartFile multipartFile, ImageType imageType) {
String originalFileName = multipartFile.getOriginalFilename();
if (originalFileName == null || !isValidExtension(getExtension(originalFileName))) {
throw new RuntimeException("Invalid file extension.");
}

// Generate a unique key for the file
String key = generateFileKey(originalFileName, imageType);

// Upload file and return the key
uploadFile(multipartFile, key);

return key;
}

public String getFileBaseUrl() {
return awsS3Properties.url();
}

private String getExtension(String filename) {
return filename.substring(filename.lastIndexOf(".")).toLowerCase();
}

private String generateFileKey(String originalFileName, ImageType imageType) {
String folder = imageType == ImageType.PROFILE ? awsS3Properties.profileFolder() : awsS3Properties.documentFolder();
return folder + "/" + UUID.randomUUID() + originalFileName;
}

private void uploadFile(MultipartFile multipartFile, String key) {
try {
// InputStream을 사용하여 파일을 업로드
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(awsS3Properties.bucket())
.key(key)
.contentType(multipartFile.getContentType())
.acl(AwsS3FileType.IMAGE.getCannedAcl())
.build();

// S3에 파일 업로드
s3TransferManager.upload(UploadRequest.builder()
.putObjectRequest(putObjectRequest)
.requestBody(AsyncRequestBody.fromInputStream(multipartFile.getInputStream(), multipartFile.getSize(), Executors.newFixedThreadPool(10))) // ExecutorService 추가
.build()).completionFuture().join(); // 동기적으로 대기
} catch (Exception e) {
throw new RuntimeException("파일 업로드 중 오류 발생", e);
}
}

private boolean isValidExtension(String extension) {
return VALID_EXTENSIONS.contains(extension);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.example.whopper.domain.file.application.impl;

import com.example.whopper.domain.file.application.usecase.SavePdfUseCase;
import com.example.whopper.infra.s3.AwsS3FileType;
import com.example.whopper.infra.s3.AwsS3Properties;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.async.AsyncRequestBody;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.transfer.s3.S3TransferManager;
import software.amazon.awssdk.transfer.s3.model.UploadRequest;

import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Service
@RequiredArgsConstructor
public class SavePdfService implements SavePdfUseCase {

private final AwsS3Properties awsS3Properties;
private final S3TransferManager s3TransferManager;
private final ExecutorService executorService = Executors.newFixedThreadPool(10);

public String savePdf(MultipartFile multipartFile) {
String folder = awsS3Properties.pdfFolder();
String date = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
String key = String.format("%s/%s-%s.pdf", folder, date, UUID.randomUUID());

uploadFile(multipartFile, key);

return key;
}

private void uploadFile(MultipartFile multipartFile, String key) {
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(awsS3Properties.bucket())
.key(key)
.acl(AwsS3FileType.PDF.getCannedAcl())
.build();

// MultipartFile의 InputStream을 사용하여 파일을 S3에 업로드
try (InputStream inputStream = multipartFile.getInputStream()) {
// S3 업로드 수행
s3TransferManager.upload(UploadRequest.builder()
.putObjectRequest(putObjectRequest)
.requestBody(AsyncRequestBody.fromInputStream(inputStream, multipartFile.getSize(), executorService)) // 수정된 부분
.build()).completionFuture().join(); // 동기적으로 대기
} catch (Exception e) {
throw new RuntimeException("파일 업로드 중 오류 발생", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.whopper.domain.file.application.usecase;

import com.example.whopper.domain.file.type.ImageType;
import org.springframework.web.multipart.MultipartFile;

public interface SaveImageUseCase {
String saveImage(MultipartFile multipartFile, ImageType imageType);
String getFileBaseUrl();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.whopper.domain.file.application.usecase;

import org.springframework.web.multipart.MultipartFile;

public interface SavePdfUseCase {
String savePdf(MultipartFile multipartFile);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.whopper.domain.file.dto.response;

public record ImagePathResponse(
String imagePath,
String baseUrl
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.whopper.domain.file.type;

public enum ImageType {
PROFILE,
DOCUMENT
}
15 changes: 15 additions & 0 deletions src/main/java/com/example/whopper/infra/config/AwsProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.whopper.infra.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "aws")
public record AwsProperties(
String region,
CredentialsProperties credentials
) {
public record CredentialsProperties(
String accessKey,
String secretKey
) {
}
}
45 changes: 45 additions & 0 deletions src/main/java/com/example/whopper/infra/s3/AwsS3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.example.whopper.infra.s3;

import com.example.whopper.infra.config.AwsProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.transfer.s3.S3TransferManager;

@Component
public class AwsS3Config {
private final AwsProperties awsProperties;

@Autowired
public AwsS3Config(AwsProperties awsProperties) {
this.awsProperties = awsProperties;
}

// https://github.com/aws/aws-sdk-java-v2/issues/1750
@Bean
public S3TransferManager s3TransferManager() {
S3AsyncClient s3AsyncClient = S3AsyncClient
.crtBuilder()
.credentialsProvider(DefaultCredentialsProvider.create())
.region(Region.of(awsProperties.region()))
.targetThroughputInGbps(20.0)
.minimumPartSizeInBytes(8L * 1024 * 1024) // 8 MB in bytes
.build();

return S3TransferManager.builder()
.s3Client(s3AsyncClient)
.build();
}

@Bean
public S3Presigner s3Presigner() {
return S3Presigner.builder()
.credentialsProvider(DefaultCredentialsProvider.create())
.region(Region.of(awsProperties.region()))
.build();
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/example/whopper/infra/s3/AwsS3FileType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.whopper.infra.s3;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import software.amazon.awssdk.services.s3.model.ObjectCannedACL;

@RequiredArgsConstructor
@Getter
public enum AwsS3FileType {
PDF(ObjectCannedACL.AUTHENTICATED_READ),
IMAGE(ObjectCannedACL.PUBLIC_READ);

private final ObjectCannedACL cannedAcl;
}
13 changes: 13 additions & 0 deletions src/main/java/com/example/whopper/infra/s3/AwsS3Properties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.whopper.infra.s3;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "aws.s3")
public record AwsS3Properties(
String url,
String bucket,
String profileFolder,
String documentFolder,
String pdfFolder
) {
}
13 changes: 12 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,22 @@ jwt:
teacherSecret: ${TEACHER_SECRET}
studentSecret: ${STUDENT_SECRET}


redis:
host: ${REDIS_HOST}
port: 6379

aws:
region: ${AWS_REGION}
credentials:
access-key: ${AWS_ACCESS_KEY_ID}
secret-key: ${AWS_SECRET_ACCESS_KEY}
s3:
url: ${AWS_S3_URL}
bucket: ${AWS_S3_BUCKET}
profile-folder: ${PROFILE_FOLDER}
document-folder: ${DOC_FOLDER}
pdf-folder: ${PDF_FOLDER}

key:
login-api-url: ${LOGIN_URL}

Expand Down

0 comments on commit 3846291

Please sign in to comment.