Ktor server for beginners – Uploading Files

Level: Beginner

In lesson 3 we took a look at how to save and perform different queries on our MongoDB now that we have decent data about our fruits how about we add some pictures :D!

Preparing

First let’s modify our Fruit data class to hold an image url, because we already had some fruits in our database we want this field to be nullable meaning that for all fruits that are already out there it will return null for the image url.

@Serializable
data class Fruit(
    val name: String,
    val season: Season = Season.Unknown,
    val countries: List<String> = emptyList(),
    @BsonId
    val id: String = ObjectId().toString(),
    val image: String?
){
    enum class Season {
        Spring, Winter, Summer, Autumn, Unknown
    }
}

its up to you where you would like to save the image files for this tutorial we are going to create a directory in the root of the project called static > fruit_pictures .

in our project’s working directory create a new package called utils in this package create a new Kotlin Object called Constants in this object we will save the base url and the path to our pictures

object Constants {
    const val BASE_URL = "localhost:8080"
    const val STATIC_ROOT = "static/"
    const val FRUIT_IMAGE_DIRECTORY = "fruit_pictures/"
    const val FRUIT_IMAGE_PATH = "$STATIC_ROOT/$FRUIT_IMAGE_DIRECTORY"
    const val EXTERNAL_FRUIT_IMAGE_PATH = "/images"
}

Receiving a file

When we send data over the internet in a POST request, we can do it in different ways. One common way is to send data in the request body as plain text, like we do in a normal body. However, sometimes we want to send more complex data, like a file or multiple values at once. don’t worry the solution is simple, we can easily do that with MultipartData, this form allows us to send multiple types of data in a single request so we can send our fruit data and an image with it.

A multipart body is a way to send more than one piece of data in a single request. Instead of just sending plain text in the body, we can send multiple parts, each with its own data. For example, we could send an image file and some text data, like a title or description, all in one request. Each part has its own headers, which describe the type of data and other information, and a body that contains the actual data.

In our FruitRoutes we modify our add-fruit end point to receive a multipart data like

    post("/add-fruit") {
        try {

            // receive multipart data from the client
            val multipart = call.receiveMultipart()

            // define variable to hold our parameters data
            var fileName: String? = null
            var name: String? = null
            var season: String? = null
            val countries: MutableList<String> = mutableListOf()
            var imageUrl: String? = null

            try {
                // loop through each part of our multipart
                multipart.forEachPart { partData ->
                    when (partData) {
                        is PartData.FormItem -> {
                            // to read parameters that we sent with the image
                            when (partData.name) {
                                "name" -> name = partData.value
                                "season" -> season = partData.value
                                "countries" -> countries.add(partData.value)
                            }
                        }

                        is PartData.FileItem -> {
                            // to read the image data we call the 'save' utility function passing our path
                            if (partData.name == "image") {
                                fileName = partData.save(Constants.FRUIT_IMAGE_PATH)
                                imageUrl = "${Constants.BASE_URL}${Constants.EXTERNAL_FRUIT_IMAGE_PATH}/$fileName"
                            }
                        }

                        else -> Unit
                    }
                }
            } catch (ex: Exception) {
                // something went wrong with the image part, delete the file
                File("${Constants.FRUIT_IMAGE_PATH}/$fileName").delete()
                ex.printStackTrace()
                call.respond(HttpStatusCode.InternalServerError, "Error")
            }
            // create a new fruit object using data we collected above
            val newFruit = Fruit(
                name = name!!,
                // the valueOf function will find the enum class type that matches this string and return it, an Exception is thrown if the string does not match any type
                season = Fruit.Season.valueOf(season!!),
                countries = countries,
                image = imageUrl
            )
            // add the received fruit to the database
            if (!addFruit(newFruit)) {
                // if not added successfully return with an error
                return@post call.respond(
                    HttpStatusCode.Conflict,
                    SimpleResponse(success = false, message = "Item already exits")
                )
            }

            // acknowledge that we successfully added the fruit by responding
            call.respond(HttpStatusCode.Created, newFruit)
        } catch (ex: Exception) {

            call.respond(HttpStatusCode.BadRequest, SimpleResponse(false, "Invalid data"))
        }

    }

