-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(image): add image task abstraction and executor
- create ImageTask subclasses for thumbnail and image operations - implement ImageExecutor for concurrent task execution
- Loading branch information
Showing
5 changed files
with
257 additions
and
111 deletions.
There are no files selected for viewing
14 changes: 14 additions & 0 deletions
14
...rc/main/kotlin/io/sakurasou/exception/service/image/io/ImageThumbnailNotFoundException.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package io.sakurasou.exception.service.image.io | ||
|
||
import io.sakurasou.exception.ServiceThrowable | ||
|
||
/** | ||
* @author Shiina Kin | ||
* 2024/9/12 12:57 | ||
*/ | ||
class ImageThumbnailNotFoundException : ServiceThrowable() { | ||
override val code: Int | ||
get() = 4004 | ||
override val message: String | ||
get() = "Image thumbnail not found, will be generated later" | ||
} |
62 changes: 62 additions & 0 deletions
62
app/src/main/kotlin/io/sakurasou/execute/executor/image/ImageExecutor.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package io.sakurasou.execute.executor.image | ||
|
||
import io.github.oshai.kotlinlogging.KotlinLogging | ||
import io.sakurasou.execute.task.image.DeleteImageTask | ||
import io.sakurasou.execute.task.image.DeleteThumbnailTask | ||
import io.sakurasou.execute.task.image.ImageTask | ||
import io.sakurasou.execute.task.image.PersistImageThumbnailTask | ||
import io.sakurasou.execute.task.image.RePersistImageThumbnailTask | ||
import io.sakurasou.model.entity.Strategy | ||
import kotlinx.coroutines.* | ||
import kotlinx.coroutines.channels.Channel | ||
import java.awt.image.BufferedImage | ||
|
||
/** | ||
* @author Shiina Kin | ||
* 2024/12/18 18:31 | ||
*/ | ||
object ImageExecutor { | ||
private val logger = KotlinLogging.logger {} | ||
|
||
private val imageExecuteScope = CoroutineScope( | ||
Dispatchers.IO + SupervisorJob() + CoroutineName("ImageExecutor") | ||
) | ||
|
||
private val taskChannel = Channel<ImageTask>(Channel.UNLIMITED) | ||
|
||
init { | ||
imageExecuteScope.launch { | ||
logger.debug { "image executor started" } | ||
for (task in taskChannel) { | ||
launch { | ||
try { | ||
task.execute() | ||
} catch (e: Exception) { | ||
e.printStackTrace() | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
suspend fun persistThumbnail(strategy: Strategy, subFolder: String, fileName: String, image: BufferedImage) { | ||
taskChannel.send(PersistImageThumbnailTask(strategy, subFolder, fileName, image)) | ||
} | ||
|
||
suspend fun rePersistThumbnail(strategy: Strategy, relativePath: String) { | ||
taskChannel.send(RePersistImageThumbnailTask(strategy, relativePath)) | ||
} | ||
|
||
suspend fun deleteImage(strategy: Strategy, relativePath: String) { | ||
taskChannel.send(DeleteImageTask(strategy, relativePath)) | ||
} | ||
|
||
suspend fun deleteThumbnail(strategy: Strategy, relativePath: String) { | ||
taskChannel.send(DeleteThumbnailTask(strategy, relativePath)) | ||
} | ||
|
||
fun shutdown() { | ||
taskChannel.close() | ||
imageExecuteScope.cancel() | ||
} | ||
} |
123 changes: 123 additions & 0 deletions
123
app/src/main/kotlin/io/sakurasou/execute/task/image/ImageTask.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package io.sakurasou.execute.task.image | ||
|
||
import io.github.oshai.kotlinlogging.KotlinLogging | ||
import io.sakurasou.exception.service.image.io.ImageFileDeleteFailedException | ||
import io.sakurasou.exception.service.image.io.ImageFileNotFoundException | ||
import io.sakurasou.model.entity.Strategy | ||
import io.sakurasou.model.group.ImageType | ||
import io.sakurasou.model.strategy.LocalStrategy | ||
import io.sakurasou.model.strategy.S3Strategy | ||
import io.sakurasou.util.ImageUtils | ||
import io.sakurasou.util.S3Utils | ||
import java.awt.image.BufferedImage | ||
import java.io.File | ||
import javax.imageio.ImageIO | ||
|
||
/** | ||
* @author Shiina Kin | ||
* 2024/12/18 18:38 | ||
*/ | ||
|
||
private const val THUMBNAIL_HEIGHT = 256 | ||
private const val THUMBNAIL_QUALITY = 0.9 | ||
|
||
sealed class ImageTask() { | ||
val logger = KotlinLogging.logger {} | ||
abstract fun execute() | ||
} | ||
|
||
class PersistImageThumbnailTask( | ||
private val strategy: Strategy, | ||
private val subFolder: String, | ||
private val fileName: String, | ||
private val image: BufferedImage | ||
) : ImageTask() { | ||
override fun execute() { | ||
val relativePath = "$subFolder/$fileName" | ||
val imageType = ImageType.valueOf(fileName.substringAfterLast('.').uppercase()) | ||
val thumbnailBytes = ImageUtils.transformImageByHeight(image, imageType, THUMBNAIL_HEIGHT, THUMBNAIL_QUALITY) | ||
ImageUtils.saveThumbnail(strategy, subFolder, fileName, thumbnailBytes, relativePath) | ||
logger.debug { "persist thumbnail $fileName in strategy(id ${strategy.id})" } | ||
} | ||
} | ||
|
||
class RePersistImageThumbnailTask( | ||
private val strategy: Strategy, | ||
private val relativePath: String | ||
) : ImageTask() { | ||
override fun execute() { | ||
val fileName = relativePath.substringAfterLast('/') | ||
when (val strategyConfig = strategy.config) { | ||
is LocalStrategy -> { | ||
val uploadFolderStr = strategyConfig.uploadFolder | ||
val uploadFile = File(uploadFolderStr, relativePath) | ||
if (!uploadFile.exists()) throw ImageFileNotFoundException() | ||
uploadFile.inputStream() | ||
} | ||
|
||
is S3Strategy -> { | ||
val filePath = "${strategyConfig.uploadFolder}/$relativePath" | ||
S3Utils.fetchImage(filePath, strategy) | ||
} | ||
}.use { rawImageInputStream -> | ||
val rawImage = ImageIO.read(rawImageInputStream) | ||
val subFolder = relativePath.substringBeforeLast('/') | ||
val imageType = ImageType.valueOf(fileName.substringAfterLast('.').uppercase()) | ||
val thumbnailBytes = | ||
ImageUtils.transformImageByHeight(rawImage, imageType, THUMBNAIL_HEIGHT, THUMBNAIL_QUALITY) | ||
ImageUtils.saveThumbnail(strategy, subFolder, fileName, thumbnailBytes, relativePath) | ||
} | ||
logger.debug { "re-persist thumbnail $fileName in strategy(id ${strategy.id})" } | ||
} | ||
} | ||
|
||
class DeleteImageTask( | ||
private val strategy: Strategy, | ||
private val relativePath: String | ||
) : ImageTask() { | ||
override fun execute() { | ||
when (val strategyConfig = strategy.config) { | ||
is LocalStrategy -> { | ||
val uploadFolderStr = strategyConfig.uploadFolder | ||
val uploadFile = File(uploadFolderStr, relativePath) | ||
|
||
if (uploadFile.exists() && !uploadFile.delete()) { | ||
logger.error { "Failed to delete image at $uploadFolderStr/$relativePath" } | ||
throw ImageFileDeleteFailedException() | ||
} | ||
} | ||
|
||
is S3Strategy -> { | ||
val filePath = "${strategyConfig.uploadFolder}/$relativePath" | ||
S3Utils.deleteImage(filePath, strategy) | ||
} | ||
} | ||
val fileName = relativePath.substringAfterLast('/') | ||
logger.debug { "delete image $fileName from strategy(id ${strategy.id})" } | ||
} | ||
} | ||
|
||
class DeleteThumbnailTask( | ||
private val strategy: Strategy, | ||
private val relativePath: String | ||
) : ImageTask() { | ||
override fun execute() { | ||
when (val strategyConfig = strategy.config) { | ||
is LocalStrategy -> { | ||
val thumbnailFolder = strategyConfig.thumbnailFolder | ||
val thumbnailFile = File(thumbnailFolder, relativePath) | ||
|
||
if (thumbnailFile.exists() && !thumbnailFile.delete()) { | ||
logger.error { "Failed to delete thumbnail at $thumbnailFolder/$relativePath" } | ||
} | ||
} | ||
|
||
is S3Strategy -> { | ||
val filePath = "${strategyConfig.thumbnailFolder}/$relativePath" | ||
S3Utils.deleteImage(filePath, strategy) | ||
} | ||
} | ||
val fileName = relativePath.substringAfterLast('/') | ||
logger.debug { "delete thumbnail $fileName from strategy(id ${strategy.id})" } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.