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 = provider().toInputStream().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.
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 …
Did you create the directory in your project?, right click on the root project directory and select new > directory.
Create ‘static’ then inside it create another one and name it ‘fruit_pictures’.
Yes I have those in Intellij. Should that work just on local host with postman?
Yes it should work if the path exists, the call
folder.mkdir()
will try to make the folder but will fail if the parent folder does not exist, you can try to add a check to see if the parent folder exists in the save function like this:this will create the parent directory if it does not already exist.
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 […]