add previews in binary content
This commit is contained in:
parent
787f8d7526
commit
72578f6b58
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user