Skip to content

Commit

Permalink
✨ [STMT-179] 이미지 업로드를 위한 Presigned URL 생성 기능 추가 (#120)
Browse files Browse the repository at this point in the history
* ✨ [STMT-179] 이미지 업로드를 위한 Presigned URL 생성 기능 추가

* ✨ [STMT-179] 파일 이름으로 이미지 유효성을 체크하는 기능 추가

* ✅ [STMT-179] 테스트시 동작하는 S3Presigner 객체 추가

* ♻️ [STMT-179] 파일의 유효성 체크를 유즈케이스에서 진행하도록 변경

* ✨ [STMT-179] 파일이름으로만 파일 유효성을 검증하는 기능 추가

* ✅ [STMT-179] Presigned url 경로 생성 기능 테스트 코드 및 문서화 진행

* ♻️ [STMT-179] 이미지 유효 시간을 환경변수로 관리하도록 변경

* ♻️ [STMT-179] 파일 경로를 별도의 Enum 클래스로 관리하도록 변경
  • Loading branch information
zxcv9203 authored May 6, 2024
1 parent c60f798 commit 9857e3b
Show file tree
Hide file tree
Showing 22 changed files with 538 additions and 132 deletions.
11 changes: 11 additions & 0 deletions src/docs/asciidoc/file-management-path.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.path로 전달할 수 있는 값

PresigendUrl 발급에 사용되는 path 필드는 어떤 기능에 대한 이미지인지를 명시해야합니다.

현재 지원 가능한 path 값은 다음과 같습니다.
|===
| 값 | 설명

| ACTIVITY | 스터디 활동 관련 이미지
|===

33 changes: 32 additions & 1 deletion src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -390,4 +390,35 @@ include::{snippets}/study-member-join/fail/not-exist-study/response-fields.adoc[

.존재하지 않는 멤버가 가입을 요청한 경우
include::{snippets}/study-member-join/fail/not-exist-member/response-body.adoc[]
include::{snippets}/study-member-join/fail/not-exist-member/response-fields.adoc[]
include::{snippets}/study-member-join/fail/not-exist-member/response-fields.adoc[]

== 파일 관리

=== Presigned URL 발급

파일 업로드를 위한 Presigned URL을 발급하는 API입니다.

해당 API는 1시간동안 유효합니다.

==== POST /api/v1/presigned-url

===== 요청

include::{snippets}/presigned-url-generate/success/http-request.adoc[]
include::{snippets}/presigned-url-generate/success/request-headers.adoc[]
include::{snippets}/presigned-url-generate/success/request-fields.adoc[]

include::file-management-path.adoc[]

===== 응답 성공 (200)
include::{snippets}/presigned-url-generate/success/response-body.adoc[]
include::{snippets}/presigned-url-generate/success/response-fields.adoc[]

===== 응답 실패 (400)
.파일이름이 유효하지 않은 경우
include::{snippets}/presigned-url-generate/fail/invalid-file-name/response-body.adoc[]
include::{snippets}/presigned-url-generate/fail/invalid-file-name/response-fields.adoc[]

.파일의 확장자가 유효하지 않은 경우
include::{snippets}/presigned-url-generate/fail/invalid-file-extension/response-body.adoc[]
include::{snippets}/presigned-url-generate/fail/invalid-file-extension/response-fields.adoc[]
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;

@Configuration
@Profile("!test")
Expand Down Expand Up @@ -38,4 +39,12 @@ public S3Client s3Client() {
.region(Region.of(region))
.build();
}

@Bean
public S3Presigner s3Presigner() {
return S3Presigner.builder()
.credentialsProvider(staticCredentialsProvider())
.region(Region.of(region))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public enum SuccessCode {
DELETE_SUCCESS(HttpStatus.OK, "삭제에 성공했습니다."),
STUDY_LEAVE_SUCCESS(HttpStatus.OK, "스터디 탈퇴에 성공했습니다."),
STUDY_KICK_SUCCESS(HttpStatus.OK, "스터디원 강퇴에 성공했습니다."),
PRESIGNED_URL_SUCCESS(HttpStatus.OK, "Presigned URL 생성에 성공했습니다."),

/**
* 201 - CREATED
Expand Down
88 changes: 49 additions & 39 deletions src/main/java/com/stumeet/server/common/util/FileValidator.java
Original file line number Diff line number Diff line change
@@ -1,51 +1,61 @@
package com.stumeet.server.common.util;

import org.springframework.web.multipart.MultipartFile;

import com.stumeet.server.common.response.ErrorCode;
import com.stumeet.server.file.domain.ImageContentType;
import com.stumeet.server.file.domain.exception.InvalidFileException;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class FileValidator {

public static boolean isValidImageFile(MultipartFile file) {
String fileName = file.getOriginalFilename();
String contentType = file.getContentType();
String extension = FileUtil.extractExtension(fileName);

return isValidFileName(fileName) && isValidFileContentType(contentType, extension);
}

public static void validateImageFile(MultipartFile file) {
String fileName = file.getOriginalFilename();
String contentType = file.getContentType();
String extension = FileUtil.extractExtension(fileName);

validateFileName(fileName);
validateImageContentType(contentType, extension);
}

public static void validateFileName(String fileName) {
if (!isValidFileName(fileName)) {
throw new InvalidFileException(ErrorCode.INVALID_FILE_NAME_EXCEPTION);
}
}

public static void validateImageContentType(String contentType, String extension) {
if (!isValidFileContentType(contentType, extension)) {
throw new InvalidFileException(ErrorCode.INVALID_FILE_CONTENT_TYPE_EXCEPTION);
}
}

private static boolean isValidFileName(String fileName) {
return fileName != null && fileName.contains(".");
}

private static boolean isValidFileContentType(String contentType, String extension) {
return contentType != null && ImageContentType.exists(contentType, extension);
}
public static boolean isValidImageFile(MultipartFile file) {
String fileName = file.getOriginalFilename();
String contentType = file.getContentType();
String extension = FileUtil.extractExtension(fileName);

return isValidFileName(fileName) && isValidFileContentType(contentType, extension);
}

public static void validateImageFile(String fileName) {
String extension = FileUtil.extractExtension(fileName);

validateFileName(fileName);
validateImageExtension(extension);
}

public static void validateImageFile(MultipartFile file) {
String fileName = file.getOriginalFilename();
String contentType = file.getContentType();
String extension = FileUtil.extractExtension(fileName);

validateFileName(fileName);
validateImageContentType(contentType, extension);
}

public static void validateFileName(String fileName) {
if (!isValidFileName(fileName)) {
throw new InvalidFileException(ErrorCode.INVALID_FILE_NAME_EXCEPTION);
}
}

public static void validateImageContentType(String contentType, String extension) {
if (!isValidFileContentType(contentType, extension)) {
throw new InvalidFileException(ErrorCode.INVALID_FILE_CONTENT_TYPE_EXCEPTION);
}
}
private static void validateImageExtension(String extension) {
if (!ImageContentType.exists(extension)) {
throw new InvalidFileException(ErrorCode.INVALID_FILE_EXTENSION_EXCEPTION);
}
}

private static boolean isValidFileName(String fileName) {
return fileName != null && fileName.contains(".");
}

private static boolean isValidFileContentType(String contentType, String extension) {
return contentType != null && ImageContentType.exists(contentType, extension);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.stumeet.server.file.adapter.in;

import com.stumeet.server.common.annotation.WebAdapter;
import com.stumeet.server.common.model.ApiResponse;
import com.stumeet.server.common.response.SuccessCode;
import com.stumeet.server.file.application.port.in.PresignedUrlGenerateUseCase;
import com.stumeet.server.file.application.port.in.command.PresignedUrlCommand;
import com.stumeet.server.file.application.port.in.response.PresignedUrlResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

@WebAdapter
@RequestMapping("/api/v1/presigned-url")
@RequiredArgsConstructor
public class PresignedUrlGenerateWebAdapter {

private final PresignedUrlGenerateUseCase presignedUrlGenerateUseCase;

@PostMapping
public ResponseEntity<ApiResponse<PresignedUrlResponse>> generatePresignedUrl(
@RequestBody PresignedUrlCommand request
) {
PresignedUrlResponse response = presignedUrlGenerateUseCase.generatePresignedUrl(request);

return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.success(SuccessCode.PRESIGNED_URL_SUCCESS, response));
}
}
Loading

0 comments on commit 9857e3b

Please sign in to comment.