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

[DEV-294] 파일서버 분리 #3

Merged
merged 3 commits into from
Jun 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
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ dependencies {
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation("com.sun.activation:jakarta.activation:1.2.2")

//Test
testImplementation "org.mockito.kotlin:mockito-kotlin:5.3.1"

// Logger
implementation 'io.github.oshai:kotlin-logging-jvm:5.1.0'
Expand Down
4 changes: 1 addition & 3 deletions src/main/kotlin/com/tiketeer/tiketeer/StorageFile.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.tiketeer.tiketeer

import org.springframework.http.codec.multipart.FilePart
import java.util.*


class StorageFile(val file: FilePart) {
val fileName = "${UUID.randomUUID()}_${file.filename()}"
class StorageFile(val file: FilePart, val fileName: String) {
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.tiketeer.tiketeer.configuration

import com.tiketeer.tiketeer.constant.StorageEnum
import com.tiketeer.tiketeer.strategy.FileStorageStrategy
import com.tiketeer.tiketeer.strategy.LocalFileStorageStrategy
import com.tiketeer.tiketeer.domain.file.strategy.LocalFileStorageStrategy
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.tiketeer.tiketeer.domain.file.controller

import com.tiketeer.tiketeer.StorageFile
import com.tiketeer.tiketeer.strategy.FileStorageStrategy
import com.tiketeer.tiketeer.domain.file.dto.UploadFileCommandDto
import com.tiketeer.tiketeer.domain.file.dto.UploadFileRequestDto
import com.tiketeer.tiketeer.domain.file.strategy.FileStorageStrategy
import com.tiketeer.tiketeer.domain.file.usecase.UploadFileUseCase
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.http.HttpHeaders
Expand All @@ -14,15 +16,25 @@ import reactor.core.publisher.Mono
import javax.activation.MimetypesFileTypeMap

@RestController
@RequestMapping("/files")
@RequestMapping("/file")
class FileController @Autowired constructor(
private val fileStorageStrategy: FileStorageStrategy
private val fileStorageStrategy: FileStorageStrategy,
private val uploadFileUseCase: UploadFileUseCase
) {

@PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
fun uploadFile(@RequestPart("file") file: Mono<FilePart>): Mono<ResponseEntity<Void>> {
return file.flatMap { filePart ->
fileStorageStrategy.uploadFile(StorageFile(filePart))
fun uploadFile(
@RequestPart("file") fileMono: Mono<FilePart>,
@RequestPart("dto") dtoMono: Mono<UploadFileRequestDto>
): Mono<ResponseEntity<Void>> {
return fileMono.zipWith(dtoMono) { file, dto ->
claycat marked this conversation as resolved.
Show resolved Hide resolved
UploadFileCommandDto(
fileName = dto.fileName,
signature = dto.signature,
file = file
)
}.flatMap { commandDto ->
uploadFileUseCase.uploadFile(commandDto)
}.then(Mono.just(ResponseEntity.ok().build()))
}

Expand All @@ -38,4 +50,6 @@ class FileController @Autowired constructor(
.body(fileStorageStrategy.retrieveFile(fileId))
)
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.tiketeer.tiketeer.domain.file.dto

import org.springframework.http.codec.multipart.FilePart

data class UploadFileCommandDto(
val fileName: String,
val signature: String,
val file: FilePart
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.tiketeer.tiketeer.domain.file.dto

data class UploadFileRequestDto(
val fileName: String,
val signature: String,
)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.tiketeer.tiketeer.strategy
package com.tiketeer.tiketeer.domain.file.strategy

import com.tiketeer.tiketeer.StorageFile
import org.springframework.core.io.buffer.DataBuffer
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.tiketeer.tiketeer.strategy
package com.tiketeer.tiketeer.domain.file.strategy

import com.tiketeer.tiketeer.StorageFile
import io.github.oshai.kotlinlogging.KotlinLogging
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.tiketeer.tiketeer.domain.file.usecase

import com.tiketeer.tiketeer.StorageFile
import com.tiketeer.tiketeer.domain.file.dto.UploadFileCommandDto
import com.tiketeer.tiketeer.domain.file.strategy.FileStorageStrategy
import com.tiketeer.tiketeer.domain.sign.SignService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono

@Service
class UploadFileUseCase @Autowired constructor(
private val fileStorageStrategy: FileStorageStrategy,
private val signService: SignService
) {
fun uploadFile(dto: UploadFileCommandDto): Mono<Void> {
return Mono.defer {
if (!signService.verify(dto.fileName, dto.signature)) {
return@defer Mono.error<Void>(RuntimeException("Invalid Signature"))
}
Comment on lines +18 to +20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 테스트 있나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가완료


val storageFile = StorageFile(
dto.file,
fileName = dto.fileName
)
fileStorageStrategy.uploadFile(storageFile)
.then()
}
}

}
34 changes: 34 additions & 0 deletions src/main/kotlin/com/tiketeer/tiketeer/domain/sign/SignService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.tiketeer.tiketeer.domain.sign

import jakarta.annotation.PostConstruct
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import java.security.MessageDigest
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

@Service
class SignService {

@Value("\${jwt.secret-key}")
private lateinit var secretKey: String

private lateinit var sha256Hmac: Mac

@PostConstruct
@Throws(Exception::class)
fun init() {
sha256Hmac = Mac.getInstance("HmacSHA256")
val secretKeySpec = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256")
sha256Hmac.init(secretKeySpec)
}

@Throws(Exception::class)
fun verify(target: String, signature: String): Boolean {
val newSignature = sha256Hmac.doFinal(target.toByteArray())
val providedSignature = Base64.getUrlDecoder().decode(signature)

return MessageDigest.isEqual(newSignature, providedSignature)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.tiketeer.tiketeer.msgconverter

import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.MediaType
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter
import org.springframework.lang.Nullable
import org.springframework.stereotype.Component

import java.lang.reflect.Type

@Component
class OctetStreamReadMsgConverter @Autowired constructor(objectMapper: ObjectMapper) :
AbstractJackson2HttpMessageConverter(objectMapper, MediaType.APPLICATION_OCTET_STREAM) {

override fun canWrite(clazz: Class<*>, mediaType: MediaType?): Boolean {
return false
}

override fun canWrite(@Nullable type: Type?, clazz: Class<*>, @Nullable mediaType: MediaType?): Boolean {
return false
}

override fun canWrite(mediaType: MediaType?): Boolean {
return false
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.tiketeer.tiketeer.configuration

import com.tiketeer.tiketeer.strategy.FileStorageStrategy
import com.tiketeer.tiketeer.strategy.LocalFileStorageStrategy
import com.tiketeer.tiketeer.domain.file.strategy.FileStorageStrategy
import com.tiketeer.tiketeer.domain.file.strategy.LocalFileStorageStrategy
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.tiketeer.tiketeer.domain.file.controller

import com.fasterxml.jackson.databind.ObjectMapper
import com.tiketeer.tiketeer.StorageFile
import com.tiketeer.tiketeer.strategy.FileStorageStrategy
import com.tiketeer.tiketeer.strategy.LocalFileStorageStrategy
import com.tiketeer.tiketeer.domain.file.dto.UploadFileRequestDto
import com.tiketeer.tiketeer.domain.file.strategy.FileStorageStrategy
import com.tiketeer.tiketeer.domain.file.strategy.LocalFileStorageStrategy
import com.tiketeer.tiketeer.strategy.MockFilePart
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
Expand All @@ -19,6 +22,9 @@ import org.springframework.web.reactive.function.BodyInserters
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class FileControllerTest {
Expand All @@ -31,6 +37,12 @@ class FileControllerTest {
@Autowired
lateinit var webTestClient: WebTestClient

@Autowired
lateinit var objectMapper: ObjectMapper

@Value("\${jwt.secret-key}")
private lateinit var secretKey: String


@TestConfiguration
class TestConfig() {
Expand All @@ -41,42 +53,55 @@ class FileControllerTest {
}

@Test
fun uploadFile() {
fun `파일 업로드 시도 - 성공`() {

//given
val sha256Mac: Mac = Mac.getInstance("HmacSHA256")
val secretKeySpec = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256")
sha256Mac.init(secretKeySpec)

val fileName = "image.png"
val signature = sha256Mac.doFinal(fileName.toByteArray())
val base64Signature = Base64.getUrlEncoder().withoutPadding().encodeToString(signature);

val image = MockMultipartFile(
"file", "image.png", "image/png", "Image Content".toByteArray()
"file", fileName, "image/png", "Image Content".toByteArray()
)

//when - then
val uploadFileRequestDto = UploadFileRequestDto(fileName, base64Signature)
val dtoJson = objectMapper.writeValueAsString(uploadFileRequestDto)

val multipart = MultipartBodyBuilder().apply {
part("file", image.resource)
part("dto", dtoJson, MediaType.APPLICATION_JSON)
}.build()

//when - then
webTestClient.post()
.uri("/files")
.uri("/file")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(multipart))
.exchange()
.expectStatus().isOk()

.expectStatus().isOk
}

@Test
fun getFile() {

fun `파일 이름 - 조회 시도 - 성공`() {
//given
val image = MockMultipartFile(
"file", "image.png", "image/png", "Image Content".toByteArray()
)

val filePart = MockFilePart(image)
val storageFile = StorageFile(filePart)
val storageFile = StorageFile(filePart, "image.png")
val fileName = storageFile.fileName
val filePath = tempDir.resolve(fileName)
//when
Files.write(filePath, "Image Content".toByteArray(), StandardOpenOption.CREATE)


//then
webTestClient.get()
.uri("/files/$fileName")
.uri("/file/$fileName")
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.IMAGE_PNG)
Expand Down
Loading
Loading