Skip to content

Commit

Permalink
feat: 이미지 업로드를 위한 presigned url 생성 (#184)
Browse files Browse the repository at this point in the history
* [#182] build: s3 의존성 추가

* [#182] feat: presigned url 생성 client 작성

* [#182] feat: presigned url 생성 api 작성

* [#182, #160] build: jacoco test coverage 검사 제거

* [#182] test: 인수테스트 로그 제거

* [#182] feat: 이미지 파일 이름 랜덤 생성

* [#160] build: sonarcloud test coverage 제거
  • Loading branch information
shin-mallang authored Jan 1, 2024
1 parent 8deab81 commit e2b9589
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 3 deletions.
7 changes: 5 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ dependencies {
implementation 'org.springframework.security:spring-security-crypto'
implementation 'org.jsoup:jsoup:1.16.1'

// S3
implementation(platform("software.amazon.awssdk:bom:2.20.56"))
implementation("software.amazon.awssdk:s3")

annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jakarta"

compileOnly 'org.projectlombok:lombok'
Expand Down Expand Up @@ -129,7 +133,6 @@ jacocoTestReport {
fileTree(dir: it, excludes: excludeCoverage)
}))
}
finalizedBy 'jacocoTestCoverageVerification'
}

jacocoTestCoverageVerification {
Expand Down Expand Up @@ -191,6 +194,6 @@ sonar {
property "sonar.projectKey", "Mallang-log_backend"
property "sonar.organization", "mallang-log"
property 'sonar.coverage.jacoco.xmlReportPaths', "${buildDir}/jacoco/index.xml"
property 'sonar.exclusions', excludeCoverage
property 'sonar.exclusions', "**"
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/mallang/auth/config/AuthConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ private AuthInterceptor setUpAuthInterceptor() {
.params(Map.of("unauthenticated", "true"))
.build(),
UriAndMethodAndParamCondition.builder()
.uriPatterns(Set.of("/members", "/members/login"))
.uriPatterns(Set.of("/members", "/members/login", "/infra/aws/s3/presigned-url"))
.httpMethods(Set.of(POST))
.build(),
UriAndMethodAndParamCondition.builder()
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/mallang/common/infra/s3/AwsS3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.mallang.common.infra.s3;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;

@Configuration
public class AwsS3Config {

/**
* S3Presigner 사용 후 close()를 권장하므로, Builder 를 반환하여 필요 시 객체를 만들어 사용 후 close 되도록 구현.
*/
@Bean
public S3Presigner.Builder s3PresignerBuilder() {
return S3Presigner.builder()
.credentialsProvider(InstanceProfileCredentialsProvider.create())
.region(Region.AP_NORTHEAST_2);
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/mallang/common/infra/s3/AwsS3Property.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.mallang.common.infra.s3;

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

@ConfigurationProperties(prefix = "aws.s3")
public record AwsS3Property(
String bucket,
String imagePath,
int presignedUrlExpiresMinutes
) {
}
43 changes: 43 additions & 0 deletions src/main/java/com/mallang/common/infra/s3/PresignedUrlService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.mallang.common.infra.s3;

import java.time.Duration;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;

@RequiredArgsConstructor
@Component
public class PresignedUrlService {

private final S3Presigner.Builder presignerBuilder;
private final AwsS3Property s3Property;

public String create(String imageExtension) {
String imageName = createImageName(imageExtension);
return createPresignedUrl(imageName);
}

private String createImageName(String imageExtension) {
String uuid = UUID.randomUUID().toString();
return uuid + "." + imageExtension;
}

private String createPresignedUrl(String imageName) {
try (S3Presigner presigner = presignerBuilder.build()) {
PutObjectRequest objectRequest = PutObjectRequest.builder()
.bucket(s3Property.bucket())
.key(s3Property.imagePath() + imageName)
.build();
PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(s3Property.presignedUrlExpiresMinutes()))
.putObjectRequest(objectRequest)
.build();
PresignedPutObjectRequest presignedRequest = presigner.presignPutObject(presignRequest);
return presignedRequest.url().toExternalForm();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.mallang.common.infra.s3.presentation;

public record CreatePresignedUrlRequest(
String imageExtension
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.mallang.common.infra.s3.presentation;

public record CreatePresignedUrlResponse(
String presignedUrl
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.mallang.common.infra.s3.presentation;

import com.mallang.common.infra.s3.PresignedUrlService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RequestMapping("/infra/aws/s3/presigned-url")
@RestController
public class PresignedUrlController {

private final PresignedUrlService presignedUrlService;

@PostMapping
public ResponseEntity<CreatePresignedUrlResponse> createPresignedUrl(
CreatePresignedUrlRequest request
) {
String url = presignedUrlService.create(request.imageExtension());
return ResponseEntity.ok(new CreatePresignedUrlResponse(url));
}
}
6 changes: 6 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ auth:
# level:
# org.hibernate.orm.jdbc.bind: TRACE

# sample
aws:
s3:
bucket: "bucket-name"
image-path: "images/" # 마지막 / 필수
presigned-url-expires-minutes: 10
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.mallang.acceptance.common.s3;

import com.mallang.common.infra.s3.AwsS3Property;
import com.mallang.common.infra.s3.PresignedUrlService;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import org.springframework.test.context.ActiveProfiles;
import software.amazon.awssdk.services.s3.presigner.S3Presigner.Builder;

@ActiveProfiles("test")
@Primary
@Component
public class MockPresignedUrlService extends PresignedUrlService {

public MockPresignedUrlService(
Builder presignerBuilder,
AwsS3Property s3Property
) {
super(presignerBuilder, s3Property);
}

@Override
public String create(String imageExtension) {
return "https://example/sample." + imageExtension;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.mallang.acceptance.common.s3;


import static io.restassured.RestAssured.given;
import static org.assertj.core.api.Assertions.assertThat;

import com.mallang.acceptance.AcceptanceTest;
import com.mallang.common.infra.s3.presentation.CreatePresignedUrlRequest;
import com.mallang.common.infra.s3.presentation.CreatePresignedUrlResponse;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

@DisplayName("S3 Presigned Url 인수테스트")
@SuppressWarnings("NonAsciiCharacters")
public class PresignedUrlAcceptanceTest extends AcceptanceTest {

@Nested
class PresignedUrl_생성_API {

@Test
void presignedUrl을_생성한다() {
// when
CreatePresignedUrlResponse response = given()
.body(new CreatePresignedUrlRequest("fileName"))
.post("/infra/aws/s3/presigned-url")
.then()
//.log().all()
.extract()
.as(CreatePresignedUrlResponse.class);

// then
String s = response.presignedUrl();
assertThat(s).isNotNull();
}
}
}

0 comments on commit e2b9589

Please sign in to comment.