add previews in binary content
This commit is contained in:
@@ -14,5 +14,10 @@ kotlin {
|
||||
api project(":postssystem.features.files.server")
|
||||
}
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api libs.scrimage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,18 +1,105 @@
|
||||
package dev.inmo.postssystem.features.content.binary.server
|
||||
|
||||
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo
|
||||
import dev.inmo.postssystem.features.common.common.singleWithBinds
|
||||
import dev.inmo.postssystem.features.common.common.singleWithRandomQualifier
|
||||
import dev.inmo.postssystem.features.common.server.sessions.Qualifiers
|
||||
import dev.inmo.postssystem.features.common.server.sessions.ServerModuleLoader
|
||||
import dev.inmo.postssystem.features.content.common.BinaryContent
|
||||
import dev.inmo.postssystem.features.content.server.ServerContentStorageWrapper
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import dev.inmo.postssystem.features.files.common.*
|
||||
import dev.inmo.postssystem.features.files.common.storage.DefaultFilesStorage
|
||||
import dev.inmo.postssystem.features.files.common.storage.FilesStorage
|
||||
import kotlinx.serialization.json.*
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.qualifier.StringQualifier
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* This provider is declaring one additional optional section: "previewDimensions". This section is an object with two
|
||||
* fields:
|
||||
*
|
||||
* * maxWidth
|
||||
* * maxHeight
|
||||
*
|
||||
* for preview images
|
||||
*/
|
||||
class BinaryContentServerModuleLoader : ServerModuleLoader {
|
||||
override fun Module.load(config: JsonObject) {
|
||||
val binaryFilesFolderQualifier = StringQualifier("binaryFilesFolder")
|
||||
val binaryStorageFilesQualifier = StringQualifier("binaryContentFiles")
|
||||
val binaryPreviewStorageFilesQualifier = StringQualifier("binaryPreviewContentFiles")
|
||||
val binaryPreviewFilesFolderQualifier = StringQualifier("binaryPreviewFilesFolder")
|
||||
val binaryFilesMetasKeyValueRepoQualifier = StringQualifier("BinaryFilesMetaKV")
|
||||
val binaryPreviewFilesMetasKeyValueRepoQualifier = StringQualifier("BinaryPreviewFilesMetaKV")
|
||||
val binaryOriginalFilesMetasKeyValueRepoQualifier = StringQualifier("BinaryOriginalFilesMetaKV")
|
||||
val binaryOriginalPreviewFilesMetasKeyValueRepoQualifier = StringQualifier("BinaryPreviewOriginalFilesMetaKV")
|
||||
|
||||
singleWithBinds(binaryFilesFolderQualifier) {
|
||||
File(get<File>(Qualifiers.filesFolderQualifier), "binary_content").apply {
|
||||
mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
single {
|
||||
val dimensionsSection = config["previewDimensions"] ?: return@single PreviewImageDimensionsConfig()
|
||||
get<Json>().decodeFromJsonElement(
|
||||
PreviewImageDimensionsConfig.serializer(),
|
||||
dimensionsSection
|
||||
)
|
||||
}
|
||||
|
||||
single<ImagesCropper> {
|
||||
ScrimageBasedImagesCropper(get())
|
||||
}
|
||||
|
||||
singleWithBinds(binaryPreviewFilesFolderQualifier) {
|
||||
File(get<File>(binaryFilesFolderQualifier), "preview").apply {
|
||||
mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
singleWithBinds(binaryOriginalFilesMetasKeyValueRepoQualifier) {
|
||||
ExposedKeyValueRepo(get(), { text("fileid") }, { text("metaInfo") }, "BinaryContentFileIdsToMetas")
|
||||
}
|
||||
|
||||
singleWithBinds(binaryOriginalPreviewFilesMetasKeyValueRepoQualifier) {
|
||||
ExposedKeyValueRepo(get(), { text("fileid") }, { text("metaInfo") }, "BinaryPreviewContentFileIdsToMetas")
|
||||
}
|
||||
|
||||
singleWithBinds(binaryFilesMetasKeyValueRepoQualifier) {
|
||||
MetasKeyValueRepo(
|
||||
get(),
|
||||
get(binaryOriginalFilesMetasKeyValueRepoQualifier)
|
||||
)
|
||||
}
|
||||
singleWithBinds(binaryPreviewFilesMetasKeyValueRepoQualifier) {
|
||||
MetasKeyValueRepo(
|
||||
get(),
|
||||
get(binaryOriginalPreviewFilesMetasKeyValueRepoQualifier)
|
||||
)
|
||||
}
|
||||
single<FilesStorage>(binaryStorageFilesQualifier) {
|
||||
DefaultFilesStorage(
|
||||
DiskReadFilesStorage(get(binaryFilesFolderQualifier), get(binaryFilesMetasKeyValueRepoQualifier)),
|
||||
WriteDistFilesStorage(get(binaryFilesFolderQualifier), get(binaryFilesMetasKeyValueRepoQualifier))
|
||||
)
|
||||
}
|
||||
single<FilesStorage>(binaryPreviewStorageFilesQualifier) {
|
||||
DefaultFilesStorage(
|
||||
DiskReadFilesStorage(get(binaryPreviewFilesFolderQualifier), get(binaryPreviewFilesMetasKeyValueRepoQualifier)),
|
||||
WriteDistFilesStorage(get(binaryPreviewFilesFolderQualifier), get(binaryPreviewFilesMetasKeyValueRepoQualifier))
|
||||
)
|
||||
}
|
||||
|
||||
singleWithRandomQualifier {
|
||||
ServerContentStorageWrapper(
|
||||
BinaryServerContentStorage(get(Qualifiers.binaryStorageFilesQualifier)),
|
||||
BinaryServerContentStorage(
|
||||
get(binaryStorageFilesQualifier),
|
||||
get(binaryPreviewStorageFilesQualifier),
|
||||
get(),
|
||||
get()
|
||||
),
|
||||
BinaryContent::class
|
||||
)
|
||||
}
|
||||
|
@@ -1,16 +1,29 @@
|
||||
package dev.inmo.postssystem.features.content.binary.server
|
||||
|
||||
import com.benasher44.uuid.uuid4
|
||||
import dev.inmo.micro_utils.coroutines.plus
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.mime_types.KnownMimeTypes
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
import dev.inmo.micro_utils.repos.UpdatedValuePair
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import dev.inmo.postssystem.features.common.common.FileBasedInputProvider
|
||||
import dev.inmo.postssystem.features.content.common.*
|
||||
import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage
|
||||
import dev.inmo.postssystem.features.files.common.*
|
||||
import dev.inmo.postssystem.features.files.common.storage.FilesStorage
|
||||
import io.ktor.util.asStream
|
||||
import io.ktor.utils.io.core.copyTo
|
||||
import io.ktor.utils.io.streams.asOutput
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.io.File
|
||||
|
||||
class BinaryServerContentStorage(
|
||||
private val filesStorage: FilesStorage
|
||||
private val filesStorage: FilesStorage,
|
||||
private val previewFilesStorage: FilesStorage,
|
||||
private val cropper: ImagesCropper,
|
||||
private val scope: CoroutineScope
|
||||
) : ServerContentStorage<BinaryContent> {
|
||||
private val FileId.asContentId
|
||||
get() = ContentId(string)
|
||||
@@ -35,6 +48,50 @@ class BinaryServerContentStorage(
|
||||
override val newObjectsFlow: Flow<RegisteredContent> = filesStorage.newObjectsFlow.map { it.asRegisteredContent }
|
||||
override val updatedObjectsFlow: Flow<RegisteredContent> = filesStorage.updatedObjectsFlow.map { it.asRegisteredContent }
|
||||
|
||||
private val fullsRemovedJob = deletedObjectsIdsFlow.subscribeSafelyWithoutExceptions(scope) {
|
||||
previewFilesStorage.deleteById(it.asFileId)
|
||||
}
|
||||
|
||||
private val previewCroppingJob = (newObjectsFlow + updatedObjectsFlow).subscribeSafelyWithoutExceptions(scope) {
|
||||
val content = it.content
|
||||
val fileId = it.id.asFileId
|
||||
if (content !is BinaryContent) {
|
||||
return@subscribeSafelyWithoutExceptions
|
||||
}
|
||||
val fullFileInfo = filesStorage.getFullFileInfo(fileId) ?: return@subscribeSafelyWithoutExceptions
|
||||
val fileInfo = fullFileInfo.fileInfo
|
||||
|
||||
if (fileInfo.mimeType is KnownMimeTypes.Image) {
|
||||
cropper.crop(fileInfo.inputProvider).subscribeSafelyWithoutExceptions(scope) {
|
||||
val tempFile = File.createTempFile(uuid4().toString(), ".${fileInfo.name.extension}").apply {
|
||||
deleteOnExit()
|
||||
createNewFile()
|
||||
}
|
||||
runCatching {
|
||||
tempFile.outputStream().use { output ->
|
||||
it.asStream().use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
val newFullFileInfo = FullFileInfo(
|
||||
fileInfo.name,
|
||||
fileInfo.mimeType,
|
||||
FileBasedInputProvider(tempFile)
|
||||
)
|
||||
if (previewFilesStorage.contains(fileId)) {
|
||||
previewFilesStorage.update(
|
||||
fullFileInfo.id,
|
||||
newFullFileInfo
|
||||
)
|
||||
} else {
|
||||
previewFilesStorage.create(newFullFileInfo).firstOrNull()
|
||||
}
|
||||
}
|
||||
tempFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun create(values: List<BinaryContent>): List<RegisteredContent> {
|
||||
return filesStorage.create(
|
||||
values.map { it.asFullFileInfo }
|
||||
@@ -83,6 +140,8 @@ class BinaryServerContentStorage(
|
||||
}
|
||||
|
||||
override suspend fun getContentPreview(id: ContentId): RegisteredContent? {
|
||||
TODO("Not yet implemented")
|
||||
val fileId = id.asFileId
|
||||
val fileInfo = previewFilesStorage.getFullFileInfo(fileId) ?: return null
|
||||
return fileInfo.asRegisteredContent
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,9 @@
|
||||
package dev.inmo.postssystem.features.content.binary.server
|
||||
|
||||
import dev.inmo.postssystem.features.common.common.SimpleInputProvider
|
||||
import io.ktor.utils.io.core.Input
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ImagesCropper {
|
||||
suspend fun crop(inputProvider: SimpleInputProvider): Flow<Input>
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package dev.inmo.postssystem.features.content.binary.server
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PreviewImageDimensionsConfig(
|
||||
val maxWidth: Int = 1024,
|
||||
val maxHeight: Int = 1024,
|
||||
)
|
@@ -0,0 +1,51 @@
|
||||
package dev.inmo.postssystem.features.content.binary.server
|
||||
|
||||
import com.benasher44.uuid.uuid4
|
||||
import com.sksamuel.scrimage.ImmutableImage
|
||||
import com.sksamuel.scrimage.nio.JpegWriter
|
||||
import dev.inmo.postssystem.features.common.common.SimpleInputProvider
|
||||
import io.ktor.util.asStream
|
||||
import io.ktor.utils.io.core.Input
|
||||
import io.ktor.utils.io.streams.asInput
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.isActive
|
||||
import java.io.File
|
||||
|
||||
class ScrimageBasedImagesCropper(
|
||||
private val config: PreviewImageDimensionsConfig
|
||||
) : ImagesCropper {
|
||||
override suspend fun crop(inputProvider: SimpleInputProvider): Flow<Input> = flow {
|
||||
val outputTempFile = File.createTempFile(uuid4().toString(), ".temp").apply {
|
||||
createNewFile()
|
||||
deleteOnExit()
|
||||
}
|
||||
ImmutableImage.loader().fromStream(
|
||||
inputProvider().asStream()
|
||||
).max(
|
||||
config.maxWidth,
|
||||
config.maxHeight
|
||||
).let {
|
||||
if (currentCoroutineContext().isActive) {
|
||||
it.output(
|
||||
JpegWriter(),
|
||||
outputTempFile
|
||||
)
|
||||
} else {
|
||||
return@flow
|
||||
}
|
||||
}
|
||||
|
||||
val input = outputTempFile.inputStream().asInput()
|
||||
runCatching {
|
||||
emit(input)
|
||||
}.onSuccess {
|
||||
if (input.endOfInput) { // remove file if it was fully read inside of emit
|
||||
outputTempFile.delete()
|
||||
}
|
||||
}.onFailure {
|
||||
outputTempFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
@@ -115,4 +115,25 @@ class ServerContentStorageAggregator(
|
||||
|
||||
return currentResults.createPaginationResult(pagination, count())
|
||||
}
|
||||
|
||||
override suspend fun getContentPreview(id: ContentId): RegisteredContent? {
|
||||
val result = CompletableDeferred<RegisteredContent>()
|
||||
|
||||
storages.map {
|
||||
scope.launch {
|
||||
val content = it.getContentPreview(id)
|
||||
if (content != null) {
|
||||
result.complete(content)
|
||||
}
|
||||
}.also { job ->
|
||||
result.invokeOnCompletion { job.cancel() }
|
||||
}
|
||||
}.joinAll()
|
||||
|
||||
return if (result.isCompleted) {
|
||||
result.getCompleted()
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user