From f3b3a607ced52e99bd2f853fe4fbaefe004b712e Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 14 Nov 2023 22:04:41 +0900 Subject: [PATCH 1/9] =?UTF-8?q?refactor:=20=E3=83=A1=E3=83=87=E3=82=A3?= =?UTF-8?q?=E3=82=A2=E9=96=A2=E4=BF=82=E3=82=92=E3=83=AA=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 3 +- .../application/config/SecurityConfig.kt | 16 +- .../exception/media/MediaProcessException.kt | 14 ++ .../hideout/core/domain/model/media/Media.kt | 2 + .../exposedrepository/MediaRepositoryImpl.kt | 11 +- ...ApatcheTikaFileTypeDeterminationService.kt | 89 ++++++++++ .../media/FileTypeDeterminationService.kt | 5 +- .../media/FileTypeDeterminationServiceImpl.kt | 26 --- .../core/service/media/MediaDataStore.kt | 1 + .../core/service/media/MediaSaveRequest.kt | 10 ++ .../core/service/media/MediaServiceImpl.kt | 155 ++++++++++-------- .../hideout/core/service/media/MimeType.kt | 3 + .../core/service/media/ProcessedMediaPath.kt | 10 ++ .../core/service/media/S3MediaDataStore.kt | 57 +++++++ .../media/converter/MediaProcessService.kt | 12 ++ .../converter/MediaProcessServiceImpl.kt | 18 +- .../image/ImageMediaProcessService.kt | 96 +++++++++++ .../image/ImageMediaProcessorConfiguration.kt | 17 ++ 18 files changed, 443 insertions(+), 102 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/domain/exception/media/MediaProcessException.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/service/media/ApatcheTikaFileTypeDeterminationService.kt delete mode 100644 src/main/kotlin/dev/usbharu/hideout/core/service/media/FileTypeDeterminationServiceImpl.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaSaveRequest.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/service/media/MimeType.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/service/media/ProcessedMediaPath.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/image/ImageMediaProcessService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/image/ImageMediaProcessorConfiguration.kt 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 +) From f389fe2412f0bce287e01f56917837a5c1236602 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 14 Nov 2023 22:47:00 +0900 Subject: [PATCH 2/9] =?UTF-8?q?refactor:=20MediaServiceImpl=E3=82=92?= =?UTF-8?q?=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3?= =?UTF-8?q?=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/service/media/MediaServiceImpl.kt | 136 ++++++++++-------- .../dev/usbharu/hideout/util/TempFileUtil.kt | 12 ++ 2 files changed, 92 insertions(+), 56 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/util/TempFileUtil.kt 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 2d1912ed..eb3d0869 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 @@ -6,6 +6,7 @@ import dev.usbharu.hideout.core.domain.model.media.Media import dev.usbharu.hideout.core.domain.model.media.MediaRepository import dev.usbharu.hideout.core.service.media.converter.MediaProcessService import dev.usbharu.hideout.mastodon.interfaces.api.media.MediaRequest +import dev.usbharu.hideout.util.withDelete import io.ktor.client.* import io.ktor.client.request.* import io.ktor.client.statement.* @@ -38,78 +39,60 @@ class MediaServiceImpl( ) val tempFile = Files.createTempFile("hideout-tmp-file", ".tmp") - AutoCloseable { println(tempFile);Files.delete(tempFile) }.use { + + + + tempFile.withDelete().use { Files.newOutputStream(tempFile).use { outputStream -> mediaRequest.file.inputStream.use { it.transferTo(outputStream) } } - val mimeType = fileTypeDeterminationService.fileType( - tempFile, + val mimeType = fileTypeDeterminationService.fileType(tempFile, fileName) + + val process = findMediaProcessor(mimeType).process( + mimeType, fileName, + tempFile, + null ) - 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) - } - + dataMediaSave.filePath.withDelete().use { + dataMediaSave.thumbnailPath.withDelete().use { + 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) } - "" - } else { - "" + 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 = generateBlurhash(process) + 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 + ) + ) } } - 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 - ) - ) } @@ -178,6 +161,47 @@ class MediaServiceImpl( ) } + protected fun findMediaProcessor(mimeType: MimeType): MediaProcessService { + try { + return mediaProcessServices.first { + try { + it.isSupport(mimeType) + } catch (e: Exception) { + false + } + } + } catch (e: NoSuchElementException) { + throw UnsupportedMediaException("MediaType: $mimeType isn't supported.") + } + } + + protected fun generateBlurhash(process: ProcessedMediaPath): String { + val path = if (process.thumbnailPath != null && process.thumbnailMimeType != null) { + process.thumbnailPath + } else { + process.filePath + } + val mimeType = if (process.thumbnailPath != null && process.thumbnailMimeType != null) { + process.thumbnailMimeType + } else { + process.fileMimeType + } + + val imageReadersByMIMEType = ImageIO.getImageReadersByMIMEType(mimeType.type + "/" + mimeType.subtype) + for (imageReader in imageReadersByMIMEType) { + try { + val bufferedImage = ImageIO.createImageInputStream(path.toFile()).use { + imageReader.input = it + imageReader.read(0) + } + return mediaBlurhashService.generateBlurhash(bufferedImage) + } catch (e: Exception) { + logger.warn("Failed to read thumbnail", e) + } + } + return "" + } + companion object { private val logger = LoggerFactory.getLogger(MediaServiceImpl::class.java) } diff --git a/src/main/kotlin/dev/usbharu/hideout/util/TempFileUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/TempFileUtil.kt new file mode 100644 index 00000000..1a3ab50b --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/util/TempFileUtil.kt @@ -0,0 +1,12 @@ +package dev.usbharu.hideout.util + +import java.nio.file.Files +import java.nio.file.Path + +fun Path?.withDelete(): TempFile = TempFile(this) + +class TempFile(val path: Path?) : AutoCloseable { + override fun close() { + path?.let { Files.deleteIfExists(it) } + } +} From 5b4112e9ccc434bdb232791b9be3149e508c12f0 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 15 Nov 2023 13:07:42 +0900 Subject: [PATCH 3/9] =?UTF-8?q?test:=20SQL=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ィア付き投稿はattachmentにDocumentとして画像が存在する.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql b/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql index 9da287af..01352445 100644 --- a/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql +++ b/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql @@ -13,9 +13,9 @@ insert into POSTS (ID, "userId", OVERVIEW, TEXT, "createdAt", VISIBILITY, URL, " VALUES (1242, 11, null, 'test post', 12345680, 0, 'https://example.com/users/test-user11/posts/1242', null, null, false, 'https://example.com/users/test-user11/posts/1242'); -insert into MEDIA (ID, NAME, URL, REMOTE_URL, THUMBNAIL_URL, TYPE, BLURHASH) -VALUES (1, 'test-media', 'https://example.com/media/test-media.png', null, null, 0, null), - (2, 'test-media2', 'https://example.com/media/test-media2.png', null, null, 0, null); +insert into MEDIA (ID, NAME, URL, REMOTE_URL, THUMBNAIL_URL, TYPE, BLURHASH, MIME_TYPE) +VALUES (1, 'test-media', 'https://example.com/media/test-media.png', null, null, 0, null, 'image/png'), + (2, 'test-media2', 'https://example.com/media/test-media2.png', null, null, 0, null, 'image/png'); insert into POSTSMEDIA(POST_ID, MEDIA_ID) VALUES (1242, 1), From dc55bba2b30620564fbba18672972ac971355b35 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 15 Nov 2023 14:36:41 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=E3=83=AA=E3=83=A2=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=81=AE=E7=94=BB=E5=83=8F=E3=81=AEOOM=E5=AF=BE?= =?UTF-8?q?=E7=AD=96=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/service/media/MediaServiceImpl.kt | 106 +++++++----------- .../media/RemoteMediaDownloadService.kt | 7 ++ .../media/RemoteMediaDownloadServiceImpl.kt | 37 ++++++ .../dev/usbharu/hideout/util/TempFileUtil.kt | 4 +- 4 files changed, 88 insertions(+), 66 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/service/media/RemoteMediaDownloadService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/service/media/RemoteMediaDownloadServiceImpl.kt 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 eb3d0869..c5731bb4 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 @@ -7,29 +7,21 @@ import dev.usbharu.hideout.core.domain.model.media.MediaRepository import dev.usbharu.hideout.core.service.media.converter.MediaProcessService import dev.usbharu.hideout.mastodon.interfaces.api.media.MediaRequest import dev.usbharu.hideout.util.withDelete -import io.ktor.client.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.util.* -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 @Service @Suppress("TooGenericExceptionCaught") -class MediaServiceImpl( +open class MediaServiceImpl( private val mediaDataStore: MediaDataStore, private val fileTypeDeterminationService: FileTypeDeterminationService, private val mediaBlurhashService: MediaBlurhashService, private val mediaRepository: MediaRepository, private val mediaProcessServices: List, - private val httpClient: HttpClient + private val remoteMediaDownloadService: RemoteMediaDownloadService ) : MediaService { override suspend fun uploadLocalMedia(mediaRequest: MediaRequest): EntityMedia { val fileName = mediaRequest.file.name @@ -102,63 +94,49 @@ class MediaServiceImpl( override suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia): Media { logger.info("MEDIA Remote media. filename:${remoteMedia.name} url:${remoteMedia.url}") - val httpResponse = httpClient.get(remoteMedia.url) - val bytes = httpResponse.bodyAsChannel().toByteArray() + remoteMediaDownloadService.download(remoteMedia.url).withDelete().use { + val mimeType = fileTypeDeterminationService.fileType(it.path, remoteMedia.name) - val contentType = httpResponse.contentType()?.toString() - val mimeType = - fileTypeDeterminationService.fileType(bytes, remoteMedia.name, contentType) + val process = findMediaProcessor(mimeType).process(mimeType, remoteMedia.name, it.path, null) - if (mimeType.fileType != FileType.Image) { - throw UnsupportedMediaException("FileType: $mimeType isn't supported.") - } - - val processedMedia = mediaProcessServices.first().process( - fileType = mimeType.fileType, - contentType = contentType.orEmpty(), - fileName = remoteMedia.name, - file = bytes, - thumbnail = null - ) - - val mediaSave = MediaSave( - "${UUID.randomUUID()}.${processedMedia.file.extension}", - "", - processedMedia.file.byteArray, - processedMedia.thumbnail?.byteArray - ) - - val save = try { - mediaDataStore.save(mediaSave) - } 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 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) { - mediaBlurhashService.generateBlurhash(ImageIO.read(bytes.inputStream())) - } - - return mediaRepository.save( - EntityMedia( - id = mediaRepository.generateId(), - name = remoteMedia.name, - url = save.url, - remoteUrl = remoteMedia.url, - thumbnailUrl = save.thumbnailUrl, - type = mimeType.fileType, - mimeType = mimeType, - blurHash = blurhash + val mediaSaveRequest = MediaSaveRequest( + process.filePath.fileName.toString(), + "", + process.filePath, + process.thumbnailPath ) - ) + + mediaSaveRequest.filePath.withDelete().use { + mediaSaveRequest.filePath.withDelete().use { + val save = try { + mediaDataStore.save(mediaSaveRequest) + } catch (e: Exception) { + logger.warn("Failed to save the media", e) + throw MediaSaveException("Failed to save the media.", e) + } + + if (save is 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 = generateBlurhash(process) + return mediaRepository.save( + EntityMedia( + id = mediaRepository.generateId(), + name = remoteMedia.name, + url = save.url, + remoteUrl = remoteMedia.url, + thumbnailUrl = save.thumbnailUrl, + type = process.fileMimeType.fileType, + mimeType = process.fileMimeType, + blurHash = blurhash + ) + ) + } + } + } } protected fun findMediaProcessor(mimeType: MimeType): MediaProcessService { diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/RemoteMediaDownloadService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/RemoteMediaDownloadService.kt new file mode 100644 index 00000000..33b9b071 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/RemoteMediaDownloadService.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.core.service.media + +import java.nio.file.Path + +interface RemoteMediaDownloadService { + suspend fun download(url: String): Path +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/RemoteMediaDownloadServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/RemoteMediaDownloadServiceImpl.kt new file mode 100644 index 00000000..6bc9040c --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/RemoteMediaDownloadServiceImpl.kt @@ -0,0 +1,37 @@ +package dev.usbharu.hideout.core.service.media + +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.utils.io.jvm.javaio.* +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.outputStream + +@Service +class RemoteMediaDownloadServiceImpl(private val httpClient: HttpClient) : RemoteMediaDownloadService { + override suspend fun download(url: String): Path { + logger.info("START Download remote file. url: {}", url) + val httpResponse = httpClient.get(url) + httpResponse.contentLength() + val createTempFile = Files.createTempFile("hideout-remote-download", ".tmp") + + logger.debug("Save to {} url: {} ", createTempFile, url) + + httpResponse.bodyAsChannel().toInputStream().use { inputStream -> + createTempFile.outputStream().use { + inputStream.transferTo(it) + } + } + + logger.info("SUCCESS Download remote file. url: {}", url) + return createTempFile + } + + companion object { + private val logger = LoggerFactory.getLogger(RemoteMediaDownloadServiceImpl::class.java) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/util/TempFileUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/TempFileUtil.kt index 1a3ab50b..186aa889 100644 --- a/src/main/kotlin/dev/usbharu/hideout/util/TempFileUtil.kt +++ b/src/main/kotlin/dev/usbharu/hideout/util/TempFileUtil.kt @@ -3,9 +3,9 @@ package dev.usbharu.hideout.util import java.nio.file.Files import java.nio.file.Path -fun Path?.withDelete(): TempFile = TempFile(this) +fun T.withDelete(): TempFile = TempFile(this) -class TempFile(val path: Path?) : AutoCloseable { +class TempFile(val path: T) : AutoCloseable { override fun close() { path?.let { Files.deleteIfExists(it) } } From 4a20054ea4f0a56d5fb58a581845b78c12930d7f Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 15 Nov 2023 20:17:02 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20ALT=E3=81=AE=E6=B0=B8=E7=B6=9A?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activitypub/service/objects/note/APNoteService.kt | 3 ++- .../hideout/application/config/HttpClientConfig.kt | 2 +- .../usbharu/hideout/core/domain/model/media/Media.kt | 3 ++- .../exposedrepository/MediaRepositoryImpl.kt | 11 +++++++++-- .../hideout/core/service/media/MediaServiceImpl.kt | 3 ++- .../usbharu/hideout/core/service/media/RemoteMedia.kt | 3 ++- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteService.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteService.kt index 0fdee80b..204c503d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteService.kt @@ -133,7 +133,8 @@ class APNoteServiceImpl( RemoteMedia( (it.name ?: it.url)!!, it.url!!, - it.mediaType ?: "application/octet-stream" + it.mediaType ?: "application/octet-stream", + description = it.name ) ) } diff --git a/src/main/kotlin/dev/usbharu/hideout/application/config/HttpClientConfig.kt b/src/main/kotlin/dev/usbharu/hideout/application/config/HttpClientConfig.kt index 51222c8f..6000f0a3 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/HttpClientConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/HttpClientConfig.kt @@ -13,7 +13,7 @@ class HttpClientConfig { fun httpClient(): HttpClient = HttpClient(CIO).config { install(Logging) { logger = Logger.DEFAULT - level = LogLevel.INFO + level = LogLevel.ALL } install(HttpCache) { } 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 d9697059..be6b3839 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 @@ -11,5 +11,6 @@ data class Media( val thumbnailUrl: String?, val type: FileType, val mimeType: MimeType, - val blurHash: String? + val blurHash: String?, + val description: String? = null ) 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 7ef54c00..0206feb9 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 @@ -28,6 +28,8 @@ class MediaRepositoryImpl(private val idGenerateService: IdGenerateService) : Me it[thumbnailUrl] = media.thumbnailUrl it[type] = media.type.ordinal it[blurhash] = media.blurHash + it[mimeType] = media.mimeType.type + "/" + media.mimeType.subtype + it[description] = media.description } } else { Media.insert { @@ -38,6 +40,8 @@ class MediaRepositoryImpl(private val idGenerateService: IdGenerateService) : Me it[thumbnailUrl] = media.thumbnailUrl it[type] = media.type.ordinal it[blurhash] = media.blurHash + it[mimeType] = media.mimeType.type + "/" + media.mimeType.subtype + it[description] = media.description } } return media @@ -71,7 +75,8 @@ fun ResultRow.toMedia(): EntityMedia { thumbnailUrl = this[Media.thumbnailUrl], type = fileType, blurHash = this[Media.blurhash], - mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType) + mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType), + description = this[Media.description] ) } @@ -86,7 +91,8 @@ 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) + mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType), + description = this[Media.description] ) } @@ -99,5 +105,6 @@ object Media : Table("media") { val type = integer("type") val blurhash = varchar("blurhash", 255).nullable() val mimeType = varchar("mime_type", 255) + val description = varchar("description", 4000).nullable() override val primaryKey = PrimaryKey(id) } 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 c5731bb4..005ea34f 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 @@ -80,7 +80,8 @@ open class MediaServiceImpl( thumbnailUrl = save.thumbnailUrl, type = process.fileMimeType.fileType, mimeType = process.fileMimeType, - blurHash = blurHash + blurHash = blurHash, + description = mediaRequest.description ) ) } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/RemoteMedia.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/RemoteMedia.kt index 273487c3..66e5f349 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/media/RemoteMedia.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/RemoteMedia.kt @@ -3,5 +3,6 @@ package dev.usbharu.hideout.core.service.media data class RemoteMedia( val name: String, val url: String, - val mediaType: String + val mediaType: String, + val description: String? = null ) From a44d0c5b1fe3a8ccef3129fdb2e44e0aa2db8c7f Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 15 Nov 2023 20:25:41 +0900 Subject: [PATCH 6/9] =?UTF-8?q?test:=20sql=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ィア付き投稿はattachmentにDocumentとして画像が存在する.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql b/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql index 01352445..810c33d4 100644 --- a/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql +++ b/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql @@ -13,9 +13,9 @@ insert into POSTS (ID, "userId", OVERVIEW, TEXT, "createdAt", VISIBILITY, URL, " VALUES (1242, 11, null, 'test post', 12345680, 0, 'https://example.com/users/test-user11/posts/1242', null, null, false, 'https://example.com/users/test-user11/posts/1242'); -insert into MEDIA (ID, NAME, URL, REMOTE_URL, THUMBNAIL_URL, TYPE, BLURHASH, MIME_TYPE) -VALUES (1, 'test-media', 'https://example.com/media/test-media.png', null, null, 0, null, 'image/png'), - (2, 'test-media2', 'https://example.com/media/test-media2.png', null, null, 0, null, 'image/png'); +insert into MEDIA (ID, NAME, URL, REMOTE_URL, THUMBNAIL_URL, TYPE, BLURHASH, MIME_TYPE, DESCRIPTION) +VALUES (1, 'test-media', 'https://example.com/media/test-media.png', null, null, 0, null, 'image/png', null), + (2, 'test-media2', 'https://example.com/media/test-media2.png', null, null, 0, null, 'image/png', null); insert into POSTSMEDIA(POST_ID, MEDIA_ID) VALUES (1242, 1), From 79b6e650058c6e0ef54e20ad91f5c8c03b8c8f35 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 15 Nov 2023 20:50:03 +0900 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20=E3=83=A1=E3=83=87=E3=82=A3?= =?UTF-8?q?=E3=82=A2=E3=81=AE=E3=83=AA=E3=83=8D=E3=83=BC=E3=83=A0=E3=82=92?= =?UTF-8?q?=E5=88=A5=E3=82=AF=E3=83=A9=E3=82=B9=E3=81=AB=E5=88=87=E3=82=8A?= =?UTF-8?q?=E5=87=BA=E3=81=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/media/MediaFileRenameService.kt | 14 ++++++++++++++ .../core/service/media/MediaServiceImpl.kt | 17 ++++++++++++++--- .../service/media/UUIDMediaFileRenameService.kt | 16 ++++++++++++++++ .../converter/image/ImageMediaProcessService.kt | 6 ++++++ 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaFileRenameService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/service/media/UUIDMediaFileRenameService.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaFileRenameService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaFileRenameService.kt new file mode 100644 index 00000000..ea0ec53d --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaFileRenameService.kt @@ -0,0 +1,14 @@ +package dev.usbharu.hideout.core.service.media + +interface MediaFileRenameService { + /** + * メディアをリネームします + * + * @param uploadName アップロードされた時点でのファイル名 + * @param uploadMimeType アップロードされた時点でのMimeType + * @param processedName 処理後のファイル名 + * @param processedMimeType 処理後のMimeType + * @return リネーム後のファイル名 + */ + fun rename(uploadName: String, uploadMimeType: MimeType, processedName: String, processedMimeType: MimeType): String +} 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 005ea34f..a28a5fa3 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 @@ -21,7 +21,8 @@ open class MediaServiceImpl( private val mediaBlurhashService: MediaBlurhashService, private val mediaRepository: MediaRepository, private val mediaProcessServices: List, - private val remoteMediaDownloadService: RemoteMediaDownloadService + private val remoteMediaDownloadService: RemoteMediaDownloadService, + private val renameService: MediaFileRenameService ) : MediaService { override suspend fun uploadLocalMedia(mediaRequest: MediaRequest): EntityMedia { val fileName = mediaRequest.file.name @@ -50,7 +51,12 @@ open class MediaServiceImpl( ) val dataMediaSave = MediaSaveRequest( - process.filePath.fileName.toString(), + renameService.rename( + mediaRequest.file.name, + mimeType, + process.filePath.fileName.toString(), + process.fileMimeType + ), "", process.filePath, process.thumbnailPath @@ -101,7 +107,12 @@ open class MediaServiceImpl( val process = findMediaProcessor(mimeType).process(mimeType, remoteMedia.name, it.path, null) val mediaSaveRequest = MediaSaveRequest( - process.filePath.fileName.toString(), + renameService.rename( + remoteMedia.name, + mimeType, + process.filePath.fileName.toString(), + process.fileMimeType + ), "", process.filePath, process.thumbnailPath diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/UUIDMediaFileRenameService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/UUIDMediaFileRenameService.kt new file mode 100644 index 00000000..d891cb93 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/UUIDMediaFileRenameService.kt @@ -0,0 +1,16 @@ +package dev.usbharu.hideout.core.service.media + +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Service +import java.util.* + +@Qualifier("uuid") +@Service +class UUIDMediaFileRenameService : MediaFileRenameService { + override fun rename( + uploadName: String, + uploadMimeType: MimeType, + processedName: String, + processedMimeType: MimeType + ): String = "${UUID.randomUUID()}.${uploadMimeType.subtype}.${processedMimeType.subtype}" +} 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 index 0efa6c83..1401b752 100644 --- 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 @@ -10,6 +10,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.slf4j.MDCContext import kotlinx.coroutines.withContext import net.coobird.thumbnailator.Thumbnails +import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Qualifier import org.springframework.stereotype.Service import java.nio.file.Files @@ -83,6 +84,7 @@ class ImageMediaProcessService(private val imageMediaProcessorConfiguration: Ima tempFile.outputStream().use { if (ImageIO.write(bufferedImage, convertType, it).not()) { + logger.warn("Failed to save a temporary file. type: {} ,path: {}", convertType, tempFile) throw MediaProcessException("Failed to save a temporary file.") } } @@ -93,4 +95,8 @@ class ImageMediaProcessService(private val imageMediaProcessorConfiguration: Ima MimeType("image", convertType, FileType.Image).takeIf { genThumbnail } ) } + + companion object { + private val logger = LoggerFactory.getLogger(ImageMediaProcessService::class.java) + } } From f206ee312dd8180c7d5886b174fb03e641c1fcb6 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:36:45 +0900 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20=E5=8B=95=E7=94=BB=E3=81=AE?= =?UTF-8?q?=E3=82=A2=E3=83=83=E3=83=97=E3=83=AD=E3=83=BC=E3=83=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 1 + .../converter/MediaProcessServiceImpl.kt | 2 +- .../movie/MovieMediaProcessService.kt | 132 ++++++++++++++++++ src/main/resources/application.yml | 8 +- 4 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/movie/MovieMediaProcessService.kt diff --git a/build.gradle.kts b/build.gradle.kts index 472a8f08..b0347c0b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -181,6 +181,7 @@ dependencies { 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("org.bytedeco:javacv-platform:1.5.9") implementation("io.ktor:ktor-client-logging-jvm:$ktor_version") 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 0a1080c4..4fe892f6 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 @@ -13,7 +13,7 @@ class MediaProcessServiceImpl( private val thumbnailGenerateService: ThumbnailGenerateService ) : MediaProcessService { override fun isSupport(mimeType: MimeType): Boolean { - TODO("Not yet implemented") + return false } override suspend fun process( diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/movie/MovieMediaProcessService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/movie/MovieMediaProcessService.kt new file mode 100644 index 00000000..416fa698 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/movie/MovieMediaProcessService.kt @@ -0,0 +1,132 @@ +package dev.usbharu.hideout.core.service.media.converter.movie + +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 org.bytedeco.ffmpeg.global.avcodec +import org.bytedeco.javacv.FFmpegFrameFilter +import org.bytedeco.javacv.FFmpegFrameGrabber +import org.bytedeco.javacv.FFmpegFrameRecorder +import org.bytedeco.javacv.Java2DFrameConverter +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Service +import java.awt.image.BufferedImage +import java.nio.file.Files +import java.nio.file.Path +import javax.imageio.ImageIO +import kotlin.math.min + +@Service +@Qualifier("video") +class MovieMediaProcessService : MediaProcessService { + override fun isSupport(mimeType: MimeType): Boolean { + return mimeType.type == "video" + } + + 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 { + val tempFile = Files.createTempFile("hideout-movie-processor-", ".tmp") + val thumbnailFile = Files.createTempFile("hideout-movie-thumbnail-generate-", ".tmp") + logger.info("START Convert Movie Media {}", fileName) + FFmpegFrameGrabber(filePath.toFile()).use { grabber -> + grabber.start() + val width = grabber.imageWidth + val height = grabber.imageHeight + val frameRate = 60.0 + + logger.debug("Movie Media Width {}, Height {}", width, height) + + FFmpegFrameFilter( + "fps=fps=${frameRate.toInt()}", + "anull", + width, + height, + grabber.audioChannels + ).use { filter -> + + filter.sampleFormat = grabber.sampleFormat + filter.sampleRate = grabber.sampleRate + filter.pixelFormat = grabber.pixelFormat + filter.frameRate = grabber.frameRate + filter.start() + + val videoBitRate = min(1300000, (width * height * frameRate * 1 * 0.07).toInt()) + + logger.debug("Movie Media BitRate {}", videoBitRate) + + FFmpegFrameRecorder(tempFile.toFile(), width, height, grabber.audioChannels).use { + it.sampleRate = grabber.sampleRate + it.format = "mp4" + it.videoCodec = avcodec.AV_CODEC_ID_H264 + it.audioCodec = avcodec.AV_CODEC_ID_AAC + it.audioChannels = grabber.audioChannels + it.videoQuality = 1.0 + it.frameRate = frameRate + it.setVideoOption("preset", "ultrafast") + it.timestamp = 0 + it.gopSize = frameRate.toInt() + it.videoBitrate = videoBitRate + it.start() + + var bufferedImage: BufferedImage? = null + + val frameConverter = Java2DFrameConverter() + + while (true) { + val grab = grabber.grab() ?: break + + + if (bufferedImage == null) { + bufferedImage = frameConverter.convert(grab) + } + + if (grab.image != null || grab.samples != null) { + filter.push(grab) + } + while (true) { + val frame = filter.pull() ?: break + it.record(frame) + } + } + + + + if (bufferedImage != null) { + ImageIO.write(bufferedImage, "jpeg", thumbnailFile.toFile()) + } + } + } + + } + + logger.info("SUCCESS Convert Movie Media {}", fileName) + + return ProcessedMediaPath( + tempFile, + thumbnailFile, + MimeType("video", "mp4", FileType.Video), + MimeType("image", "jpeg", FileType.Image) + ) + } + + companion object { + private val logger = LoggerFactory.getLogger(MovieMediaProcessService::class.java) + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b792d06d..1aef91a7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -30,7 +30,10 @@ spring: database: hideout # username: hideoutuser # password: hideoutpass - + servlet: + multipart: + max-file-size: 40MB + max-request-size: 40MB h2: console: enabled: true @@ -42,5 +45,6 @@ server: basedir: tomcat accesslog: enabled: true - + max-http-form-post-size: 40MB + max-swallow-size: 40MB port: 8081 From c67b1ec1e97a505ce6ebd54dd466c119f4c6ade5 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 16 Nov 2023 20:59:22 +0900 Subject: [PATCH 9/9] fix: lint --- .../usbharu/hideout/activitypub/domain/model/Delete.kt | 4 +--- .../usbharu/hideout/activitypub/domain/model/JsonLd.kt | 8 ++------ .../hideout/activitypub/domain/model/objects/Object.kt | 4 +--- .../interfaces/api/actor/UserAPControllerImpl.kt | 2 +- .../activity/delete/APReceiveDeleteServiceImpl.kt | 2 +- .../activitypub/service/objects/note/APNoteService.kt | 3 --- .../exposedrepository/PostRepositoryImpl.kt | 1 - .../hideout/core/service/media/MediaServiceImpl.kt | 9 +++------ .../service/media/converter/MediaProcessServiceImpl.kt | 4 +--- .../media/converter/image/ImageMediaProcessService.kt | 4 +++- .../media/converter/movie/MovieMediaProcessService.kt | 8 +------- .../hideout/core/service/reaction/ReactionServiceImpl.kt | 2 +- .../service/objects/note/APNoteServiceImplTest.kt | 6 ------ 13 files changed, 15 insertions(+), 42 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Delete.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Delete.kt index 07223628..0305fff2 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Delete.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Delete.kt @@ -41,7 +41,5 @@ open class Delete : Object { return result } - override fun toString(): String { - return "Delete(`object`=$`object`, published=$published) ${super.toString()}" - } + override fun toString(): String = "Delete(`object`=$`object`, published=$published) ${super.toString()}" } diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/JsonLd.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/JsonLd.kt index 19b88de3..864332f4 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/JsonLd.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/JsonLd.kt @@ -62,13 +62,9 @@ class ContextDeserializer : JsonDeserializer() { class ContextSerializer : JsonSerializer>() { - override fun isEmpty(value: List?): Boolean { - return value.isNullOrEmpty() - } + override fun isEmpty(value: List?): Boolean = value.isNullOrEmpty() - override fun isEmpty(provider: SerializerProvider?, value: List?): Boolean { - return value.isNullOrEmpty() - } + override fun isEmpty(provider: SerializerProvider?, value: List?): Boolean = value.isNullOrEmpty() override fun serialize(value: List?, gen: JsonGenerator?, serializers: SerializerProvider) { if (value.isNullOrEmpty()) { diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/Object.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/Object.kt index 62c32928..23f26eac 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/Object.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/Object.kt @@ -46,9 +46,7 @@ open class Object : JsonLd { return result } - override fun toString(): String { - return "Object(type=$type, name=$name, actor=$actor, id=$id) ${super.toString()}" - } + override fun toString(): String = "Object(type=$type, name=$name, actor=$actor, id=$id) ${super.toString()}" companion object { @JvmStatic diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/actor/UserAPControllerImpl.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/actor/UserAPControllerImpl.kt index 74ede688..ea8ad508 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/actor/UserAPControllerImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/actor/UserAPControllerImpl.kt @@ -12,7 +12,7 @@ class UserAPControllerImpl(private val apUserService: APUserService) : UserAPCon override suspend fun userAp(username: String): ResponseEntity { val person = try { apUserService.getPersonByName(username) - } catch (e: FailedToGetResourcesException) { + } catch (_: FailedToGetResourcesException) { return ResponseEntity.notFound().build() } person.context += listOf("https://www.w3.org/ns/activitystreams") diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/delete/APReceiveDeleteServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/delete/APReceiveDeleteServiceImpl.kt index d5272203..c00aeda6 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/delete/APReceiveDeleteServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/delete/APReceiveDeleteServiceImpl.kt @@ -22,7 +22,7 @@ class APReceiveDeleteServiceImpl( val post = try { postQueryService.findByApId(deleteId) - } catch (e: FailedToGetResourcesException) { + } catch (_: FailedToGetResourcesException) { return@transaction ActivityPubStringResponse(HttpStatusCode.OK, "Resource not found or already deleted") } postRepository.delete(post.id) diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteService.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteService.kt index 204c503d..f7300314 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteService.kt @@ -1,6 +1,5 @@ package dev.usbharu.hideout.activitypub.service.objects.note -import com.fasterxml.jackson.databind.ObjectMapper import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException import dev.usbharu.hideout.activitypub.domain.exception.IllegalActivityPubObjectException import dev.usbharu.hideout.activitypub.domain.model.Note @@ -24,7 +23,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.slf4j.MDCContext import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Qualifier import org.springframework.cache.annotation.Cacheable import org.springframework.stereotype.Service import java.time.Instant @@ -50,7 +48,6 @@ class APNoteServiceImpl( private val postRepository: PostRepository, private val apUserService: APUserService, private val postQueryService: PostQueryService, - @Qualifier("activitypub") private val objectMapper: ObjectMapper, private val postService: PostService, private val apResourceResolveService: APResourceResolveService, private val postBuilder: Post.PostBuilder, diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/PostRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/PostRepositoryImpl.kt index c219f52c..16322ce0 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/PostRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/PostRepositoryImpl.kt @@ -73,7 +73,6 @@ class PostRepositoryImpl( .let(postQueryMapper::map) .singleOr { FailedToGetResourcesException("id: $id was not found.", it) } - override suspend fun delete(id: Long) { Posts.deleteWhere { Posts.id eq id } } 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 a28a5fa3..fbf99674 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 @@ -24,6 +24,7 @@ open class MediaServiceImpl( private val remoteMediaDownloadService: RemoteMediaDownloadService, private val renameService: MediaFileRenameService ) : MediaService { + @Suppress("LongMethod") override suspend fun uploadLocalMedia(mediaRequest: MediaRequest): EntityMedia { val fileName = mediaRequest.file.name logger.info( @@ -33,8 +34,6 @@ open class MediaServiceImpl( val tempFile = Files.createTempFile("hideout-tmp-file", ".tmp") - - tempFile.withDelete().use { Files.newOutputStream(tempFile).use { outputStream -> mediaRequest.file.inputStream.use { @@ -93,8 +92,6 @@ open class MediaServiceImpl( } } } - - } // TODO: 仮の処理として保存したように動かす @@ -156,11 +153,11 @@ open class MediaServiceImpl( return mediaProcessServices.first { try { it.isSupport(mimeType) - } catch (e: Exception) { + } catch (_: Exception) { false } } - } catch (e: NoSuchElementException) { + } catch (_: NoSuchElementException) { throw UnsupportedMediaException("MediaType: $mimeType isn't supported.") } } 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 4fe892f6..7f62d148 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 @@ -12,9 +12,7 @@ class MediaProcessServiceImpl( private val mediaConverterRoot: MediaConverterRoot, private val thumbnailGenerateService: ThumbnailGenerateService ) : MediaProcessService { - override fun isSupport(mimeType: MimeType): Boolean { - return false - } + override fun isSupport(mimeType: MimeType): Boolean = false override suspend fun process( fileType: FileType, 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 index 1401b752..2893fa4c 100644 --- 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 @@ -70,7 +70,9 @@ class ImageMediaProcessService(private val imageMediaProcessorConfiguration: Ima Thumbnails.of(thumbnails.toFile()).size(width, height).asBufferedImage() } else { Thumbnails.of(bufferedImage).size(width, height).asBufferedImage() - }, convertType, it + }, + convertType, + it ) if (write) { tempThumbnailFile diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/movie/MovieMediaProcessService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/movie/MovieMediaProcessService.kt index 416fa698..9b39e854 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/movie/MovieMediaProcessService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/converter/movie/MovieMediaProcessService.kt @@ -22,9 +22,7 @@ import kotlin.math.min @Service @Qualifier("video") class MovieMediaProcessService : MediaProcessService { - override fun isSupport(mimeType: MimeType): Boolean { - return mimeType.type == "video" - } + override fun isSupport(mimeType: MimeType): Boolean = mimeType.type == "video" override suspend fun process( fileType: FileType, @@ -92,7 +90,6 @@ class MovieMediaProcessService : MediaProcessService { while (true) { val grab = grabber.grab() ?: break - if (bufferedImage == null) { bufferedImage = frameConverter.convert(grab) } @@ -106,14 +103,11 @@ class MovieMediaProcessService : MediaProcessService { } } - - if (bufferedImage != null) { ImageIO.write(bufferedImage, "jpeg", thumbnailFile.toFile()) } } } - } logger.info("SUCCESS Convert Movie Media {}", fileName) diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/reaction/ReactionServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/reaction/ReactionServiceImpl.kt index baad0e56..056e5249 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/reaction/ReactionServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/reaction/ReactionServiceImpl.kt @@ -45,7 +45,7 @@ class ReactionServiceImpl( reactionQueryService.findByPostIdAndUserIdAndEmojiId(postId, userId, 0) reactionRepository.delete(findByPostIdAndUserIdAndEmojiId) apReactionService.removeReaction(findByPostIdAndUserIdAndEmojiId) - } catch (e: FailedToGetResourcesException) { + } catch (_: FailedToGetResourcesException) { } } diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt index c7e047ed..540f642c 100644 --- a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt @@ -37,7 +37,6 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.mockito.kotlin.* -import utils.JsonObjectMapper.objectMapper import utils.PostBuilder import utils.UserBuilder import java.time.Instant @@ -74,7 +73,6 @@ class APNoteServiceImplTest { postRepository = mock(), apUserService = mock(), postQueryService = mock(), - objectMapper = objectMapper, postService = mock(), apResourceResolveService = mock(), postBuilder = Post.PostBuilder(CharacterLimit()), @@ -152,7 +150,6 @@ class APNoteServiceImplTest { postRepository = postRepository, apUserService = apUserService, postQueryService = postQueryService, - objectMapper = objectMapper, postService = mock(), apResourceResolveService = apResourceResolveService, postBuilder = Post.PostBuilder(CharacterLimit()), @@ -221,7 +218,6 @@ class APNoteServiceImplTest { postRepository = mock(), apUserService = mock(), postQueryService = postQueryService, - objectMapper = objectMapper, postService = mock(), apResourceResolveService = apResourceResolveService, postBuilder = Post.PostBuilder(CharacterLimit()), @@ -274,7 +270,6 @@ class APNoteServiceImplTest { postRepository = postRepository, apUserService = apUserService, postQueryService = mock(), - objectMapper = objectMapper, postService = postService, apResourceResolveService = mock(), postBuilder = postBuilder, @@ -333,7 +328,6 @@ class APNoteServiceImplTest { postRepository = mock(), apUserService = mock(), postQueryService = mock(), - objectMapper = objectMapper, postService = mock(), apResourceResolveService = mock(), postBuilder = postBuilder,