add integration with posts creating

This commit is contained in:
InsanusMokrassar 2022-01-22 13:30:49 +06:00
parent d1fa0242fa
commit 5e61c2a770
22 changed files with 548 additions and 43 deletions

View File

@ -23,6 +23,9 @@ kotlin {
api project(":postssystem.features.content.client") api project(":postssystem.features.content.client")
api project(":postssystem.features.content.text.client") api project(":postssystem.features.content.text.client")
api project(":postssystem.features.content.binary.client") api project(":postssystem.features.content.binary.client")
api project(":postssystem.services.posts.client")
api "dev.inmo:micro_utils.fsm.common:$microutils_version" api "dev.inmo:micro_utils.fsm.common:$microutils_version"
api "dev.inmo:micro_utils.fsm.repos.common:$microutils_version" api "dev.inmo:micro_utils.fsm.repos.common:$microutils_version"
api "dev.inmo:micro_utils.crypto:$microutils_version" api "dev.inmo:micro_utils.crypto:$microutils_version"

View File

@ -25,11 +25,12 @@ import dev.inmo.postssystem.client.settings.auth.AuthSettings
import dev.inmo.postssystem.client.settings.auth.DefaultAuthSettings import dev.inmo.postssystem.client.settings.auth.DefaultAuthSettings
import dev.inmo.postssystem.features.common.common.SerializersModuleConfigurator import dev.inmo.postssystem.features.common.common.SerializersModuleConfigurator
import dev.inmo.postssystem.features.common.common.singleWithRandomQualifier import dev.inmo.postssystem.features.common.common.singleWithRandomQualifier
import dev.inmo.postssystem.features.content.binary.common.BinaryContentSerializerModuleConfigurator
import dev.inmo.postssystem.features.content.common.ContentSerializersModuleConfigurator import dev.inmo.postssystem.features.content.common.ContentSerializersModuleConfigurator
import dev.inmo.postssystem.features.content.common.OtherContentSerializerModuleConfigurator import dev.inmo.postssystem.features.content.common.OtherContentSerializerModuleConfigurator
import dev.inmo.postssystem.features.content.text.common.TextContentSerializerModuleConfigurator import dev.inmo.postssystem.features.content.text.common.TextContentSerializerModuleConfigurator
import dev.inmo.postssystem.features.status.client.StatusFeatureClient import dev.inmo.postssystem.features.status.client.StatusFeatureClient
import dev.inmo.postssystem.services.posts.client.ClientPostsService
import dev.inmo.postssystem.services.posts.common.*
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -43,6 +44,7 @@ import org.koin.core.context.startKoin
import org.koin.core.module.Module import org.koin.core.module.Module
import org.koin.core.qualifier.* import org.koin.core.qualifier.*
import org.koin.core.scope.Scope import org.koin.core.scope.Scope
import org.koin.dsl.binds
import org.koin.dsl.module import org.koin.dsl.module
val UIScopeQualifier = StringQualifier("CoroutineScopeUI") val UIScopeQualifier = StringQualifier("CoroutineScopeUI")
@ -69,7 +71,6 @@ fun baseKoin(
module { module {
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { OtherContentSerializerModuleConfigurator } singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { OtherContentSerializerModuleConfigurator }
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { TextContentSerializerModuleConfigurator } singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { TextContentSerializerModuleConfigurator }
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { BinaryContentSerializerModuleConfigurator }
singleWithRandomQualifier<SerializersModuleConfigurator.Element> { ContentSerializersModuleConfigurator(getAll()) } singleWithRandomQualifier<SerializersModuleConfigurator.Element> { ContentSerializersModuleConfigurator(getAll()) }
single { SerializersModuleConfigurator(getAll()) } single { SerializersModuleConfigurator(getAll()) }
@ -133,5 +134,9 @@ fun getAuthorizedFeaturesDIModule(
single<ReadFilesStorage> { ClientReadFilesStorage(get(serverUrlQualifier), get(), get()) } single<ReadFilesStorage> { ClientReadFilesStorage(get(serverUrlQualifier), get(), get()) }
single<ReadUsersStorage> { UsersStorageKtorClient(get(serverUrlQualifier), get()) } single<ReadUsersStorage> { UsersStorageKtorClient(get(serverUrlQualifier), get()) }
single<RolesStorage<Role>> { ClientRolesStorage(get(serverUrlQualifier), get(), Role.serializer()) } single<RolesStorage<Role>> { ClientRolesStorage(get(serverUrlQualifier), get(), Role.serializer()) }
single<PostsService> { ClientPostsService(get(serverUrlQualifier), get()) } binds arrayOf(
ReadPostsService::class,
WritePostsService::class
)
} }
} }

View File

@ -1,11 +0,0 @@
package dev.inmo.postssystem.features.content.binary.common
import dev.inmo.postssystem.features.content.common.Content
import dev.inmo.postssystem.features.content.common.ContentSerializersModuleConfigurator
import kotlinx.serialization.modules.PolymorphicModuleBuilder
object BinaryContentSerializerModuleConfigurator : ContentSerializersModuleConfigurator.Element {
override fun PolymorphicModuleBuilder<Content>.invoke() {
subclass(DefaultBinaryContent::class, DefaultBinaryContent.serializer())
}
}

View File

@ -1,14 +0,0 @@
package dev.inmo.postssystem.features.content.binary.common
import dev.inmo.micro_utils.common.FileName
import dev.inmo.micro_utils.mime_types.MimeType
import dev.inmo.postssystem.features.common.common.SimpleInputProvider
import dev.inmo.postssystem.features.content.common.BinaryContent
import kotlinx.serialization.Serializable
@Serializable
data class DefaultBinaryContent(
override val filename: FileName,
override val mimeType: MimeType,
override val inputProvider: SimpleInputProvider
) : BinaryContent

View File

@ -2,7 +2,6 @@ package dev.inmo.postssystem.features.content.binary.server
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.repos.UpdatedValuePair import dev.inmo.micro_utils.repos.UpdatedValuePair
import dev.inmo.postssystem.features.content.binary.common.DefaultBinaryContent
import dev.inmo.postssystem.features.content.common.* import dev.inmo.postssystem.features.content.common.*
import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage
import dev.inmo.postssystem.features.files.common.* import dev.inmo.postssystem.features.files.common.*
@ -12,7 +11,7 @@ import kotlinx.coroutines.flow.map
class BinaryServerContentStorage( class BinaryServerContentStorage(
private val filesStorage: FilesStorage private val filesStorage: FilesStorage
) : ServerContentStorage<DefaultBinaryContent> { ) : ServerContentStorage<BinaryContent> {
private val FileId.asContentId private val FileId.asContentId
get() = ContentId(string) get() = ContentId(string)
private val ContentId.asFileId private val ContentId.asFileId
@ -20,13 +19,13 @@ class BinaryServerContentStorage(
private val FullFileInfoStorageWrapper.asRegisteredContent private val FullFileInfoStorageWrapper.asRegisteredContent
get() = RegisteredContent( get() = RegisteredContent(
id.asContentId, id.asContentId,
DefaultBinaryContent( BinaryContent(
fileInfo.name, fileInfo.name,
fileInfo.mimeType, fileInfo.mimeType,
fileInfo.inputProvider fileInfo.inputProvider
) )
) )
private val DefaultBinaryContent.asFullFileInfo private val BinaryContent.asFullFileInfo
get() = FullFileInfo( get() = FullFileInfo(
filename, filename,
mimeType, mimeType,
@ -36,7 +35,7 @@ class BinaryServerContentStorage(
override val newObjectsFlow: Flow<RegisteredContent> = filesStorage.newObjectsFlow.map { it.asRegisteredContent } override val newObjectsFlow: Flow<RegisteredContent> = filesStorage.newObjectsFlow.map { it.asRegisteredContent }
override val updatedObjectsFlow: Flow<RegisteredContent> = filesStorage.updatedObjectsFlow.map { it.asRegisteredContent } override val updatedObjectsFlow: Flow<RegisteredContent> = filesStorage.updatedObjectsFlow.map { it.asRegisteredContent }
override suspend fun create(values: List<DefaultBinaryContent>): List<RegisteredContent> { override suspend fun create(values: List<BinaryContent>): List<RegisteredContent> {
return filesStorage.create( return filesStorage.create(
values.map { it.asFullFileInfo } values.map { it.asFullFileInfo }
).map { it.asRegisteredContent } ).map { it.asRegisteredContent }
@ -46,14 +45,14 @@ class BinaryServerContentStorage(
filesStorage.deleteById(ids.map { it.asFileId }) filesStorage.deleteById(ids.map { it.asFileId })
} }
override suspend fun update(id: ContentId, value: DefaultBinaryContent): RegisteredContent? { override suspend fun update(id: ContentId, value: BinaryContent): RegisteredContent? {
return filesStorage.update( return filesStorage.update(
id.asFileId, id.asFileId,
value.asFullFileInfo value.asFullFileInfo
) ?.asRegisteredContent ) ?.asRegisteredContent
} }
override suspend fun update(values: List<UpdatedValuePair<ContentId, DefaultBinaryContent>>): List<RegisteredContent> { override suspend fun update(values: List<UpdatedValuePair<ContentId, BinaryContent>>): List<RegisteredContent> {
return filesStorage.update( return filesStorage.update(
values.map { (id, content) -> values.map { (id, content) ->
id.asFileId to content.asFullFileInfo id.asFileId to content.asFullFileInfo

View File

@ -3,6 +3,7 @@ package dev.inmo.postssystem.features.content.common
import dev.inmo.micro_utils.common.FileName import dev.inmo.micro_utils.common.FileName
import dev.inmo.micro_utils.mime_types.MimeType import dev.inmo.micro_utils.mime_types.MimeType
import dev.inmo.postssystem.features.common.common.SimpleInputProvider import dev.inmo.postssystem.features.common.common.SimpleInputProvider
import kotlinx.serialization.PolymorphicSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline import kotlin.jvm.JvmInline
@ -26,11 +27,13 @@ interface SimpleContent : Content
/** /**
* This type represents some binary data which can be sent with multipart and deserialized from it * This type represents some binary data which can be sent with multipart and deserialized from it
*/ */
interface BinaryContent : Content { data class BinaryContent(
val filename: FileName val filename: FileName,
val mimeType: MimeType val mimeType: MimeType,
val inputProvider: SimpleInputProvider val inputProvider: SimpleInputProvider
} ) : Content
val ContentSerializer = PolymorphicSerializer(Content::class)
/** /**
* Content which is already registered in database. Using its [id] you can retrieve all known * Content which is already registered in database. Using its [id] you can retrieve all known

View File

@ -13,13 +13,13 @@ kotlin_version=1.6.10
kotlin_serialisation_core_version=1.3.2 kotlin_serialisation_core_version=1.3.2
koin_version=3.1.2 koin_version=3.1.2
microutils_version=0.9.1 microutils_version=0.9.4
ktor_version=1.6.7 ktor_version=1.6.7
logback_version=1.2.10 logback_version=1.2.10
uuid_version=0.3.1 uuid_version=0.3.1
klock_version=2.4.10 klock_version=2.4.12
tgbotapi_version=0.38.1 tgbotapi_version=0.38.3
# Server # Server

View File

@ -23,6 +23,8 @@ dependencies {
api project(":postssystem.features.content.binary.server") api project(":postssystem.features.content.binary.server")
api project(":postssystem.features.publication.server") api project(":postssystem.features.publication.server")
api project(":postssystem.services.posts.server")
api project(":postssystem.targets.telegram.publication.server") api project(":postssystem.targets.telegram.publication.server")
api "io.ktor:ktor-server-netty:$ktor_version" api "io.ktor:ktor-server-netty:$ktor_version"

View File

@ -25,7 +25,6 @@ import dev.inmo.micro_utils.ktor.server.createKtorServer
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo
import dev.inmo.micro_utils.repos.exposed.onetomany.ExposedOneToManyKeyValueRepo import dev.inmo.micro_utils.repos.exposed.onetomany.ExposedOneToManyKeyValueRepo
import dev.inmo.postssystem.features.common.common.* import dev.inmo.postssystem.features.common.common.*
import dev.inmo.postssystem.features.content.binary.common.BinaryContentSerializerModuleConfigurator
import dev.inmo.postssystem.features.content.binary.server.BinaryServerContentStorage import dev.inmo.postssystem.features.content.binary.server.BinaryServerContentStorage
import dev.inmo.postssystem.features.content.common.* import dev.inmo.postssystem.features.content.common.*
import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage
@ -36,6 +35,7 @@ import dev.inmo.postssystem.features.posts.server.ExposedServerPostsStorage
import dev.inmo.postssystem.features.posts.server.ServerPostsStorage import dev.inmo.postssystem.features.posts.server.ServerPostsStorage
import dev.inmo.postssystem.features.publication.server.PublicationManager import dev.inmo.postssystem.features.publication.server.PublicationManager
import dev.inmo.postssystem.features.publication.server.PublicationTarget import dev.inmo.postssystem.features.publication.server.PublicationTarget
import dev.inmo.postssystem.services.posts.server.ServerPostsServiceRoutingConfigurator
import dev.inmo.postssystem.targets.telegram.publication.server.PublicationTargetTelegram import dev.inmo.postssystem.targets.telegram.publication.server.PublicationTargetTelegram
import io.ktor.application.featureOrNull import io.ktor.application.featureOrNull
import io.ktor.application.log import io.ktor.application.log
@ -86,7 +86,6 @@ fun getDIModule(
return module { return module {
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { OtherContentSerializerModuleConfigurator } singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { OtherContentSerializerModuleConfigurator }
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { TextContentSerializerModuleConfigurator } singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { TextContentSerializerModuleConfigurator }
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { BinaryContentSerializerModuleConfigurator }
singleWithRandomQualifier<SerializersModuleConfigurator.Element> { ContentSerializersModuleConfigurator(getAll()) } singleWithRandomQualifier<SerializersModuleConfigurator.Element> { ContentSerializersModuleConfigurator(getAll()) }
single { SerializersModuleConfigurator(getAll()) } single { SerializersModuleConfigurator(getAll()) }
@ -184,6 +183,7 @@ fun getDIModule(
singleWithBinds { UsersStorageServerRoutesConfigurator(get(), get()) } singleWithBinds { UsersStorageServerRoutesConfigurator(get(), get()) }
singleWithBinds { RolesStorageReadServerRoutesConfigurator<Role>(get(), RoleSerializer, get()) } singleWithBinds { RolesStorageReadServerRoutesConfigurator<Role>(get(), RoleSerializer, get()) }
singleWithBinds { RolesManagerRolesStorageServerRoutesConfigurator(get(), get()) } singleWithBinds { RolesManagerRolesStorageServerRoutesConfigurator(get(), get()) }
singleWithBinds { ServerPostsServiceRoutingConfigurator(get(), get(), get()) }
singleWithBinds { ClientStaticRoutingConfiguration(get<Config>().clientStatic) } singleWithBinds { ClientStaticRoutingConfiguration(get<Config>().clientStatic) }
singleWithBinds { singleWithBinds {

View File

@ -0,0 +1,11 @@
package dev.inmo.postssystem.services.posts.client
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
import dev.inmo.postssystem.services.posts.common.*
class ClientPostsService(
baseUrl: String,
unifiedRequester: UnifiedRequester
) : PostsService,
ReadPostsService by ClientReadPostsService(baseUrl, unifiedRequester),
WritePostsService by ClientWritePostsService(baseUrl, unifiedRequester)

View File

@ -0,0 +1,22 @@
package dev.inmo.postssystem.services.posts.client
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
import dev.inmo.micro_utils.repos.ReadCRUDRepo
import dev.inmo.micro_utils.repos.ktor.client.crud.KtorReadStandardCrudRepo
import dev.inmo.postssystem.features.posts.common.PostId
import dev.inmo.postssystem.features.posts.common.RegisteredPost
import dev.inmo.postssystem.services.posts.common.ReadPostsService
import dev.inmo.postssystem.services.posts.common.postsRootPath
import kotlinx.serialization.builtins.nullable
class ClientReadPostsService(
private val baseUrl: String,
private val unifiedRequester: UnifiedRequester
) : ReadPostsService, ReadCRUDRepo<RegisteredPost, PostId> by KtorReadStandardCrudRepo(
buildStandardUrl(baseUrl, postsRootPath),
unifiedRequester,
RegisteredPost.serializer(),
RegisteredPost.serializer().nullable,
PostId.serializer()
)

View File

@ -0,0 +1,123 @@
package dev.inmo.postssystem.services.posts.client
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
import dev.inmo.micro_utils.ktor.common.encodeHex
import dev.inmo.micro_utils.repos.ktor.common.crud.createRouting
import dev.inmo.micro_utils.repos.ktor.common.crud.updateRouting
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.removeRoute
import dev.inmo.postssystem.features.content.common.*
import dev.inmo.postssystem.features.posts.common.PostId
import dev.inmo.postssystem.features.posts.common.RegisteredPost
import dev.inmo.postssystem.services.posts.common.*
import io.ktor.client.request.forms.InputProvider
import io.ktor.client.request.forms.formData
import io.ktor.client.request.headers
import io.ktor.client.request.post
import io.ktor.http.HttpHeaders
import kotlinx.serialization.builtins.*
class ClientWritePostsService(
private val baseUrl: String,
private val unifiedRequester: UnifiedRequester
) : WritePostsService {
private val root = buildStandardUrl(baseUrl, postsRootPath)
private val contentEitherSerializer = EitherSerializer(ContentId.serializer(), ContentSerializer)
private val contentsEitherSerializer = ListSerializer(contentEitherSerializer)
private val contentsSerializer = ListSerializer(ContentSerializer)
private val createFullPath = buildStandardUrl(
root,
createRouting
)
private val removeFullPath = buildStandardUrl(
root,
removeRoute
)
override suspend fun create(newPost: FullNewPost): RegisteredPost? {
return if (newPost.content.any { it is BinaryContent }) {
val answer = unifiedRequester.client.post<ByteArray>(createFullPath) {
formData {
newPost.content.forEachIndexed { i, content ->
when (content) {
is BinaryContent -> append(
i.toString(),
InputProvider(block = content.inputProvider),
headers {
append(HttpHeaders.ContentType, content.mimeType.raw)
append(HttpHeaders.ContentDisposition, "filename=\"${content.filename.name}\"")
}.build()
)
else -> append(
i.toString(),
unifiedRequester.serialFormat.encodeHex(ContentSerializer, content)
)
}
}
}
}
unifiedRequester.serialFormat.decodeFromByteArray(RegisteredPost.serializer().nullable, answer)
} else {
unifiedRequester.unipost(
createFullPath,
contentsSerializer to newPost.content,
RegisteredPost.serializer().nullable
)
}
}
override suspend fun update(
postId: PostId,
content: List<Either<ContentId, Content>>
): RegisteredPost? {
return if (content.any { it.optionalT2.data is BinaryContent }) {
val answer = unifiedRequester.client.post<ByteArray>(createFullPath) {
formData {
content.forEachIndexed { i, eitherContent ->
eitherContent.onFirst {
append(
i.toString(),
unifiedRequester.serialFormat.encodeHex(contentEitherSerializer, it.either())
)
}.onSecond {
when (it) {
is BinaryContent -> append(
i.toString(),
InputProvider(block = it.inputProvider),
headers {
append(HttpHeaders.ContentType, it.mimeType.raw)
append(HttpHeaders.ContentDisposition, "filename=\"${it.filename.name}\"")
}.build()
)
else -> append(
i.toString(),
unifiedRequester.serialFormat.encodeHex(contentEitherSerializer, it.either())
)
}
}
}
}
}
unifiedRequester.serialFormat.decodeFromByteArray(RegisteredPost.serializer().nullable, answer)
} else {
unifiedRequester.unipost(
buildStandardUrl(
root,
updateRouting,
postsPostIdParameter to unifiedRequester.encodeUrlQueryValue(PostId.serializer(), postId)
),
contentsEitherSerializer to content,
RegisteredPost.serializer().nullable
)
}
}
override suspend fun remove(postId: PostId) = unifiedRequester.unipost(
removeFullPath,
PostId.serializer() to postId,
Unit.serializer()
)
}

View File

@ -12,6 +12,8 @@ kotlin {
dependencies { dependencies {
api project(":postssystem.features.common.common") api project(":postssystem.features.common.common")
api project(":postssystem.features.posts.common") api project(":postssystem.features.posts.common")
api "dev.inmo:micro_utils.repos.common:$microutils_version"
api "dev.inmo:micro_utils.repos.ktor.client:$microutils_version"
} }
} }
} }

View File

@ -1,3 +1,5 @@
package dev.inmo.postssystem.services.posts.common package dev.inmo.postssystem.services.posts.common
const val postsRootPath = "posts" const val postsRootPath = "posts"
const val postsPostIdParameter = "postId"

View File

@ -0,0 +1,9 @@
package dev.inmo.postssystem.services.posts.common
import dev.inmo.postssystem.features.content.common.Content
import kotlinx.serialization.Serializable
@Serializable
data class FullNewPost(
val content: List<Content>
)

View File

@ -0,0 +1,3 @@
package dev.inmo.postssystem.services.posts.common
interface PostsService : ReadPostsService, WritePostsService

View File

@ -0,0 +1,7 @@
package dev.inmo.postssystem.services.posts.common
import dev.inmo.micro_utils.repos.ReadCRUDRepo
import dev.inmo.postssystem.features.posts.common.PostId
import dev.inmo.postssystem.features.posts.common.RegisteredPost
interface ReadPostsService : ReadCRUDRepo<RegisteredPost, PostId>

View File

@ -0,0 +1,16 @@
package dev.inmo.postssystem.services.posts.common
import dev.inmo.micro_utils.common.Either
import dev.inmo.postssystem.features.content.common.Content
import dev.inmo.postssystem.features.content.common.ContentId
import dev.inmo.postssystem.features.posts.common.PostId
import dev.inmo.postssystem.features.posts.common.RegisteredPost
interface WritePostsService {
suspend fun create(newPost: FullNewPost): RegisteredPost?
suspend fun update(
postId: PostId,
content: List<Either<ContentId, Content>>
): RegisteredPost?
suspend fun remove(postId: PostId)
}

View File

@ -14,6 +14,8 @@ kotlin {
api project(":postssystem.features.content.server") api project(":postssystem.features.content.server")
api project(":postssystem.features.posts.server") api project(":postssystem.features.posts.server")
api project(":postssystem.features.users.server") api project(":postssystem.features.users.server")
api "dev.inmo:micro_utils.common:$microutils_version"
api "org.jetbrains.kotlinx:kotlinx-serialization-properties:$kotlin_serialisation_core_version"
} }
} }
} }

View File

@ -0,0 +1,49 @@
package dev.inmo.postssystem.services.posts.server
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.repos.create
import dev.inmo.micro_utils.repos.deleteById
import dev.inmo.postssystem.features.content.common.Content
import dev.inmo.postssystem.features.content.common.ContentId
import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage
import dev.inmo.postssystem.features.posts.common.*
import dev.inmo.postssystem.features.posts.server.ServerPostsStorage
import dev.inmo.postssystem.services.posts.common.FullNewPost
import dev.inmo.postssystem.services.posts.common.WritePostsService
class DefaultWritePostsService(
private val postsStorage: ServerPostsStorage,
private val contentStorage: ServerContentStorage<Content>
) : WritePostsService {
override suspend fun create(newPost: FullNewPost): RegisteredPost? {
val contentIds = contentStorage.create(newPost.content).map { it.id }
return postsStorage.create(NewPost(contentIds)).firstOrNull()
}
override suspend fun update(postId: PostId, content: List<Either<ContentId, Content>>): RegisteredPost? {
if (!postsStorage.contains(postId)) {
return null
}
val newContent = content.mapNotNull {
when (it) {
is EitherFirst -> {
it.t1
}
is EitherSecond -> {
contentStorage.create(it.t2).firstOrNull() ?.id
}
}
}
return postsStorage.update(
postId,
NewPost(newContent)
)
}
override suspend fun remove(postId: PostId) {
return postsStorage.deleteById(postId)
}
}

View File

@ -0,0 +1,61 @@
package dev.inmo.postssystem.services.posts.server
import dev.inmo.micro_utils.common.FileName
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.ktor.common.StandardKtorSerialFormat
import dev.inmo.micro_utils.ktor.common.decodeHex
import dev.inmo.postssystem.features.content.common.ContentSerializer
import dev.inmo.postssystem.services.posts.common.FullNewPost
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.http.content.PartData
import io.ktor.request.receiveMultipart
import io.ktor.util.asStream
import io.ktor.util.pipeline.PipelineContext
import io.ktor.utils.io.core.use
suspend fun PipelineContext<Unit, ApplicationCall>.downloadFullNewPost(
serialFormat: StandardKtorSerialFormat
): FullNewPost {
val multipart = call.receiveMultipart()
val map = mutableMapOf<String, Any>()
var part = multipart.readPart()
while (part != null) {
val name = part.name
val capturedPart = part
when {
name == null -> {}
capturedPart is PartData.FormItem -> {
map[name] = serialFormat.decodeHex(
ContentSerializer,
capturedPart.value
)
}
capturedPart is PartData.FileItem -> {
val filename = FileName(capturedPart.originalFileName ?: error("File name is unknown for default part"))
val resultInput = MPPFile.createTempFile(
filename.nameWithoutExtension.let {
var resultName = it
while (resultName.length < 3) {
resultName += "_"
}
resultName
},
".${filename.extension}"
).apply {
outputStream().use { fileStream ->
capturedPart.provider().asStream().copyTo(fileStream)
}
}
}
else -> {}
}
part = multipart.readPart()
}
}

View File

@ -0,0 +1,211 @@
package dev.inmo.postssystem.services.posts.server
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.ktor.common.decodeHex
import dev.inmo.micro_utils.ktor.server.*
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
import dev.inmo.micro_utils.mime_types.findBuiltinMimeType
import dev.inmo.micro_utils.repos.ktor.common.crud.createRouting
import dev.inmo.micro_utils.repos.ktor.common.crud.updateRouting
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.removeRoute
import dev.inmo.micro_utils.repos.ktor.server.crud.configureReadStandardCrudRepoRoutes
import dev.inmo.postssystem.features.content.common.*
import dev.inmo.postssystem.features.posts.common.*
import dev.inmo.postssystem.services.posts.common.*
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.auth.authenticate
import io.ktor.http.content.PartData
import io.ktor.request.isMultipart
import io.ktor.request.receiveMultipart
import io.ktor.routing.*
import io.ktor.util.asStream
import io.ktor.util.pipeline.PipelineContext
import io.ktor.utils.io.core.use
import io.ktor.utils.io.streams.asInput
import kotlinx.serialization.builtins.*
class ServerPostsServiceRoutingConfigurator(
private val readPostsService: ReadPostsService,
private val writePostsService: WritePostsService? = readPostsService as? WritePostsService,
private val unifiedRouter: UnifiedRouter
) : ApplicationRoutingConfigurator.Element {
private val contentEitherSerializer = EitherSerializer(ContentId.serializer(), ContentSerializer)
private val contentsEitherSerializer = ListSerializer(contentEitherSerializer)
private val contentsSerializer = ListSerializer(ContentSerializer)
private suspend fun PipelineContext<Unit, ApplicationCall>.receiveContents(): List<Content> {
return unifiedRouter.run {
if (call.request.isMultipart()) {
val multipart = call.receiveMultipart()
val list = mutableListOf<Pair<String, Content>>()
var part = multipart.readPart()
while (part != null) {
val name = part.name
val capturedPart = part
when {
name == null -> {}
capturedPart is PartData.FormItem -> {
list.add(
name to unifiedRouter.serialFormat.decodeHex(
ContentSerializer,
capturedPart.value
)
)
}
capturedPart is PartData.FileItem -> {
val filename = capturedPart.originalFileName ?.let(::FileName) ?: error("File name is unknown for default part")
val mimeType = capturedPart.contentType ?.let {
findBuiltinMimeType("${it.contentType}/${it.contentSubtype}")
} ?: error("File type is unknown for default part")
val resultInput = MPPFile.createTempFile(
filename.nameWithoutExtension.let {
var resultName = it
while (resultName.length < 3) {
resultName += "_"
}
resultName
},
".${filename.extension}"
).apply {
outputStream().use { fileStream ->
capturedPart.provider().asStream().copyTo(fileStream)
}
}
list.add(
name to BinaryContent(
filename,
mimeType
) { resultInput.inputStream().asInput() }
)
}
else -> {}
}
part = multipart.readPart()
}
list.sortedBy { it.first }.map { it.second }
} else {
uniload(contentsSerializer)
}
}
}
private suspend fun PipelineContext<Unit, ApplicationCall>.receiveContentsEithers(): List<Either<ContentId, Content>> {
return unifiedRouter.run {
if (call.request.isMultipart()) {
val multipart = call.receiveMultipart()
val list = mutableListOf<Pair<String, Either<ContentId, Content>>>()
var part = multipart.readPart()
while (part != null) {
val name = part.name
val capturedPart = part
when {
name == null -> {}
capturedPart is PartData.FormItem -> {
list.add(
name to unifiedRouter.serialFormat.decodeHex(
contentEitherSerializer,
capturedPart.value
)
)
}
capturedPart is PartData.FileItem -> {
val filename = capturedPart.originalFileName ?.let(::FileName) ?: error("File name is unknown for default part")
val mimeType = capturedPart.contentType ?.let {
findBuiltinMimeType("${it.contentType}/${it.contentSubtype}")
} ?: error("File type is unknown for default part")
val resultInput = MPPFile.createTempFile(
filename.nameWithoutExtension.let {
var resultName = it
while (resultName.length < 3) {
resultName += "_"
}
resultName
},
".${filename.extension}"
).apply {
outputStream().use { fileStream ->
capturedPart.provider().asStream().copyTo(fileStream)
}
}
list.add(
name to BinaryContent(
filename,
mimeType
) { resultInput.inputStream().asInput() }.either()
)
}
else -> {}
}
part = multipart.readPart()
}
list.sortedBy { it.first }.map { it.second }
} else {
uniload(contentsEitherSerializer)
}
}
}
override fun Route.invoke() {
authenticate {
route(postsRootPath) {
configureReadStandardCrudRepoRoutes(
readPostsService,
RegisteredPost.serializer(),
RegisteredPost.serializer().nullable,
PostId.serializer(),
unifiedRouter
)
writePostsService ?.let {
unifiedRouter.apply {
post(createRouting) {
val data = receiveContents()
unianswer(
RegisteredPost.serializer().nullable,
writePostsService.create(FullNewPost(data))
)
}
post(updateRouting) {
val postId = call.decodeUrlQueryValueOrSendError(postsPostIdParameter, PostId.serializer()) ?: return@post
val data = receiveContentsEithers()
unianswer(
RegisteredPost.serializer().nullable,
writePostsService.update(
postId,
data
)
)
}
post(removeRoute) {
val postId = uniload(PostId.serializer())
unianswer(
Unit.serializer(),
writePostsService.remove(postId)
)
}
}
}
}
}
}
}