Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] s3 presignedUrl 발급 기능 구현 #84

Merged
merged 13 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,16 @@ jobs:
# Gradle test를 실행
- name: Test with Gradle
run: ./gradlew clean testCoverage --no-daemon
env:
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
S3_BUCKET: ${{ secrets.S3_TEST_BUCKET }}
k-kbk marked this conversation as resolved.
Show resolved Hide resolved

# Report upload
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
flags: '!**/s3/*'
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
file: ./build/reports/jacoco/test/jacocoTestReport.xml
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,5 @@ gradle-app.setting
*.hprof

redis/

.run
6 changes: 6 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ ARG PROFILE=prod
ARG DB_URL
ARG DB_USERNAME
ARG DB_PASSWORD
ARG S3_ACCESS_KEY
ARG S3_SECRET_KEY
ARG S3_BUCKET

COPY ${JAR_FILE} app.jar

ENV PROFILE=${PROFILE}
ENV DB_URL=${DB_URL}
ENV DB_USERNAME=${DB_USERNAME}
ENV DB_PASSWORD=${DB_PASSWORD}
ENV S3_ACCESS_KEY=${S3_ACCESS_KEY}
ENV S3_SECRET_KEY=${S3_SECRET_KEY}
ENV S3_BUCKET=${S3_BUCKET}

ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=${PROFILE}", "-Djava.security.egd=file:/dev/./urandom", "/app.jar"]
6 changes: 5 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ dependencies {
jooqGenerator("org.jooq:jooq-meta-extensions-liquibase")
jooqGenerator("org.liquibase:liquibase-core")

// aws
implementation("software.amazon.awssdk:s3:2.22.12")

// test
testImplementation("org.testcontainers:postgresql")
testImplementation("org.testcontainers:testcontainers:$testContainerVersion")
Expand Down Expand Up @@ -194,7 +197,8 @@ tasks.jacocoTestCoverageVerification {
"*.dto.*",
"com.mjucow.eatda.jooq.*",
"*.Companion",
"*.popularstore.*" // FIXME: redis 이슈 해결 후 제거'[
"*.s3.*",
"*.popularstore.*" // FIXME: redis 이슈 해결 후 제거
)
}
}
Expand Down
6 changes: 3 additions & 3 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ applicationVersion=0.1.8-RELEASE
### Project configs ###
projectGroup="com.mjucow"

### Project depdency versions ###
### Project dependency versions ###
kotlinVersion=1.9.10
javaVersion=17

### Plugin depdency versions ###
### Plugin dependency versions ###
asciidoctorConvertVersion=3.3.2
ktlintVersion=11.6.0
jacocoVersion=0.8.9
Expand All @@ -17,7 +17,7 @@ jacocoVersion=0.8.9
springBootVersion=3.1.6
springDependencyManagementVersion=1.1.3

### DB depedency versions ###
### DB dependency versions ###
jooqPluginVersion=8.2
jooqVersion="3.18.4"

Expand Down
29 changes: 29 additions & 0 deletions src/main/kotlin/com/mjucow/eatda/common/config/S3Config.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.mjucow.eatda.common.config

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.presigner.S3Presigner

