diff --git a/build.gradle.kts b/build.gradle.kts index 9e63dc58..472a8f08 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -179,7 +179,8 @@ dependencies { implementation("org.postgresql:postgresql:42.6.0") implementation("com.twelvemonkeys.imageio:imageio-webp:3.10.0") - + implementation("org.apache.tika:tika-core:2.9.1") + implementation("net.coobird:thumbnailator:0.4.20") implementation("io.ktor:ktor-client-logging-jvm:$ktor_version") diff --git a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt index eb76234f..a5762bb1 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt @@ -59,7 +59,7 @@ import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey import java.util.* -@EnableWebSecurity(debug = true) +@EnableWebSecurity(debug = false) @Configuration @Suppress("FunctionMaxLength", "TooManyFunctions") class SecurityConfig { @@ -73,7 +73,12 @@ class SecurityConfig { @Bean @Order(1) - fun httpSignatureFilterChain(http: HttpSecurity, httpSignatureFilter: HttpSignatureFilter): SecurityFilterChain { + fun httpSignatureFilterChain( + http: HttpSecurity, + httpSignatureFilter: HttpSignatureFilter, + introspector: HandlerMappingIntrospector + ): SecurityFilterChain { + val builder = MvcRequestMatcher.Builder(introspector) http .securityMatcher("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox", "/users/*/posts/*") .addFilter(httpSignatureFilter) @@ -82,7 +87,12 @@ class SecurityConfig { HttpSignatureFilter::class.java ) .authorizeHttpRequests { - it.requestMatchers("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox").authenticated() + it.requestMatchers( + builder.pattern("/inbox"), + builder.pattern("/outbox"), + builder.pattern("/users/*/inbox"), + builder.pattern("/users/*/outbox") + ).authenticated() it.anyRequest().permitAll() } .csrf { diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/media/MediaProcessException.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/media/MediaProcessException.kt new file mode 100644 index 00000000..4292d0e8 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/media/MediaProcessException.kt @@ -0,0 +1,14 @@ +package dev.usbharu.hideout.core.domain.exception.media + +class MediaProcessException : MediaException { + constructor() : super() + constructor(message: String?) : super(message) + constructor(message: String?, cause: Throwable?) : super(message, cause) + constructor(cause: Throwable?) : super(cause) + constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super( + message, + cause, + enableSuppression, + writableStackTrace + ) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/Media.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/Media.kt index bf5d35cb..d9697059 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/Media.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/Media.kt @@ -1,6 +1,7 @@ package dev.usbharu.hideout.core.domain.model.media import dev.usbharu.hideout.core.service.media.FileType +import dev.usbharu.hideout.core.service.media.MimeType data class Media( val id: Long, @@ -9,5 +10,6 @@ data class Media( val remoteUrl: String?, val thumbnailUrl: String?, val type: FileType, + val mimeType: MimeType, val blurHash: String? ) diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/MediaRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/MediaRepositoryImpl.kt index f47d946f..7ef54c00 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/MediaRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/MediaRepositoryImpl.kt @@ -3,7 +3,9 @@ package dev.usbharu.hideout.core.infrastructure.exposedrepository import dev.usbharu.hideout.application.service.id.IdGenerateService import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException import dev.usbharu.hideout.core.domain.model.media.MediaRepository +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Media.mimeType import dev.usbharu.hideout.core.service.media.FileType +import dev.usbharu.hideout.core.service.media.MimeType import dev.usbharu.hideout.util.singleOr import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq @@ -59,18 +61,23 @@ class MediaRepositoryImpl(private val idGenerateService: IdGenerateService) : Me } fun ResultRow.toMedia(): EntityMedia { + val fileType = FileType.values().first { it.ordinal == this[Media.type] } + val mimeType = this[Media.mimeType] return EntityMedia( id = this[Media.id], name = this[Media.name], url = this[Media.url], remoteUrl = this[Media.remoteUrl], thumbnailUrl = this[Media.thumbnailUrl], - type = FileType.values().first { it.ordinal == this[Media.type] }, + type = fileType, blurHash = this[Media.blurhash], + mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType) ) } fun ResultRow.toMediaOrNull(): EntityMedia? { + val fileType = FileType.values().first { it.ordinal == (this.getOrNull(Media.type) ?: return null) } + val mimeType = this.getOrNull(Media.mimeType) ?: return null return EntityMedia( id = this.getOrNull(Media.id) ?: return null, name = this.getOrNull(Media.name) ?: return null, @@ -79,6 +86,7 @@ fun ResultRow.toMediaOrNull(): EntityMedia? { thumbnailUrl = this[Media.thumbnailUrl], type = FileType.values().first { it.ordinal == this.getOrNull(Media.type) }, blurHash = this[Media.blurhash], + mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType) ) } @@ -90,5 +98,6 @@ object Media : Table("media") { val thumbnailUrl = varchar("thumbnail_url", 255).nullable() val type = integer("type") val blurhash = varchar("blurhash", 255).nullable() + val mimeType = varchar("mime_type", 255) override val primaryKey = PrimaryKey(id) } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/ApatcheTikaFileTypeDeterminationService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/ApatcheTikaFileTypeDeterminationService.kt new file mode 100644 index 00000000..d5ca9ef8 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/ApatcheTikaFileTypeDeterminationService.kt @@ -0,0 +1,89 @@ +package dev.usbharu.hideout.core.service.media + +import org.apache.tika.Tika +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import java.nio.file.Path + +@Component +class ApatcheTikaFileTypeDeterminationService : FileTypeDeterminationService { + override fun fileType( + byteArray: ByteArray, + filename: String, + contentType: String? + ): MimeType { + logger.info("START Detect file type name: {}", filename) + + val tika = Tika() + + val detect = try { + tika.detect(byteArray, filename) + } catch (e: IllegalStateException) { + logger.warn("FAILED Detect file type", e) + "application/octet-stream" + } + + val type = detect.substringBefore("/") + val fileType = when (type) { + "image" -> { + FileType.Image + } + + "video" -> { + FileType.Video + } + + "audio" -> { + FileType.Audio + } + + else -> { + FileType.Unknown + } + } + val mimeType = MimeType(type, detect.substringAfter("/"), fileType) + + logger.info("SUCCESS Detect file type name: {},MimeType: {}", filename, mimeType) + return mimeType + } + + override fun fileType(path: Path, filename: String): MimeType { + logger.info("START Detect file type name: {}", filename) + + val tika = Tika() + + val detect = try { + tika.detect(path) + } catch (e: IllegalStateException) { + logger.warn("FAILED Detect file type", e) + "application/octet-stream" + } + + val type = detect.substringBefore("/") + val fileType = when (type) { + "image" -> { + FileType.Image + } + + "video" -> { + FileType.Video + } + + "audio" -> { + FileType.Audio + } + + else -> { + FileType.Unknown + } + } + val mimeType = MimeType(type, detect.substringAfter("/"), fileType) + + logger.info("SUCCESS Detect file type name: {},MimeType: {}", filename, mimeType) + return mimeType + } + + companion object { + private val logger = LoggerFactory.getLogger(ApatcheTikaFileTypeDeterminationService::class.java) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/FileTypeDeterminationService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/FileTypeDeterminationService.kt index f19f72be..7746b5c9 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/media/FileTypeDeterminationService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/FileTypeDeterminationService.kt @@ -1,5 +1,8 @@ package dev.usbharu.hideout.core.service.media +import java.nio.file.Path + interface FileTypeDeterminationService { - fun fileType(byteArray: ByteArray, filename: String, contentType: String?): FileType + fun fileType(byteArray: ByteArray, filename: String, contentType: String?): MimeType + fun fileType(path: Path, filename: String): MimeType } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/FileTypeDeterminationServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/FileTypeDeterminationServiceImpl.kt deleted file mode 100644 index 8d5d0226..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/media/FileTypeDeterminationServiceImpl.kt +++ /dev/null @@ -1,26 +0,0 @@ -package dev.usbharu.hideout.core.service.media - -import org.springframework.stereotype.Component - -@Component -class FileTypeDeterminationServiceImpl : FileTypeDeterminationService { - override fun fileType( - byteArray: ByteArray, - filename: String, - contentType: String? - ): FileType { - if (contentType == null) { - return FileType.Unknown - } - if (contentType.startsWith("image")) { - return FileType.Image - } - if (contentType.startsWith("video")) { - return FileType.Video - } - if (contentType.startsWith("audio")) { - return FileType.Audio - } - return FileType.Unknown - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaDataStore.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaDataStore.kt index 0545a577..03ee3e9f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaDataStore.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaDataStore.kt @@ -2,5 +2,6 @@ package dev.usbharu.hideout.core.service.media interface MediaDataStore { suspend fun save(dataMediaSave: MediaSave): SavedMedia + suspend fun save(dataSaveRequest: MediaSaveRequest): SavedMedia suspend fun delete(id: String) } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaSaveRequest.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaSaveRequest.kt new file mode 100644 index 00000000..d1bf321d --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaSaveRequest.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.core.service.media + +import java.nio.file.Path + +data class MediaSaveRequest( + val name: String, + val preffix: String, + val filePath: Path, + val thumbnailPath: Path? +) diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaServiceImpl.kt index ca0f1387..2d1912ed 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaServiceImpl.kt @@ -1,6 +1,5 @@ package dev.usbharu.hideout.core.service.media -import dev.usbharu.hideout.core.domain.exception.media.MediaFileSizeIsZeroException import dev.usbharu.hideout.core.domain.exception.media.MediaSaveException import dev.usbharu.hideout.core.domain.exception.media.UnsupportedMediaException import dev.usbharu.hideout.core.domain.model.media.Media @@ -16,6 +15,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.slf4j.LoggerFactory import org.springframework.stereotype.Service +import java.nio.file.Files import java.util.* import javax.imageio.ImageIO import dev.usbharu.hideout.core.domain.model.media.Media as EntityMedia @@ -27,72 +27,92 @@ class MediaServiceImpl( private val fileTypeDeterminationService: FileTypeDeterminationService, private val mediaBlurhashService: MediaBlurhashService, private val mediaRepository: MediaRepository, - private val mediaProcessService: MediaProcessService, + private val mediaProcessServices: List, private val httpClient: HttpClient ) : MediaService { override suspend fun uploadLocalMedia(mediaRequest: MediaRequest): EntityMedia { + val fileName = mediaRequest.file.name logger.info( - "Media upload. filename:${mediaRequest.file.name} size:${mediaRequest.file.size} " + + "Media upload. filename:$fileName " + "contentType:${mediaRequest.file.contentType}" ) - if (mediaRequest.file.size == 0L) { - throw MediaFileSizeIsZeroException("Media file size is zero.") - } - - val fileType = fileTypeDeterminationService.fileType( - mediaRequest.file.bytes, - mediaRequest.file.name, - mediaRequest.file.contentType - ) - if (fileType != FileType.Image) { - throw UnsupportedMediaException("FileType: $fileType is not supported.") - } - - val process = mediaProcessService.process( - fileType, - mediaRequest.file.contentType.orEmpty(), - mediaRequest.file.name, - mediaRequest.file.bytes, - mediaRequest.thumbnail?.bytes - ) - - val dataMediaSave = MediaSave( - "${UUID.randomUUID()}.${process.file.extension}", - "", - process.file.byteArray, - process.thumbnail?.byteArray - ) - val save = try { - mediaDataStore.save(dataMediaSave) - } catch (e: Exception) { - logger.warn("Failed save media", e) - throw MediaSaveException("Failed save media.", e) - } - - if (save.success.not()) { - save as FaildSavedMedia - logger.warn("Failed save media. reason: ${save.reason}") - logger.warn(save.description, save.trace) - throw MediaSaveException("Failed save media.") - } - save as SuccessSavedMedia - - val blurHash = withContext(Dispatchers.IO) { - mediaBlurhashService.generateBlurhash(ImageIO.read(mediaRequest.file.bytes.inputStream())) - } - - return mediaRepository.save( - EntityMedia( - id = mediaRepository.generateId(), - name = mediaRequest.file.name, - url = save.url, - remoteUrl = null, - thumbnailUrl = save.thumbnailUrl, - type = fileType, - blurHash = blurHash + val tempFile = Files.createTempFile("hideout-tmp-file", ".tmp") + AutoCloseable { println(tempFile);Files.delete(tempFile) }.use { + Files.newOutputStream(tempFile).use { outputStream -> + mediaRequest.file.inputStream.use { + it.transferTo(outputStream) + } + } + val mimeType = fileTypeDeterminationService.fileType( + tempFile, + fileName, ) - ) + val process = try { + mediaProcessServices.first { it.isSupport(mimeType) }.process( + mimeType, + fileName, + tempFile, + null + ) + } catch (e: NoSuchElementException) { + throw UnsupportedMediaException("MediaType: $mimeType isn't supported.") + } + val dataMediaSave = MediaSaveRequest( + process.filePath.fileName.toString(), + "", + process.filePath, + process.thumbnailPath + ) + val save = try { + mediaDataStore.save(dataMediaSave) + } catch (e: Exception) { + logger.warn("Failed to save the media", e) + throw MediaSaveException("Failed to save the media.", e) + } + if (save.success.not()) { + save as FaildSavedMedia + logger.warn("Failed to save the media. reason: ${save.reason}") + logger.warn(save.description, save.trace) + throw MediaSaveException("Failed to save the media.") + } + save as SuccessSavedMedia + val blurHash = withContext(Dispatchers.IO) { + if (process.thumbnailPath != null && process.thumbnailMimeType != null) { + val iterator = + ImageIO.getImageReadersByMIMEType(process.thumbnailMimeType.type + "/" + process.thumbnailMimeType.subtype) + for (imageReader in iterator) { + try { + ImageIO.createImageInputStream(process.thumbnailPath.toFile()).use { + imageReader.input = it + val read = imageReader.read(0) + return@withContext mediaBlurhashService.generateBlurhash(read) + } + } catch (e: Exception) { + logger.warn("Failed to read thumbnail", e) + } + + } + "" + } else { + "" + } + } + return mediaRepository.save( + EntityMedia( + id = mediaRepository.generateId(), + name = fileName, + url = save.url, + remoteUrl = null, + thumbnailUrl = save.thumbnailUrl, + type = process.fileMimeType.fileType, + mimeType = process.fileMimeType, + blurHash = blurHash + ) + ) + } + + } // TODO: 仮の処理として保存したように動かす @@ -103,15 +123,15 @@ class MediaServiceImpl( val bytes = httpResponse.bodyAsChannel().toByteArray() val contentType = httpResponse.contentType()?.toString() - val fileType = + val mimeType = fileTypeDeterminationService.fileType(bytes, remoteMedia.name, contentType) - if (fileType != FileType.Image) { - throw UnsupportedMediaException("FileType: $fileType is not supported.") + if (mimeType.fileType != FileType.Image) { + throw UnsupportedMediaException("FileType: $mimeType isn't supported.") } - val processedMedia = mediaProcessService.process( - fileType = fileType, + val processedMedia = mediaProcessServices.first().process( + fileType = mimeType.fileType, contentType = contentType.orEmpty(), fileName = remoteMedia.name, file = bytes, @@ -134,9 +154,9 @@ class MediaServiceImpl( if (save.success.not()) { save as FaildSavedMedia - logger.warn("Failed save media. reason: ${save.reason}") + logger.warn("Failed to save the media. reason: ${save.reason}") logger.warn(save.description, save.trace) - throw MediaSaveException("Failed save media.") + throw MediaSaveException("Failed to save the media.") } save as SuccessSavedMedia @@ -151,7 +171,8 @@ class MediaServiceImpl( url = save.url, remoteUrl = remoteMedia.url, thumbnailUrl = save.thumbnailUrl, - type = fileType, + type = mimeType.fileType, + mimeType = mimeType, blurHash = blurhash ) ) diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/MimeType.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MimeType.kt new file mode 100644 index 00000000..e77aa3d4 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MimeType.kt @@ -0,0 +1,3 @@ +package dev.usbharu.hideout.core.service.media + +data class MimeType(val type: String, val subtype: String, val fileType: FileType) diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/ProcessedMediaPath.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/ProcessedMediaPath.kt new file mode 100644 index 00000000..6d81c320 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/ProcessedMediaPath.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.core.service.media + +import java.nio.file.Path + +data class ProcessedMediaPath( + val filePath: Path, + val thumbnailPath: Path?, + val fileMimeType: MimeType, + val thumbnailMimeType: MimeType? +) diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/S3MediaDataStore.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/S3MediaDataStore.kt index 0b60aac4..4377fdc0 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/media/S3MediaDataStore.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/S3MediaDataStore.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext +import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import software.amazon.awssdk.core.sync.RequestBody import software.amazon.awssdk.services.s3.S3Client @@ -54,6 +55,58 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig ) } + override suspend fun save(dataSaveRequest: MediaSaveRequest): SavedMedia { + logger.info("MEDIA upload. {}", dataSaveRequest.name) + + val fileUploadRequest = PutObjectRequest.builder() + .bucket(storageConfig.bucket) + .key(dataSaveRequest.name) + .build() + + logger.info("MEDIA upload. bucket: {} key: {}", storageConfig.bucket, dataSaveRequest.name) + + val thumbnailKey = "thumbnail-${dataSaveRequest.name}" + val thumbnailUploadRequest = PutObjectRequest.builder() + .bucket(storageConfig.bucket) + .key(thumbnailKey) + .build() + + logger.info("MEDIA upload. bucket: {} key: {}", storageConfig.bucket, thumbnailKey) + + withContext(Dispatchers.IO) { + awaitAll( + async { + if (dataSaveRequest.thumbnailPath != null) { + s3Client.putObject( + thumbnailUploadRequest, + RequestBody.fromFile(dataSaveRequest.thumbnailPath) + ) + } else { + null + } + }, + async { + s3Client.putObject(fileUploadRequest, RequestBody.fromFile(dataSaveRequest.filePath)) + } + ) + } + val successSavedMedia = SuccessSavedMedia( + name = dataSaveRequest.name, + url = "${storageConfig.publicUrl}/${storageConfig.bucket}/${dataSaveRequest.name}", + thumbnailUrl = "${storageConfig.publicUrl}/${storageConfig.bucket}/$thumbnailKey" + ) + + logger.info("SUCCESS Media upload. {}", dataSaveRequest.name) + logger.debug( + "name: {} url: {} thumbnail url: {}", + successSavedMedia.name, + successSavedMedia.url, + successSavedMedia.thumbnailUrl + ) + + return successSavedMedia + } + override suspend fun delete(id: String) { val fileDeleteRequest = DeleteObjectRequest.builder().bucket(storageConfig.bucket).key(id).build() val thumbnailDeleteRequest = @@ -61,4 +114,8 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig s3Client.deleteObject(fileDeleteRequest) s3Client.deleteObject(thumbnailDeleteRequest) } + + companion object { + private val logger = LoggerFactory.getLogger(S3MediaDataStore::class.java) + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/MediaProcessService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/MediaProcessService.kt index a7fed906..0a5770e3 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/MediaProcessService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/MediaProcessService.kt @@ -1,9 +1,14 @@ package dev.usbharu.hideout.core.service.media.converter import dev.usbharu.hideout.core.service.media.FileType +import dev.usbharu.hideout.core.service.media.MimeType import dev.usbharu.hideout.core.service.media.ProcessedMedia +import dev.usbharu.hideout.core.service.media.ProcessedMediaPath +import java.nio.file.Path interface MediaProcessService { + fun isSupport(mimeType: MimeType): Boolean + suspend fun process( fileType: FileType, contentType: String, @@ -11,4 +16,11 @@ interface MediaProcessService { file: ByteArray, thumbnail: ByteArray? ): ProcessedMedia + + suspend fun process( + mimeType: MimeType, + fileName: String, + filePath: Path, + thumbnails: Path? + ): ProcessedMediaPath } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/MediaProcessServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/MediaProcessServiceImpl.kt index e95b324a..0a1080c4 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/MediaProcessServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/MediaProcessServiceImpl.kt @@ -1,11 +1,10 @@ package dev.usbharu.hideout.core.service.media.converter import dev.usbharu.hideout.core.domain.exception.media.MediaConvertException -import dev.usbharu.hideout.core.service.media.FileType -import dev.usbharu.hideout.core.service.media.ProcessedMedia -import dev.usbharu.hideout.core.service.media.ThumbnailGenerateService +import dev.usbharu.hideout.core.service.media.* import org.slf4j.LoggerFactory import org.springframework.stereotype.Service +import java.nio.file.Path @Service @Suppress("TooGenericExceptionCaught") @@ -13,6 +12,10 @@ class MediaProcessServiceImpl( private val mediaConverterRoot: MediaConverterRoot, private val thumbnailGenerateService: ThumbnailGenerateService ) : MediaProcessService { + override fun isSupport(mimeType: MimeType): Boolean { + TODO("Not yet implemented") + } + override suspend fun process( fileType: FileType, contentType: String, @@ -42,6 +45,15 @@ class MediaProcessServiceImpl( ) } + override suspend fun process( + mimeType: MimeType, + fileName: String, + filePath: Path, + thumbnails: Path? + ): ProcessedMediaPath { + TODO("Not yet implemented") + } + companion object { private val logger = LoggerFactory.getLogger(MediaProcessServiceImpl::class.java) } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/image/ImageMediaProcessService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/image/ImageMediaProcessService.kt new file mode 100644 index 00000000..0efa6c83 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/image/ImageMediaProcessService.kt @@ -0,0 +1,96 @@ +package dev.usbharu.hideout.core.service.media.converter.image + +import dev.usbharu.hideout.core.domain.exception.media.MediaProcessException +import dev.usbharu.hideout.core.service.media.FileType +import dev.usbharu.hideout.core.service.media.MimeType +import dev.usbharu.hideout.core.service.media.ProcessedMedia +import dev.usbharu.hideout.core.service.media.ProcessedMediaPath +import dev.usbharu.hideout.core.service.media.converter.MediaProcessService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.slf4j.MDCContext +import kotlinx.coroutines.withContext +import net.coobird.thumbnailator.Thumbnails +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Service +import java.nio.file.Files +import java.nio.file.Path +import java.util.* +import javax.imageio.ImageIO +import kotlin.io.path.inputStream +import kotlin.io.path.outputStream + +@Service +@Qualifier("image") +class ImageMediaProcessService(private val imageMediaProcessorConfiguration: ImageMediaProcessorConfiguration?) : + MediaProcessService { + + private val convertType = imageMediaProcessorConfiguration?.convert ?: "jpeg" + + private val supportedTypes = imageMediaProcessorConfiguration?.supportedType ?: listOf("webp", "jpeg", "png") + + private val genThumbnail = imageMediaProcessorConfiguration?.thubnail?.generate ?: true + + private val width = imageMediaProcessorConfiguration?.thubnail?.width ?: 1000 + private val height = imageMediaProcessorConfiguration?.thubnail?.height ?: 1000 + + override fun isSupport(mimeType: MimeType): Boolean { + if (mimeType.type != "image") { + return false + } + return supportedTypes.contains(mimeType.subtype) + } + + override suspend fun process( + fileType: FileType, + contentType: String, + fileName: String, + file: ByteArray, + thumbnail: ByteArray? + ): ProcessedMedia { + TODO("Not yet implemented") + } + + override suspend fun process( + mimeType: MimeType, + fileName: String, + filePath: Path, + thumbnails: Path? + ): ProcessedMediaPath = withContext(Dispatchers.IO + MDCContext()) { + val bufferedImage = ImageIO.read(filePath.inputStream()) + val tempFileName = UUID.randomUUID().toString() + val tempFile = Files.createTempFile(tempFileName, "tmp") + + val thumbnailPath = if (genThumbnail) { + val tempThumbnailFile = Files.createTempFile("thumbnail-$tempFileName", ".tmp") + + tempThumbnailFile.outputStream().use { + val write = ImageIO.write( + if (thumbnails != null) { + Thumbnails.of(thumbnails.toFile()).size(width, height).asBufferedImage() + } else { + Thumbnails.of(bufferedImage).size(width, height).asBufferedImage() + }, convertType, it + ) + if (write) { + tempThumbnailFile + } else { + null + } + } + } else { + null + } + + tempFile.outputStream().use { + if (ImageIO.write(bufferedImage, convertType, it).not()) { + throw MediaProcessException("Failed to save a temporary file.") + } + } + ProcessedMediaPath( + tempFile, + thumbnailPath, + MimeType("image", convertType, FileType.Image), + MimeType("image", convertType, FileType.Image).takeIf { genThumbnail } + ) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/image/ImageMediaProcessorConfiguration.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/image/ImageMediaProcessorConfiguration.kt new file mode 100644 index 00000000..23eeadb7 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/image/ImageMediaProcessorConfiguration.kt @@ -0,0 +1,17 @@ +package dev.usbharu.hideout.core.service.media.converter.image + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("hideout.media.image") +data class ImageMediaProcessorConfiguration( + val convert: String?, + val thubnail: ImageMediaProcessorThumbnailConfiguration?, + val supportedType: List?, + + ) + +data class ImageMediaProcessorThumbnailConfiguration( + val generate: Boolean, + val width: Int, + val height: Int +)