When the server receives a multipart request, it can parse the request body and extract each part separately. This allows us to handle more complex data, like files, in a POST request.

in the code above we use two types of parts from the multipart data first is PartData which means we will access the parameters like name, season and countries, the second one is FileItem which is the image file itself we then use an extension function save to read the byte data and create a random name for the file and write the byte data to the new file, for convenience we keep this function in our utils package.

import io.ktor.http.content.*
import java.io.File
import java.util.*

fun PartData.FileItem.save(path: String): String {
    // read the file bytes
    val fileBytes = streamProvider().readBytes()
    // find the file extension eg: .jpg
    val fileExtension = originalFileName?.takeLastWhile { it != '.' }
    // generate a random name for the new file and append the file extension
    val fileName = UUID.randomUUID().toString() + "." + fileExtension
    // create our new file in the server
    val folder = File(path)
    // create parent directory if not exits
    if (!folder.parentFile.exists()) {
        folder.parentFile.mkdirs()
    }
    // continue with creating our new file
    folder.mkdir()
    // write bytes to our newly created file
    File("$path$fileName").writeBytes(fileBytes)
    return fileName
}

Now let’s try to make a request!

in postman instead of using body we use form data to send our data and we fill in the parameters in key value pairs like below, for the image we change the value type from text to file like in the screenshot below.

Congrats!

now try holding ctrl and clicking on the link, don’t worry if the image doesn’t load we’ll get to it in the next step.

Enabling static content

So far we should be able to send the image data to client when they request it via a specific end point but We want to be able to access the images directly via a URL since that’s a common use case, to do that we need to enable serving of static content in Ktor.

static content refers to files that are served directly by the server, without any processing or modification. This could include files like images, CSS stylesheets, JavaScript files, or HTML files.

Static content is typically stored on disk, either within the application itself or in a separate directory. When a client requests a static file, the server simply reads the file from disk and sends it back to the client as-is, without any modification or processing.

to do this simply go to Routing.kt and add the following inside the routing block

        static("/") {
            // sets the base route for static routes in this block, in other words all static blocks here will start at "static/fruit_pictures/" by default instead of project root
            staticRootFolder = File("static/")

            // the path the client will use to access files: /images
            static("/images"){
                
                // serve all files in fruit_pictures as static content under /images
                files("fruit_pictures/")
            }
        }

the staticRootFolder defines the default folder for relative files calculations for static content, files(“fruit_pictures”) would allow for any file located in the fruit_pictures folder to be served as static content under the given URL pattern, which in this case is images.

This means that a request to /images/banana.jpg would serve the static/fruit_pictures/banana.jpg file.

Here is the code again but this time we use Constants to avoid misspelling anything and avoid possible errors.

static("/") {
            // sets the base route for static routes in this block, in other words all static blocks here will start at "static/fruit_pictures/" by default instead of project root
            staticRootFolder = File(Constants.STATIC_ROOT)

            // the path the client will use to access files: /images
            static(Constants.EXTERNAL_FRUIT_IMAGE_PATH){

                // serve all files in fruit_pictures as static content under /images
                files(Constants.FRUIT_IMAGE_DIRECTORY)
            }
        }

Great job so far! you can ask any questions in the comments below and leave a rating if you like ;), the source code for this lesson can be found here.

5 1 vote
Article Rating
Subscribe
Notify of
guest

6 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Sam

thank you!, exactly what I was looking for and well explained. I must be doing something wrong with the directories as I’m getting:
java.io.FileNotFoundException: static\fruit_pictures\b327979f-0726-4784-8d5b-d985393b026f.jpg (The system cannot find the path specified)
on localhost with postman. I’ll continue trying …

Sam

Yes I have those in Intellij. Should that work just on local host with postman?

Sam

ok… I had folder set up wrong (I’m a real beginner!) thanks again for everything – this is a big help and will allow me to play around with some ideas.

[…] the previous lesson we learned how to store files and provide them as static content, at the moment anyone with access […]