diff --git a/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonMediaApiController.kt b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonMediaApiController.kt index e1951898..f2d19aab 100644 --- a/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonMediaApiController.kt +++ b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonMediaApiController.kt @@ -2,18 +2,29 @@ package dev.usbharu.hideout.controller.mastodon import dev.usbharu.hideout.controller.mastodon.generated.MediaApi import dev.usbharu.hideout.domain.mastodon.model.generated.MediaAttachment +import dev.usbharu.hideout.domain.model.hideout.form.Media +import dev.usbharu.hideout.service.api.mastodon.MediaApiService import org.springframework.http.ResponseEntity import org.springframework.stereotype.Controller import org.springframework.web.multipart.MultipartFile @Controller -class MastodonMediaApiController : MediaApi { +class MastodonMediaApiController(private val mediaApiService: MediaApiService) : MediaApi { override fun apiV1MediaPost( file: MultipartFile, thumbnail: MultipartFile?, description: String?, focus: String? ): ResponseEntity { - + return ResponseEntity.ok( + mediaApiService.postMedia( + Media( + file, + thumbnail, + description, + focus + ) + ) + ) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/MediaSave.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/MediaSave.kt new file mode 100644 index 00000000..f4086904 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/MediaSave.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.domain.model + +import java.io.InputStream + +data class MediaSave( + val name: String, + val prefix: String, + val fileInputStream: InputStream, + val thumbnailInputStream: InputStream +) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Media.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Media.kt new file mode 100644 index 00000000..6f5f0e78 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Media.kt @@ -0,0 +1,13 @@ +package dev.usbharu.hideout.domain.model.hideout.entity + +import dev.usbharu.hideout.service.media.FileTypeDeterminationService + +data class Media( + val id: Long, + val name: String, + val url: String, + val remoteUrl: String?, + val thumbnailUrl: String?, + val type: FileTypeDeterminationService.FileType, + val blurHash: String? +) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/Media.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/Media.kt new file mode 100644 index 00000000..978eaa56 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/Media.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.domain.model.hideout.form + +import org.springframework.web.multipart.MultipartFile + +data class Media( + val file: MultipartFile, + val thumbnail: MultipartFile?, + val description: String?, + val focus: String? +) diff --git a/src/main/kotlin/dev/usbharu/hideout/exception/media/MediaException.kt b/src/main/kotlin/dev/usbharu/hideout/exception/media/MediaException.kt new file mode 100644 index 00000000..922a42a8 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/exception/media/MediaException.kt @@ -0,0 +1,14 @@ +package dev.usbharu.hideout.exception.media + +abstract class MediaException : RuntimeException { + 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/exception/media/MediaUploadException.kt b/src/main/kotlin/dev/usbharu/hideout/exception/media/MediaUploadException.kt new file mode 100644 index 00000000..36afc8b7 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/exception/media/MediaUploadException.kt @@ -0,0 +1,14 @@ +package dev.usbharu.hideout.exception.media + +open class MediaUploadException : 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/repository/MediaRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/MediaRepository.kt new file mode 100644 index 00000000..be55bb86 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/repository/MediaRepository.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.repository + +import dev.usbharu.hideout.domain.model.hideout.entity.Media + +interface MediaRepository { + suspend fun generateId(): Long + suspend fun save(media: Media): Media + suspend fun findById(id: Long): Media + suspend fun delete(id: Long) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/MediaRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/MediaRepositoryImpl.kt new file mode 100644 index 00000000..ba5724d0 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/repository/MediaRepositoryImpl.kt @@ -0,0 +1,80 @@ +package dev.usbharu.hideout.repository + + +import dev.usbharu.hideout.exception.FailedToGetResourcesException +import dev.usbharu.hideout.service.core.IdGenerateService +import dev.usbharu.hideout.service.media.FileTypeDeterminationService +import dev.usbharu.hideout.util.singleOr +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.springframework.stereotype.Repository +import dev.usbharu.hideout.domain.model.hideout.entity.Media as EntityMedia + +@Repository +class MediaRepositoryImpl(private val idGenerateService: IdGenerateService) : MediaRepository { + override suspend fun generateId(): Long = idGenerateService.generateId() + + override suspend fun save(media: EntityMedia): EntityMedia { + if (Media.select { + Media.id eq media.id + }.singleOrNull() != null) { + Media.update({ Media.id eq media.id }) { + it[Media.name] = media.name + it[Media.url] = media.url + it[Media.remoteUrl] = media.remoteUrl + it[Media.thumbnailUrl] = media.thumbnailUrl + it[Media.type] = media.type.ordinal + it[Media.blurhash] = media.blurHash + } + } else { + Media.insert { + it[Media.id] = media.id + it[Media.name] = media.name + it[Media.url] = media.url + it[Media.remoteUrl] = media.remoteUrl + it[Media.thumbnailUrl] = media.thumbnailUrl + it[Media.type] = media.type.ordinal + it[Media.blurhash] = media.blurHash + } + } + return media + } + + override suspend fun findById(id: Long): EntityMedia { + return Media + .select { + Media.id eq id + } + .singleOr { + FailedToGetResourcesException("id: $id was not found.") + }.toMedia() + } + + override suspend fun delete(id: Long) { + Media.deleteWhere { + Media.id eq id + } + } + + fun ResultRow.toMedia(): EntityMedia { + return EntityMedia( + this[Media.id], + this[Media.name], + this[Media.url], + this[Media.remoteUrl], + this[Media.thumbnailUrl], + FileTypeDeterminationService.FileType.values().first { it.ordinal == this[Media.type] }, + this[Media.blurhash], + ) + } +} + +object Media : Table("media") { + val id = long("id") + val name = varchar("name", 255) + val url = varchar("url", 255) + val remoteUrl = varchar("remote_url", 255).nullable() + val thumbnailUrl = varchar("thumbnail_url", 255).nullable() + val type = integer("type") + val blurhash = varchar("blurhash", 255).nullable() +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/MediaApiService.kt b/src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/MediaApiService.kt new file mode 100644 index 00000000..8b1da1b9 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/MediaApiService.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.service.api.mastodon + +import dev.usbharu.hideout.domain.mastodon.model.generated.MediaAttachment +import dev.usbharu.hideout.domain.model.hideout.form.Media +import org.springframework.stereotype.Service + +@Service +interface MediaApiService { + suspend fun postMedia(media: Media): MediaAttachment +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/MediaApiServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/MediaApiServiceImpl.kt new file mode 100644 index 00000000..230e749e --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/MediaApiServiceImpl.kt @@ -0,0 +1,11 @@ +package dev.usbharu.hideout.service.api.mastodon + +import dev.usbharu.hideout.domain.mastodon.model.generated.MediaAttachment +import dev.usbharu.hideout.domain.model.hideout.form.Media +import dev.usbharu.hideout.service.media.MediaService + +class MediaApiServiceImpl(private val mediaService: MediaService) : MediaApiService { + override suspend fun postMedia(media: Media): MediaAttachment { + mediaService.uploadLocalMedia(media) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/FileTypeDeterminationService.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/FileTypeDeterminationService.kt new file mode 100644 index 00000000..c30762de --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/FileTypeDeterminationService.kt @@ -0,0 +1,12 @@ +package dev.usbharu.hideout.service.media + +interface FileTypeDeterminationService { + fun fileType(byteArray: ByteArray, filename: String, contentType: String?): FileType + + enum class FileType { + Image, + Video, + Audio, + Unknown + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/FileTypeDeterminationServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/FileTypeDeterminationServiceImpl.kt new file mode 100644 index 00000000..5ab2df2e --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/FileTypeDeterminationServiceImpl.kt @@ -0,0 +1,26 @@ +package dev.usbharu.hideout.service.media + +import org.springframework.stereotype.Component + +@Component +class FileTypeDeterminationServiceImpl : FileTypeDeterminationService { + override fun fileType( + byteArray: ByteArray, + filename: String, + contentType: String? + ): FileTypeDeterminationService.FileType { + if (contentType == null) { + return FileTypeDeterminationService.FileType.Unknown + } + if (contentType.startsWith("image")) { + return FileTypeDeterminationService.FileType.Image + } + if (contentType.startsWith("video")) { + return FileTypeDeterminationService.FileType.Video + } + if (contentType.startsWith("audio")) { + return FileTypeDeterminationService.FileType.Audio + } + return FileTypeDeterminationService.FileType.Unknown + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/MediaBlurhashService.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/MediaBlurhashService.kt new file mode 100644 index 00000000..490a2ffe --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/MediaBlurhashService.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.service.media + +import java.awt.image.BufferedImage + +interface MediaBlurhashService { + fun generateBlurhash(bufferedImage: BufferedImage): String +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/MediaDataStore.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/MediaDataStore.kt new file mode 100644 index 00000000..bb452f0d --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/MediaDataStore.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.service.media + +import dev.usbharu.hideout.domain.model.MediaSave + +interface MediaDataStore { + suspend fun save(dataMediaSave: MediaSave) + suspend fun delete(id: Long) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/MediaService.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/MediaService.kt new file mode 100644 index 00000000..47446a3b --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/MediaService.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.service.media + +import dev.usbharu.hideout.domain.model.hideout.form.Media + +interface MediaService { + suspend fun uploadLocalMedia(media: Media): SavedMedia +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/MediaServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/MediaServiceImpl.kt new file mode 100644 index 00000000..799e0dc4 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/MediaServiceImpl.kt @@ -0,0 +1,56 @@ +package dev.usbharu.hideout.service.media + +import dev.usbharu.hideout.domain.model.MediaSave +import dev.usbharu.hideout.domain.model.hideout.form.Media +import dev.usbharu.hideout.exception.media.MediaException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.springframework.stereotype.Service +import javax.imageio.ImageIO + +@Service +class MediaServiceImpl( + private val mediaDataStore: MediaDataStore, + private val fileTypeDeterminationService: FileTypeDeterminationService, + private val mediaBlurhashService: MediaBlurhashService +) : MediaService { + override suspend fun uploadLocalMedia(media: Media): SavedMedia { + if (media.file.size == 0L) { + return FaildSavedMedia( + "File size is 0.", + "Cannot upload a file with a file size of 0." + ) + } + + val fileType = fileTypeDeterminationService.fileType(media.file.bytes, media.file.name, media.file.contentType) + if (fileType != FileTypeDeterminationService.FileType.Image) { + return FaildSavedMedia("Unsupported file type.", "FileType: $fileType is not supported.") + } + + try { + mediaDataStore.save( + MediaSave( + media.file.name, + "", + media.file.inputStream, + media.thumbnail.inputStream + ) + ) + } catch (e: MediaException) { + return FaildSavedMedia( + "Faild to upload.", + e.localizedMessage, + e + ) + } + + val withContext = withContext(Dispatchers.IO) { + mediaBlurhashService.generateBlurhash(ImageIO.read(media.file.inputStream)) + } + + return SuccessSavedMedia( + media.file.name, "", "", + withContext + ) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/SavedMedia.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/SavedMedia.kt new file mode 100644 index 00000000..ffd94ef8 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/SavedMedia.kt @@ -0,0 +1,18 @@ +package dev.usbharu.hideout.service.media + +sealed class SavedMedia(val success: Boolean) + +class SuccessSavedMedia( + val name: String, + val url: String, + val thumbnailUrl: String, + val blurhash: String +) : + SavedMedia(true) + + +class FaildSavedMedia( + val reason: String, + val description: String, + val trace: Throwable? = null +) : SavedMedia(false)