@Configuration
@Profile("prod")
class S3Config(
@Value("\${aws.s3.credentials.access-key}")
private val accessKey: String,
@Value("\${aws.s3.credentials.secret-key}")
private val secretKey: String,
) {

@Bean
fun s3Presigner(): S3Presigner {
val credential = AwsBasicCredentials.create(accessKey, secretKey)
k-kbk marked this conversation as resolved.
Show resolved Hide resolved

return S3Presigner.builder()
.region(Region.AP_NORTHEAST_2)
.credentialsProvider { credential }
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.mjucow.eatda.domain.s3.dto

import java.net.URL

data class PresignedUrlDto(
val url: URL,
) {

companion object {
fun from(url: URL): PresignedUrlDto {
return PresignedUrlDto(url)
}
}
}
59 changes: 59 additions & 0 deletions src/main/kotlin/com/mjucow/eatda/domain/s3/service/S3Service.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.mjucow.eatda.domain.s3.service

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Service
import software.amazon.awssdk.services.s3.model.GetObjectRequest
import software.amazon.awssdk.services.s3.model.PutObjectRequest
import software.amazon.awssdk.services.s3.presigner.S3Presigner
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest
import java.net.URL
import java.time.Duration

@Service
@Profile("prod")
class S3Service(
private val s3Presigner: S3Presigner,
@Value("\${aws.s3.bucket}")
private val bucket: String,
) {

fun createPutPresignedUrl(key: String, contentType: String): URL {
val putObjectRequest = PutObjectRequest
.builder()
.bucket(bucket)
.key(key)
.contentType(contentType)
.build()

val presignRequest = PutObjectPresignRequest
.builder()
.signatureDuration(Duration.ofMinutes(UPLOAD_DURATION_MINUTES))
.putObjectRequest(putObjectRequest)
.build()

return s3Presigner.presignPutObject(presignRequest).url()
}

fun createGetPresignedUrl(key: String): URL {
val getObjectRequest = GetObjectRequest
.builder()
.bucket(bucket)
.key(key)
.build()

val presignRequest = GetObjectPresignRequest
.builder()
.signatureDuration(Duration.ofHours(DOWNLOAD_DURATION_HOURS))
.getObjectRequest(getObjectRequest)
.build()

return s3Presigner.presignGetObject(presignRequest).url()
}

companion object {
const val UPLOAD_DURATION_MINUTES = 3L
const val DOWNLOAD_DURATION_HOURS = 24L
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.mjucow.eatda.presentation.s3

import com.mjucow.eatda.domain.s3.dto.PresignedUrlDto
import com.mjucow.eatda.presentation.common.ApiResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag

@Tag(name = "S3 API", description = "S3 관련 API")
interface S3ApiPresentation {

@Operation(
summary = "이미지 업로드용 presigned URL 발급",
description = "*key: 버킷의 폴더 경로"
)
fun getPutPresignedUrl(key: String, contentType: String): ApiResponse<PresignedUrlDto>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.mjucow.eatda.presentation.s3

import com.mjucow.eatda.domain.s3.dto.PresignedUrlDto
import com.mjucow.eatda.domain.s3.service.S3Service
import com.mjucow.eatda.presentation.common.ApiResponse
import org.springframework.context.annotation.Profile
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@RequestMapping("/api/v1/s3")
@RestController
@Profile("prod")
class S3Controller(
private val s3Service: S3Service,
) : S3ApiPresentation {

@GetMapping("/presigned-url")
override fun getPutPresignedUrl(
@RequestParam key: String,
@RequestParam contentType: String,
): ApiResponse<PresignedUrlDto> {
return ApiResponse.success(
PresignedUrlDto(
s3Service.createPutPresignedUrl(
key = key,
contentType = contentType
)
)
)
}
}
7 changes: 7 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,10 @@ spring:

jooq:
sql-dialect: postgres

aws:
s3:
credentials:
access-key: ${S3_ACCESS_KEY}
secret-key: ${S3_SECRET_KEY}
bucket: ${S3_BUCKET}
k-kbk marked this conversation as resolved.
Show resolved Hide resolved
38 changes: 19 additions & 19 deletions src/test/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
spring:
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
format_sql: true
show_sql: true
database-platform: org.hibernate.dialect.PostgreSQLDialect
database: postgresql
liquibase:
change-log: classpath:/db/changelog-master.yml
enabled: true
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
format_sql: true
show_sql: true
database-platform: org.hibernate.dialect.PostgreSQLDialect
database: postgresql
liquibase:
change-log: classpath:/db/changelog-master.yml
enabled: true

datasource:
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
url: jdbc:tc:postgresql:15.4-alpine://test-database
datasource:
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
url: jdbc:tc:postgresql:15.4-alpine://test-database

data:
redis:
host: 127.0.0.1
port: 6379
data:
redis:
host: 127.0.0.1
port: 6379

logging.config: classpath:logback-test.xml
18 changes: 9 additions & 9 deletions src/test/resources/logback-test.xml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<Logger level="debug" name="org.jooq">
<AppenderRef ref="Console"/>
</Logger>

<appender class="ch.qos.logback.core.ConsoleAppender" name="STDOUT">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern>
</encoder>
</appender>

<!-- Other jOOQ related debug log output -->
<logger level="INFO" name="org.testcontainers"/>

<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>

<!-- Other jOOQ related debug log output -->
<Logger name="org.jooq" level="debug">
<AppenderRef ref="Console"/>
</Logger>

<logger name="org.testcontainers" level="INFO"/>
</configuration>
Loading