feat: リモートの画像のOOM対策を追加

This commit is contained in:
usbharu 2023-11-15 14:36:41 +09:00
parent 5f50f4fb28
commit 58b0931a49
4 changed files with 88 additions and 66 deletions

View File

@ -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.core.service.media.converter.MediaProcessService
import dev.usbharu.hideout.mastodon.interfaces.api.media.MediaRequest import dev.usbharu.hideout.mastodon.interfaces.api.media.MediaRequest
import dev.usbharu.hideout.util.withDelete 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.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.nio.file.Files import java.nio.file.Files
import java.util.*
import javax.imageio.ImageIO import javax.imageio.ImageIO
import dev.usbharu.hideout.core.domain.model.media.Media as EntityMedia import dev.usbharu.hideout.core.domain.model.media.Media as EntityMedia
@Service @Service
@Suppress("TooGenericExceptionCaught") @Suppress("TooGenericExceptionCaught")
class MediaServiceImpl( open class MediaServiceImpl(
private val mediaDataStore: MediaDataStore, private val mediaDataStore: MediaDataStore,
private val fileTypeDeterminationService: FileTypeDeterminationService, private val fileTypeDeterminationService: FileTypeDeterminationService,
private val mediaBlurhashService: MediaBlurhashService, private val mediaBlurhashService: MediaBlurhashService,
private val mediaRepository: MediaRepository, private val mediaRepository: MediaRepository,
private val mediaProcessServices: List<MediaProcessService>, private val mediaProcessServices: List<MediaProcessService>,
private val httpClient: HttpClient private val remoteMediaDownloadService: RemoteMediaDownloadService
) : MediaService { ) : MediaService {
override suspend fun uploadLocalMedia(mediaRequest: MediaRequest): EntityMedia { override suspend fun uploadLocalMedia(mediaRequest: MediaRequest): EntityMedia {
val fileName = mediaRequest.file.name val fileName = mediaRequest.file.name
@ -102,51 +94,34 @@ class MediaServiceImpl(
override suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia): Media { override suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia): Media {
logger.info("MEDIA Remote media. filename:${remoteMedia.name} url:${remoteMedia.url}") logger.info("MEDIA Remote media. filename:${remoteMedia.name} url:${remoteMedia.url}")
val httpResponse = httpClient.get(remoteMedia.url) remoteMediaDownloadService.download(remoteMedia.url).withDelete().use {
val bytes = httpResponse.bodyAsChannel().toByteArray() val mimeType = fileTypeDeterminationService.fileType(it.path, remoteMedia.name)
val contentType = httpResponse.contentType()?.toString() val process = findMediaProcessor(mimeType).process(mimeType, remoteMedia.name, it.path, null)
val mimeType =
fileTypeDeterminationService.fileType(bytes, remoteMedia.name, contentType)
if (mimeType.fileType != FileType.Image) { val mediaSaveRequest = MediaSaveRequest(
throw UnsupportedMediaException("FileType: $mimeType isn't supported.") process.filePath.fileName.toString(),
}
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, process.filePath,
processedMedia.thumbnail?.byteArray process.thumbnailPath
) )
mediaSaveRequest.filePath.withDelete().use {
mediaSaveRequest.filePath.withDelete().use {
val save = try { val save = try {
mediaDataStore.save(mediaSave) mediaDataStore.save(mediaSaveRequest)
} catch (e: Exception) { } catch (e: Exception) {
logger.warn("Failed save media", e) logger.warn("Failed to save the media", e)
throw MediaSaveException("Failed save media.", e) throw MediaSaveException("Failed to save the media.", e)
} }
if (save.success.not()) { if (save is FaildSavedMedia) {
save as FaildSavedMedia
logger.warn("Failed to save the media. reason: ${save.reason}") logger.warn("Failed to save the media. reason: ${save.reason}")
logger.warn(save.description, save.trace) logger.warn(save.description, save.trace)
throw MediaSaveException("Failed to save the media.") throw MediaSaveException("Failed to save the media.")
} }
save as SuccessSavedMedia save as SuccessSavedMedia
val blurhash = generateBlurhash(process)
val blurhash = withContext(Dispatchers.IO) {
mediaBlurhashService.generateBlurhash(ImageIO.read(bytes.inputStream()))
}
return mediaRepository.save( return mediaRepository.save(
EntityMedia( EntityMedia(
id = mediaRepository.generateId(), id = mediaRepository.generateId(),
@ -154,12 +129,15 @@ class MediaServiceImpl(
url = save.url, url = save.url,
remoteUrl = remoteMedia.url, remoteUrl = remoteMedia.url,
thumbnailUrl = save.thumbnailUrl, thumbnailUrl = save.thumbnailUrl,
type = mimeType.fileType, type = process.fileMimeType.fileType,
mimeType = mimeType, mimeType = process.fileMimeType,
blurHash = blurhash blurHash = blurhash
) )
) )
} }
}
}
}
protected fun findMediaProcessor(mimeType: MimeType): MediaProcessService { protected fun findMediaProcessor(mimeType: MimeType): MediaProcessService {
try { try {

View File

@ -0,0 +1,7 @@
package dev.usbharu.hideout.core.service.media
import java.nio.file.Path
interface RemoteMediaDownloadService {
suspend fun download(url: String): Path
}

View File

@ -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)
}
}

View File

@ -3,9 +3,9 @@ package dev.usbharu.hideout.util
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
fun Path?.withDelete(): TempFile = TempFile(this) fun <T : Path?> T.withDelete(): TempFile<T> = TempFile(this)
class TempFile(val path: Path?) : AutoCloseable { class TempFile<T : Path?>(val path: T) : AutoCloseable {
override fun close() { override fun close() {
path?.let { Files.deleteIfExists(it) } path?.let { Files.deleteIfExists(it) }
} }