add previews in binary content
This commit is contained in:
features
common
server
src
jvmMain
kotlin
dev
inmo
postssystem
features
common
server
sessions
content
binary
server
server
src
jvmMain
kotlin
dev
inmo
postssystem
features
content
files
common
src
jvmMain
kotlin
dev
inmo
postssystem
features
files
common
gradle
server/src/main/java/dev/inmo/postssystem/server
services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ui/create
@ -3,13 +3,6 @@ package dev.inmo.postssystem.features.common.server.sessions
|
||||
import org.koin.core.qualifier.StringQualifier
|
||||
|
||||
object Qualifiers {
|
||||
val binaryFilesFolderQualifier = StringQualifier("binaryFilesFolder")
|
||||
val originalFilesMetasKeyValueRepoQualifier = StringQualifier("OriginalFilesMetaKV")
|
||||
val binaryOriginalFilesMetasKeyValueRepoQualifier = StringQualifier("BinaryOriginalFilesMetaKV")
|
||||
val commonFilesMetasKeyValueRepoQualifier = StringQualifier("CommonFilesMetaKV")
|
||||
val binaryFilesMetasKeyValueRepoQualifier = StringQualifier("BinaryFilesMetaKV")
|
||||
val filesFolderQualifier = StringQualifier("rootFilesFolder")
|
||||
val commonFilesFolderQualifier = StringQualifier("commonFilesFolder")
|
||||
val filesFolderQualifier = StringQualifier("filesFolder")
|
||||
val usersRolesKeyValueFactoryQualifier = StringQualifier("usersRolesKeyValueFactory")
|
||||
val binaryStorageFilesQualifier = StringQualifier("binaryContentFiles")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
9
features/content/binary/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/content/binary/server/ImagesCropper.kt
Normal file
9
features/content/binary/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/content/binary/server/ImagesCropper.kt
Normal file
@ -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,
|
||||
)
|
51
features/content/binary/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/content/binary/server/ScrimageBasedImagesCropper.kt
Normal file
51
features/content/binary/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/content/binary/server/ScrimageBasedImagesCropper.kt
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,9 @@ class WriteDistFilesStorage(
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
FullFileInfoStorageWrapper(newId, it)
|
||||
FullFileInfoStorageWrapper(newId, it).also {
|
||||
_newObjectsFlow.emit(it)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteById(ids: List<FileId>) {
|
||||
|
@ -10,6 +10,7 @@ ktor = "1.6.8"
|
||||
klock = "2.6.3"
|
||||
exposed = "0.37.3"
|
||||
psql = "42.3.0"
|
||||
scrimage = "4.0.31"
|
||||
|
||||
android-dexcount = "3.0.1"
|
||||
android-junit = "4.12"
|
||||
@ -50,6 +51,8 @@ tgbotapi = { module = "dev.inmo:tgbotapi", version.ref = "tgbotapi" }
|
||||
|
||||
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" }
|
||||
|
||||
scrimage = { module = "com.sksamuel.scrimage:scrimage-core", version.ref = "scrimage" }
|
||||
|
||||
androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test-junit" }
|
||||
androidx-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso-core" }
|
||||
|
||||
|
@ -32,9 +32,4 @@ data class Config(
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
val commonFilesFolder: File
|
||||
get() = File(filesFolderFile, "common").also { it.mkdirs() }
|
||||
val binaryFilesFolder: File
|
||||
get() = File(filesFolderFile, "binary_content").also { it.mkdirs() }
|
||||
}
|
||||
|
@ -94,25 +94,8 @@ fun getDIModule(
|
||||
singleWithBinds { get<Config>().databaseConfig }
|
||||
singleWithBinds { get<Config>().authConfig }
|
||||
singleWithBinds(Qualifiers.filesFolderQualifier) { get<Config>().filesFolderFile }
|
||||
singleWithBinds(Qualifiers.commonFilesFolderQualifier) { get<Config>().commonFilesFolder }
|
||||
singleWithBinds(Qualifiers.binaryFilesFolderQualifier) { get<Config>().binaryFilesFolder }
|
||||
|
||||
singleWithBinds { get<DatabaseConfig>().database }
|
||||
singleWithBinds(Qualifiers.originalFilesMetasKeyValueRepoQualifier) {
|
||||
ExposedKeyValueRepo(get(), { text("fileid") }, { text("metaInfo") }, "FileIdsToMetas")
|
||||
}
|
||||
singleWithBinds(Qualifiers.binaryOriginalFilesMetasKeyValueRepoQualifier) {
|
||||
ExposedKeyValueRepo(get(), { text("fileid") }, { text("metaInfo") }, "BinaryContentFileIdsToMetas")
|
||||
}
|
||||
singleWithBinds(Qualifiers.commonFilesMetasKeyValueRepoQualifier) {
|
||||
MetasKeyValueRepo(
|
||||
get(),
|
||||
get(Qualifiers.originalFilesMetasKeyValueRepoQualifier)
|
||||
)
|
||||
}
|
||||
single<ReadFilesStorage> { DiskReadFilesStorage(get(Qualifiers.commonFilesFolderQualifier), get(Qualifiers.commonFilesMetasKeyValueRepoQualifier)) }
|
||||
single<WriteFilesStorage> { WriteDistFilesStorage(get(Qualifiers.commonFilesFolderQualifier), get(Qualifiers.commonFilesMetasKeyValueRepoQualifier)) }
|
||||
single<FilesStorage> { DefaultFilesStorage(get(), get()) }
|
||||
singleWithBinds { ExposedUsersStorage(get()) }
|
||||
singleWithBinds { exposedUsersAuthenticator(get(), get()) }
|
||||
|
||||
@ -139,18 +122,6 @@ fun getDIModule(
|
||||
factory<CoroutineScope> { baseScope.LinkedSupervisorScope() }
|
||||
|
||||
// Content storages
|
||||
singleWithBinds(Qualifiers.binaryFilesMetasKeyValueRepoQualifier) {
|
||||
MetasKeyValueRepo(
|
||||
get(),
|
||||
get(Qualifiers.binaryOriginalFilesMetasKeyValueRepoQualifier)
|
||||
)
|
||||
}
|
||||
single<FilesStorage>(Qualifiers.binaryStorageFilesQualifier) {
|
||||
DefaultFilesStorage(
|
||||
DiskReadFilesStorage(get(Qualifiers.binaryFilesFolderQualifier), get(Qualifiers.binaryFilesMetasKeyValueRepoQualifier)),
|
||||
WriteDistFilesStorage(get(Qualifiers.binaryFilesFolderQualifier), get(Qualifiers.binaryFilesMetasKeyValueRepoQualifier))
|
||||
)
|
||||
}
|
||||
|
||||
single<ServerContentStorage<Content>> { ServerContentStorageAggregator(getAll(), get()) } binds arrayOf(
|
||||
ServerReadContentStorage::class,
|
||||
|
@ -20,7 +20,7 @@ class DefaultPostCreateUIModel(
|
||||
FullNewPost(content)
|
||||
) ?: return@runCatching
|
||||
delay(1000L)
|
||||
publicationService.publish(post.id)
|
||||
// publicationService.publish(post.id)
|
||||
}.onFailure {
|
||||
_currentState.value = PostCreateUIState.Fail
|
||||
}.onSuccess {
|
||||
|
Reference in New Issue
Block a user