diff --git a/build.gradle.kts b/build.gradle.kts index 0b2a4472..0b8f61b2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -119,6 +119,8 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-data-mongodb") implementation("org.jetbrains.exposed:exposed-spring-boot-starter:0.44.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") diff --git a/src/main/kotlin/dev/usbharu/hideout/config/AwsConfig.kt b/src/main/kotlin/dev/usbharu/hideout/config/AwsConfig.kt new file mode 100644 index 00000000..48b4d4bb --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/config/AwsConfig.kt @@ -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() + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/MediaSave.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/MediaSave.kt index 49dc04e1..0a32a8e3 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/MediaSave.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/MediaSave.kt @@ -1,10 +1,8 @@ package dev.usbharu.hideout.domain.model -import java.io.OutputStream - data class MediaSave( val name: String, val prefix: String, - val fileInputStream: OutputStream, - val thumbnailInputStream: OutputStream? + val fileInputStream: ByteArray, + val thumbnailInputStream: ByteArray? ) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/SavedMedia.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/SavedMedia.kt index 18dd9e9d..60ea05b8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/SavedMedia.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/SavedMedia.kt @@ -6,7 +6,6 @@ class SuccessSavedMedia( val name: String, val url: String, val thumbnailUrl: String, - val blurhash: String ) : SavedMedia(true) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/MediaServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/MediaServiceImpl.kt index 041deb57..b9afd85d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/media/MediaServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/MediaServiceImpl.kt @@ -37,7 +37,7 @@ class MediaServiceImpl( 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( UUID.randomUUID().toString(), diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/S3MediaDataStore.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/S3MediaDataStore.kt new file mode 100644 index 00000000..cc7d459f --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/S3MediaDataStore.kt @@ -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") + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/ThumbnailGenerateService.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/ThumbnailGenerateService.kt index 0d53fe3c..9be5c865 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/media/ThumbnailGenerateService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/ThumbnailGenerateService.kt @@ -1,10 +1,8 @@ package dev.usbharu.hideout.service.media -import java.io.ByteArrayOutputStream import java.io.InputStream -import java.io.OutputStream interface ThumbnailGenerateService { - fun generate(bufferedImage: InputStream, width: Int, height: Int): ByteArrayOutputStream - fun generate(outputStream: OutputStream, width: Int, height: Int): ByteArrayOutputStream + fun generate(bufferedImage: InputStream, width: Int, height: Int): ByteArray + fun generate(outputStream: ByteArray, width: Int, height: Int): ByteArray } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaConverter.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaConverter.kt index b57c7a50..06b6c624 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaConverter.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaConverter.kt @@ -2,9 +2,8 @@ package dev.usbharu.hideout.service.media.converter import dev.usbharu.hideout.domain.model.hideout.dto.FileType import java.io.InputStream -import java.io.OutputStream interface MediaConverter { fun isSupport(fileType: FileType): Boolean - fun convert(inputStream: InputStream): OutputStream + fun convert(inputStream: InputStream): ByteArray } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaConverterRoot.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaConverterRoot.kt index 77c99a8d..fd788277 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaConverterRoot.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaConverterRoot.kt @@ -2,8 +2,7 @@ package dev.usbharu.hideout.service.media.converter import dev.usbharu.hideout.domain.model.hideout.dto.FileType import java.io.InputStream -import java.io.OutputStream interface MediaConverterRoot { - suspend fun convert(fileType: FileType, inputStream: InputStream): OutputStream + suspend fun convert(fileType: FileType, inputStream: InputStream): ByteArray } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaConverterRootImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaConverterRootImpl.kt index 9c005458..404f3f34 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaConverterRootImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaConverterRootImpl.kt @@ -1,20 +1,18 @@ package dev.usbharu.hideout.service.media.converter import dev.usbharu.hideout.domain.model.hideout.dto.FileType +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.springframework.stereotype.Service -import java.io.ByteArrayOutputStream import java.io.InputStream -import java.io.OutputStream @Service class MediaConverterRootImpl(private val converters: List) : MediaConverterRoot { - override suspend fun convert(fileType: FileType, inputStream: InputStream): OutputStream { + override suspend fun convert(fileType: FileType, inputStream: InputStream): ByteArray { return converters.find { it.isSupport(fileType) - }?.convert(inputStream) ?: inputStream.let { - val byteArrayOutputStream = ByteArrayOutputStream() - it.transferTo(byteArrayOutputStream) - byteArrayOutputStream + }?.convert(inputStream) ?: withContext(Dispatchers.IO) { + inputStream.readAllBytes() } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaProcessService.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaProcessService.kt index c31ecd66..06f752ff 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaProcessService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaProcessService.kt @@ -1,13 +1,11 @@ package dev.usbharu.hideout.service.media.converter import dev.usbharu.hideout.domain.model.hideout.dto.FileType -import java.io.InputStream -import java.io.OutputStream interface MediaProcessService { suspend fun process( fileType: FileType, - file: InputStream, - thumbnail: InputStream? - ): Pair + file: ByteArray, + thumbnail: ByteArray? + ): Pair } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaProcessServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaProcessServiceImpl.kt index bbab8626..470e0bcf 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaProcessServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaProcessServiceImpl.kt @@ -5,8 +5,6 @@ import dev.usbharu.hideout.exception.media.MediaConvertException import dev.usbharu.hideout.service.media.ThumbnailGenerateService import org.slf4j.LoggerFactory import org.springframework.stereotype.Service -import java.io.InputStream -import java.io.OutputStream @Service class MediaProcessServiceImpl( @@ -15,18 +13,18 @@ class MediaProcessServiceImpl( ) : MediaProcessService { override suspend fun process( fileType: FileType, - file: InputStream, - thumbnail: InputStream? - ): Pair { + file: ByteArray, + thumbnail: ByteArray? + ): Pair { val fileInputStream = try { - mediaConverterRoot.convert(fileType, file) + mediaConverterRoot.convert(fileType, file.inputStream().buffered()) } catch (e: Exception) { logger.warn("Failed convert media.", e) throw MediaConvertException("Failed convert media.", e) } val thumbnailInputStream = try { - thumbnail?.let { mediaConverterRoot.convert(fileType, it) } + thumbnail?.let { mediaConverterRoot.convert(fileType, it.inputStream().buffered()) } } catch (e: Exception) { logger.warn("Failed convert thumbnail media.", e) null