From fb1ed2b9a2478752310b4af47a49f50cc4c0f8c2 Mon Sep 17 00:00:00 2001 From: Lanius Trolling Date: Tue, 17 Dec 2024 22:08:16 -0500 Subject: [PATCH] Fix file uploads --- .../kotlin/info/mechyrdia/data/ViewsFiles.kt | 10 ++-- .../info/mechyrdia/route/ResourceMultipart.kt | 59 ++++++++++++++++--- .../info/mechyrdia/route/ResourceTypes.kt | 4 +- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/info/mechyrdia/data/ViewsFiles.kt b/src/main/kotlin/info/mechyrdia/data/ViewsFiles.kt index c10039c..dc1eb49 100644 --- a/src/main/kotlin/info/mechyrdia/data/ViewsFiles.kt +++ b/src/main/kotlin/info/mechyrdia/data/ViewsFiles.kt @@ -5,6 +5,7 @@ import info.mechyrdia.lore.adminPage import info.mechyrdia.lore.dateTime import info.mechyrdia.lore.mapSuspend import info.mechyrdia.lore.redirectHref +import info.mechyrdia.route.MultiPartPayloadPart import info.mechyrdia.route.Root import info.mechyrdia.route.href import info.mechyrdia.route.installCsrfToken @@ -282,19 +283,18 @@ suspend fun ApplicationCall.adminDoCopyFile(from: StoragePath, into: StoragePath respond(HttpStatusCode.Conflict) } -suspend fun ApplicationCall.adminUploadFile(path: StoragePath, part: PartData.FileItem) { +suspend fun ApplicationCall.adminUploadFile(path: StoragePath, part: MultiPartPayloadPart.FileData) { val name = part.originalFileName ?: throw MissingRequestParameterException("originalFileName") val filePath = path / name - val content = part.provider().toByteArray() - if (FileStorage.instance.writeFile(filePath, content)) + if (FileStorage.instance.writeFile(filePath, part.contents)) redirectHref(Root.Admin.Vfs.View(filePath.elements), HttpStatusCode.SeeOther) else respond(HttpStatusCode.Conflict) } -suspend fun ApplicationCall.adminOverwriteFile(path: StoragePath, part: PartData.FileItem) { - if (FileStorage.instance.writeFile(path, part.provider().toByteArray())) +suspend fun ApplicationCall.adminOverwriteFile(path: StoragePath, part: MultiPartPayloadPart.FileData) { + if (FileStorage.instance.writeFile(path, part.contents)) redirectHref(Root.Admin.Vfs.View(path.elements), HttpStatusCode.SeeOther) else respond(HttpStatusCode.Conflict) diff --git a/src/main/kotlin/info/mechyrdia/route/ResourceMultipart.kt b/src/main/kotlin/info/mechyrdia/route/ResourceMultipart.kt index 6675550..2257e5d 100644 --- a/src/main/kotlin/info/mechyrdia/route/ResourceMultipart.kt +++ b/src/main/kotlin/info/mechyrdia/route/ResourceMultipart.kt @@ -1,12 +1,17 @@ package info.mechyrdia.route +import io.ktor.http.ContentDisposition +import io.ktor.http.ContentType +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders import io.ktor.http.content.MultiPartData import io.ktor.http.content.PartData import io.ktor.http.content.forEachPart +import io.ktor.utils.io.toByteArray import kotlin.reflect.full.companionObjectInstance interface MultiPartPayload : AutoCloseable { - val payload: List + val payload: List override fun close() { for (data in payload) @@ -23,19 +28,51 @@ inline fun payloadProcessor(): MultiPartPayloadPr return P::class.companionObjectInstance as MultiPartPayloadProcessor

} +sealed class MultiPartPayloadPart { + abstract val headers: Headers + abstract fun dispose() + + val contentDisposition: ContentDisposition? + get() = headers[HttpHeaders.ContentDisposition]?.let { ContentDisposition.parse(it) } + + val contentType: ContentType? + get() = headers[HttpHeaders.ContentType]?.let { ContentType.parse(it) } + + val name: String? + get() = contentDisposition?.name + + class FormData(val value: String, override val headers: Headers, private val disposer: () -> Unit) : MultiPartPayloadPart() { + override fun dispose() { + disposer() + } + } + + class FileData(val contents: ByteArray, override val headers: Headers, private val disposer: () -> Unit) : MultiPartPayloadPart() { + override fun dispose() { + disposer() + } + + val originalFileName: String? = contentDisposition?.parameter(ContentDisposition.Parameters.FileName) + } +} + data class CsrfProtectedMultiPartPayload( override val csrfToken: String? = null, - override val payload: List + override val payload: List ) : CsrfProtectedResourcePayload, MultiPartPayload { companion object : MultiPartPayloadProcessor { override suspend fun process(data: MultiPartData): CsrfProtectedMultiPartPayload { var csrfToken: String? = null - val payload = mutableListOf() + val payload = mutableListOf() data.forEachPart { part -> if (part is PartData.FormItem && part.name == "csrfToken") csrfToken = part.value - else payload.add(part) + else when (part) { + is PartData.FormItem -> MultiPartPayloadPart.FormData(part.value, part.headers, part.dispose) + is PartData.FileItem -> MultiPartPayloadPart.FileData(part.provider().toByteArray(), part.headers, part.dispose) + else -> null + }?.let(payload::add) } return CsrfProtectedMultiPartPayload(csrfToken, payload) @@ -44,12 +81,20 @@ data class CsrfProtectedMultiPartPayload( } data class PlainMultiPartPayload( - override val payload: List + override val payload: List ) : MultiPartPayload { companion object : MultiPartPayloadProcessor { override suspend fun process(data: MultiPartData): PlainMultiPartPayload { - val payload = mutableListOf() - data.forEachPart { part -> payload.add(part) } + val payload = mutableListOf() + + data.forEachPart { part -> + when (part) { + is PartData.FormItem -> MultiPartPayloadPart.FormData(part.value, part.headers, part.dispose) + is PartData.FileItem -> MultiPartPayloadPart.FileData(part.provider().toByteArray(), part.headers, part.dispose) + else -> null + }?.let(payload::add) + } + return PlainMultiPartPayload(payload) } } diff --git a/src/main/kotlin/info/mechyrdia/route/ResourceTypes.kt b/src/main/kotlin/info/mechyrdia/route/ResourceTypes.kt index 58ad73b..3bc18db 100644 --- a/src/main/kotlin/info/mechyrdia/route/ResourceTypes.kt +++ b/src/main/kotlin/info/mechyrdia/route/ResourceTypes.kt @@ -482,7 +482,7 @@ class Root : ResourceHandler, ResourceFilter { with(vfs) { call.filterCall() } with(payload) { call.verifyCsrfToken() } - val fileItem = payload.payload.filterIsInstance().singleOrNull() + val fileItem = payload.payload.filterIsInstance().singleOrNull() ?: throw MissingRequestParameterException("file") call.adminUploadFile(StoragePath(path), fileItem) @@ -495,7 +495,7 @@ class Root : ResourceHandler, ResourceFilter { with(vfs) { call.filterCall() } with(payload) { call.verifyCsrfToken() } - val fileItem = payload.payload.filterIsInstance().singleOrNull() + val fileItem = payload.payload.filterIsInstance().singleOrNull() ?: throw MissingRequestParameterException("file") call.adminOverwriteFile(StoragePath(path), fileItem) -- 2.25.1