Search code examples
kotlinfile-uploadmultipartktorktor-client

check whether all parameter exist or not in multipart request body with ktor


I am trying to create a multipart request with ktor, whose code is as follows,

import com.firstapp.modal.response.SuccessResponse
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart
import io.ktor.http.content.streamProvider
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.request.isMultipart
import io.ktor.request.receive
import io.ktor.request.receiveMultipart
import io.ktor.response.respond
import io.ktor.routing.Route
import io.ktor.util.getOrFail
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.lang.IllegalArgumentException

@Location("/uploadVideo/{title}")
class UploadVideo(val title:String)

fun Route.upload(uploadDir: File) {

    post<UploadVideo> {
        val multipart = call.receiveMultipart()
        var videoFile: File? = null

        // Processes each part of the multipart input content of the user
        multipart.forEachPart { part ->
            when (part) {
                is PartData.FormItem -> {
                    if (part.name != "title")
                        throw IllegalArgumentException("Title parameter not found")
                    //title = part.value
                }
                is PartData.FileItem -> {
                    if (part.name != "file")
                        throw IllegalArgumentException("file parameter not found")

                    val ext = File(part.originalFileName).extension
                    val file = File(uploadDir, "upload-${System.currentTimeMillis()}-${call.parameters.getOrFail("title").hashCode()}.$ext")
                    part.streamProvider().use { input -> file.outputStream().buffered().use { output -> input.copyToSuspend(output) } }
                    videoFile = file
                }
            }

            part.dispose()
        }

        call.respond(
            HttpStatusCode.OK,
            SuccessResponse(
                videoFile!!,
                HttpStatusCode.OK.value,
                "video file stored"
            )
        )
    }
}


suspend fun InputStream.copyToSuspend(
    out: OutputStream,
    bufferSize: Int = DEFAULT_BUFFER_SIZE,
    yieldSize: Int = 4 * 1024 * 1024,
    dispatcher: CoroutineDispatcher = Dispatchers.IO
): Long {
    return withContext(dispatcher) {
        val buffer = ByteArray(bufferSize)
        var bytesCopied = 0L
        var bytesAfterYield = 0L
        while (true) {
            val bytes = read(buffer).takeIf { it >= 0 } ?: break
            out.write(buffer, 0, bytes)
            if (bytesAfterYield >= yieldSize) {
                yield()
                bytesAfterYield %= yieldSize
            }
            bytesCopied += bytes
            bytesAfterYield += bytes
        }
        return@withContext bytesCopied
    }
}

The above code or rest api is working fine but the issue is that, i want to check, whether all parameters are available or not i.e. i want to send additional parameters along with file in format as follows,

class VideoDetail(val type: String, val userId: String, val userName: String)

I am giving here an example, what i want i.e.

post("/") { request ->
    val requestParamenter = call.receive<UserInsert>()
}

here, whatever the paramenters, we pass will automatically get converted into pojo classes, and if we hadn't passed it, it will throw exception,

So, the similar thing i want to achieve with multipart.


Solution

  • Finally, i was able to sort the issue, below is the code,

     @Location("/uploadVideo/{id}")
    class UploadVideo(val id: Int)
    
    fun Route.upload(uploadDir: File) {
    
        post<UploadVideo> {
    
            val multipart = call.receiveMultipart().readAllParts()
            val multiMap = multipart.associateBy { it.name }.toMap()
            val data = PersonForm(multiMap)
            println(data)
            
            val ext = File(data.file.originalFileName).extension
            val file = File(uploadDir, "upload-${System.currentTimeMillis()}-${data.file.originalFileName}")
            data.file.streamProvider()
                .use { input -> file.outputStream().buffered().use { output -> input.copyToSuspend(output) } }
    
            call.respond(
                HttpStatusCode.OK,
                SuccessResponse(
                    file,
                    HttpStatusCode.OK.value,
                    "video file stored"
                )
            )
        }
    }
    
    suspend fun InputStream.copyToSuspend(
        out: OutputStream,
        bufferSize: Int = DEFAULT_BUFFER_SIZE,
        yieldSize: Int = 4 * 1024 * 1024,
        dispatcher: CoroutineDispatcher = Dispatchers.IO
    ): Long {
        return withContext(dispatcher) {
            val buffer = ByteArray(bufferSize)
            var bytesCopied = 0L
            var bytesAfterYield = 0L
            while (true) {
                val bytes = read(buffer).takeIf { it >= 0 } ?: break
                out.write(buffer, 0, bytes)
                if (bytesAfterYield >= yieldSize) {
                    yield()
                    bytesAfterYield %= yieldSize
                }
                bytesCopied += bytes
                bytesAfterYield += bytes
            }
            return@withContext bytesCopied
        }
    }
    
    class PersonForm(map: Map<String?, PartData>) {
        val file: PartData.FileItem by map
        val type: PartData.FormItem by map
        val title: PartData.FormItem by map
    
        override fun toString() = "${file.originalFileName}, ${type.value}, ${title.value}"
    }
    

    The only issue with this approach is by using map delegation, you have to access, the propertly to know, whether all the parameters are present in the map or not, i.e.

     val data = PersonForm(multiMap)
            println(data)