mirror of https://github.com/usbharu/Hideout.git
feat: S3アップロードを実装
This commit is contained in:
parent
0fd620d16e
commit
60242693f9
|
@ -119,6 +119,8 @@ dependencies {
|
||||||
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
|
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
|
||||||
implementation("org.jetbrains.exposed:exposed-spring-boot-starter:0.44.0")
|
implementation("org.jetbrains.exposed:exposed-spring-boot-starter:0.44.0")
|
||||||
implementation("io.trbl:blurhash:1.0.0")
|
implementation("io.trbl:blurhash:1.0.0")
|
||||||
|
implementation("software.amazon.awssdk:s3:2.20.157")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
|
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package dev.usbharu.hideout.config
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
|
||||||
|
import software.amazon.awssdk.services.s3.S3Client
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class AwsConfig {
|
||||||
|
@Bean
|
||||||
|
fun s3(awsConfig: StorageConfig): S3Client {
|
||||||
|
return S3Client.builder()
|
||||||
|
.credentialsProvider { AwsBasicCredentials.create(awsConfig.accessKey, awsConfig.secretKey) }
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,8 @@
|
||||||
package dev.usbharu.hideout.domain.model
|
package dev.usbharu.hideout.domain.model
|
||||||
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
data class MediaSave(
|
data class MediaSave(
|
||||||
val name: String,
|
val name: String,
|
||||||
val prefix: String,
|
val prefix: String,
|
||||||
val fileInputStream: OutputStream,
|
val fileInputStream: ByteArray,
|
||||||
val thumbnailInputStream: OutputStream?
|
val thumbnailInputStream: ByteArray?
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,7 +6,6 @@ class SuccessSavedMedia(
|
||||||
val name: String,
|
val name: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val thumbnailUrl: String,
|
val thumbnailUrl: String,
|
||||||
val blurhash: String
|
|
||||||
) :
|
) :
|
||||||
SavedMedia(true)
|
SavedMedia(true)
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ class MediaServiceImpl(
|
||||||
throw UnsupportedMediaException("FileType: $fileType is not supported.")
|
throw UnsupportedMediaException("FileType: $fileType is not supported.")
|
||||||
}
|
}
|
||||||
|
|
||||||
val process = mediaProcessService.process(fileType, media.file.inputStream, media.thumbnail?.inputStream)
|
val process = mediaProcessService.process(fileType, media.file.bytes, media.thumbnail?.bytes)
|
||||||
|
|
||||||
val dataMediaSave = MediaSave(
|
val dataMediaSave = MediaSave(
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package dev.usbharu.hideout.service.media
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.config.StorageConfig
|
||||||
|
import dev.usbharu.hideout.domain.model.MediaSave
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.SavedMedia
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.SuccessSavedMedia
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import software.amazon.awssdk.core.sync.RequestBody
|
||||||
|
import software.amazon.awssdk.services.s3.S3Client
|
||||||
|
import software.amazon.awssdk.services.s3.model.GetUrlRequest
|
||||||
|
import software.amazon.awssdk.services.s3.model.PutObjectRequest
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig: StorageConfig) : MediaDataStore {
|
||||||
|
override suspend fun save(dataMediaSave: MediaSave): SavedMedia {
|
||||||
|
val fileUploadRequest = PutObjectRequest.builder()
|
||||||
|
.bucket(storageConfig.bucket)
|
||||||
|
.key(dataMediaSave.name)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val thumbnailKey = "thumbnail-${dataMediaSave.name}"
|
||||||
|
val thumbnailUploadRequest = PutObjectRequest.builder()
|
||||||
|
.bucket(storageConfig.bucket)
|
||||||
|
.key(thumbnailKey)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val pairList = withContext(Dispatchers.IO) {
|
||||||
|
awaitAll(
|
||||||
|
async {
|
||||||
|
if (dataMediaSave.thumbnailInputStream != null) {
|
||||||
|
s3Client.putObject(fileUploadRequest, RequestBody.fromBytes(dataMediaSave.thumbnailInputStream))
|
||||||
|
"thumbnail" to s3Client.utilities()
|
||||||
|
.getUrl(GetUrlRequest.builder().bucket(storageConfig.bucket).key(thumbnailKey).build())
|
||||||
|
} else {
|
||||||
|
"thumbnail" to null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
s3Client.putObject(thumbnailUploadRequest, RequestBody.fromBytes(dataMediaSave.fileInputStream))
|
||||||
|
"file" to s3Client.utilities()
|
||||||
|
.getUrl(GetUrlRequest.builder().bucket(storageConfig.bucket).key(dataMediaSave.name).build())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}.toMap()
|
||||||
|
return SuccessSavedMedia(
|
||||||
|
dataMediaSave.name,
|
||||||
|
pairList.getValue("file").toString(),
|
||||||
|
pairList.getValue("thumbnail").toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(id: Long) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,8 @@
|
||||||
package dev.usbharu.hideout.service.media
|
package dev.usbharu.hideout.service.media
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
interface ThumbnailGenerateService {
|
interface ThumbnailGenerateService {
|
||||||
fun generate(bufferedImage: InputStream, width: Int, height: Int): ByteArrayOutputStream
|
fun generate(bufferedImage: InputStream, width: Int, height: Int): ByteArray
|
||||||
fun generate(outputStream: OutputStream, width: Int, height: Int): ByteArrayOutputStream
|
fun generate(outputStream: ByteArray, width: Int, height: Int): ByteArray
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,8 @@ package dev.usbharu.hideout.service.media.converter
|
||||||
|
|
||||||
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
interface MediaConverter {
|
interface MediaConverter {
|
||||||
fun isSupport(fileType: FileType): Boolean
|
fun isSupport(fileType: FileType): Boolean
|
||||||
fun convert(inputStream: InputStream): OutputStream
|
fun convert(inputStream: InputStream): ByteArray
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,7 @@ package dev.usbharu.hideout.service.media.converter
|
||||||
|
|
||||||
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
interface MediaConverterRoot {
|
interface MediaConverterRoot {
|
||||||
suspend fun convert(fileType: FileType, inputStream: InputStream): OutputStream
|
suspend fun convert(fileType: FileType, inputStream: InputStream): ByteArray
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,18 @@
|
||||||
package dev.usbharu.hideout.service.media.converter
|
package dev.usbharu.hideout.service.media.converter
|
||||||
|
|
||||||
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class MediaConverterRootImpl(private val converters: List<MediaConverter>) : MediaConverterRoot {
|
class MediaConverterRootImpl(private val converters: List<MediaConverter>) : MediaConverterRoot {
|
||||||
override suspend fun convert(fileType: FileType, inputStream: InputStream): OutputStream {
|
override suspend fun convert(fileType: FileType, inputStream: InputStream): ByteArray {
|
||||||
return converters.find {
|
return converters.find {
|
||||||
it.isSupport(fileType)
|
it.isSupport(fileType)
|
||||||
}?.convert(inputStream) ?: inputStream.let {
|
}?.convert(inputStream) ?: withContext(Dispatchers.IO) {
|
||||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
inputStream.readAllBytes()
|
||||||
it.transferTo(byteArrayOutputStream)
|
|
||||||
byteArrayOutputStream
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
package dev.usbharu.hideout.service.media.converter
|
package dev.usbharu.hideout.service.media.converter
|
||||||
|
|
||||||
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
interface MediaProcessService {
|
interface MediaProcessService {
|
||||||
suspend fun process(
|
suspend fun process(
|
||||||
fileType: FileType,
|
fileType: FileType,
|
||||||
file: InputStream,
|
file: ByteArray,
|
||||||
thumbnail: InputStream?
|
thumbnail: ByteArray?
|
||||||
): Pair<OutputStream, OutputStream>
|
): Pair<ByteArray, ByteArray>
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,6 @@ import dev.usbharu.hideout.exception.media.MediaConvertException
|
||||||
import dev.usbharu.hideout.service.media.ThumbnailGenerateService
|
import dev.usbharu.hideout.service.media.ThumbnailGenerateService
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class MediaProcessServiceImpl(
|
class MediaProcessServiceImpl(
|
||||||
|
@ -15,18 +13,18 @@ class MediaProcessServiceImpl(
|
||||||
) : MediaProcessService {
|
) : MediaProcessService {
|
||||||
override suspend fun process(
|
override suspend fun process(
|
||||||
fileType: FileType,
|
fileType: FileType,
|
||||||
file: InputStream,
|
file: ByteArray,
|
||||||
thumbnail: InputStream?
|
thumbnail: ByteArray?
|
||||||
): Pair<OutputStream, OutputStream> {
|
): Pair<ByteArray, ByteArray> {
|
||||||
|
|
||||||
val fileInputStream = try {
|
val fileInputStream = try {
|
||||||
mediaConverterRoot.convert(fileType, file)
|
mediaConverterRoot.convert(fileType, file.inputStream().buffered())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn("Failed convert media.", e)
|
logger.warn("Failed convert media.", e)
|
||||||
throw MediaConvertException("Failed convert media.", e)
|
throw MediaConvertException("Failed convert media.", e)
|
||||||
}
|
}
|
||||||
val thumbnailInputStream = try {
|
val thumbnailInputStream = try {
|
||||||
thumbnail?.let { mediaConverterRoot.convert(fileType, it) }
|
thumbnail?.let { mediaConverterRoot.convert(fileType, it.inputStream().buffered()) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn("Failed convert thumbnail media.", e)
|
logger.warn("Failed convert thumbnail media.", e)
|
||||||
null
|
null
|
||||||
|
|
Loading…
Reference in New Issue