Skip to content

Commit

Permalink
[feature] s3 presignedUrl 발급 기능 구현 (#84)
Browse files Browse the repository at this point in the history
  • Loading branch information
k-kbk authored Feb 6, 2024
1 parent 77fc5e6 commit 8676f3e
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 32 deletions.
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 }}

# 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)

return S3Presigner.builder()
.region(Region.AP_NORTHEAST_2)
.credentialsProvider { credential }
.build()
}
}
14 changes: 14 additions & 0 deletions src/main/kotlin/com/mjucow/eatda/domain/s3/dto/PresignedUrlDto.kt
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>
}
33 changes: 33 additions & 0 deletions src/main/kotlin/com/mjucow/eatda/presentation/s3/S3Controller.kt
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}
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>

0 comments on commit 8676f3e

Please sign in to comment.