mirror of https://github.com/usbharu/Hideout.git
refactor: メディア関係をリファクタリング
This commit is contained in:
parent
19de2d544d
commit
cdf0f23332
|
@ -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")
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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?
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
)
|
|
@ -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<MediaProcessService>,
|
||||
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
|
||||
)
|
||||
)
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
package dev.usbharu.hideout.core.service.media
|
||||
|
||||
data class MimeType(val type: String, val subtype: String, val fileType: FileType)
|
|
@ -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?
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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<String>?,
|
||||
|
||||
)
|
||||
|
||||
data class ImageMediaProcessorThumbnailConfiguration(
|
||||
val generate: Boolean,
|
||||
val width: Int,
|
||||
val height: Int
|
||||
)
|
Loading…
Reference in New Issue