142 lines
4.4 KiB
Markdown
142 lines
4.4 KiB
Markdown
+++
|
|
template = "article.html"
|
|
title = "Serving static files with Ktor"
|
|
date = 2022-09-03T19:00:00+10:00
|
|
description = "A guide to serving static files with Ktor, including configuration for default files, HTML minification, and proper MIME types."
|
|
|
|
[taxonomies]
|
|
tags = ["kotlin", "ktor", "web"]
|
|
+++
|
|
|
|
Serving static files with [Ktor](https://ktor.io) for a static website seems easy enough:
|
|
|
|
```kotlin
|
|
fun main() {
|
|
embeddedServer(Netty, port = 8080) {
|
|
routing {
|
|
static {
|
|
files("static-data")
|
|
}
|
|
}
|
|
}.start(wait = true)
|
|
}
|
|
```
|
|
|
|
Done? Not quite. Opening [http://localhost:8080](http://localhost:8080) will only get you a 404,
|
|
even if you have an `index.html` file in the `static-data` folder. You have to let Ktor know you
|
|
want to opt-in serving a default file. Let's try again (omitting anything outside the `static`
|
|
block):
|
|
|
|
```kotlin
|
|
static {
|
|
files("static-data")
|
|
default("static-data/index.html")
|
|
}
|
|
```
|
|
|
|
Surely now this works? Well, kind of. The root folder will have a default `index.html`, but sadly if
|
|
you have any sub-folders with `index.html` files inside, they won't be served by default - that
|
|
`default()` function only works for the top-level folder. Let's work around that.
|
|
|
|
Let's store our `static-data` directory into a `File` variable, replace the `files` function call by
|
|
our own extension. Note that this extension will handle the fallback to `index.html` in all cases,
|
|
so we can also remove the call to `default()`:
|
|
|
|
```kotlin
|
|
val staticData = File("/path/to/static-data")
|
|
|
|
static {
|
|
filesWithDefaultIndex(staticData)
|
|
}
|
|
|
|
fun Route.filesWithDefaultIndex(dir: File) {
|
|
}
|
|
```
|
|
|
|
In there, we'll need a `get` handler:
|
|
|
|
```kotlin
|
|
fun Route.filesWithDefaultIndex(dir: File) {
|
|
get("{static_path...}") {}
|
|
}
|
|
```
|
|
|
|
This will catch anything inside the static path, and let us access the requested path through
|
|
`call.parameters.getAll("static_path")`. This gets us a list of URL segments that we need to join
|
|
with `File.separator`:
|
|
|
|
```kotlin
|
|
val relativePath = call.parameters
|
|
.getAll("static_path")
|
|
?.joinToString(File.separator) ?: return@get
|
|
```
|
|
|
|
Now, we have three options:
|
|
|
|
- The requested path points to an existing file. Great! We can just return it.
|
|
- The requested path points to a folder. If there's an `index.html` file in there, let's serve it.
|
|
- In any other case (the requested path doesn't exist, or points to a folder without an `index.html`
|
|
file), we let Ktor handle that (most likely returning a 404).
|
|
|
|
We can easily access the corresponding local file, and its fallback if it ends up being a directory:
|
|
|
|
```kotlin
|
|
val combinedDir = staticRootFolder?.resolve(dir) ?: dir
|
|
val file = combinedDir.combineSafe(relativePath)
|
|
val fallbackFile = file.combineSafe("index.html")
|
|
```
|
|
|
|
Now that we have all those defined, here's how we take the decision on what to serve:
|
|
|
|
```kotlin
|
|
val localFile = when {
|
|
file.isFile -> file
|
|
file.isDirectory && fallbackFile.isFile -> fallbackFile
|
|
else -> return@get
|
|
}
|
|
```
|
|
|
|
And finally, we serve the file:
|
|
|
|
```kotlin
|
|
call.respond(LocalFileContent(localFile, ContentType.defaultForFile(localFile)))
|
|
```
|
|
|
|
Great! Let's see what this looks like all together:
|
|
|
|
```kotlin
|
|
fun Route.filesWithDefaultIndex(dir: File) {
|
|
val combinedDir = staticRootFolder?.resolve(dir) ?: dir
|
|
get("{static_path...}") {
|
|
val relativePath = call.parameters
|
|
.getAll("static_path")
|
|
?.joinToString(File.separator) ?: return@get
|
|
val file = combinedDir.combineSafe(relativePath)
|
|
val fallbackFile = file.combineSafe("index.html")
|
|
|
|
val localFile = when {
|
|
file.isFile -> file
|
|
file.isDirectory && fallbackFile.isFile -> fallbackFile
|
|
else -> return@get
|
|
}
|
|
|
|
call.respond(LocalFileContent(localFile, ContentType.defaultForFile(localFile)))
|
|
}
|
|
}
|
|
```
|
|
|
|
Now if you try to open [http://localhost:8080](http://localhost:8080) or
|
|
[http://localhost:8080/foo](http://localhost:8080/foo), both should work (assuming there's
|
|
`index.html` files both at the root and in a folder named `foo`). But if you open
|
|
[http://localhost:8080/foo/](http://localhost:8080/foo/), with a trailing slash, it still doesn't
|
|
work. That's because Ktor handles the two routes (with and without trailing slash) differently (for
|
|
good reasons, see [here](https://youtrack.jetbrains.com/issue/KTOR-372) for some context).
|
|
|
|
In our case, that's not what we want. Luckily, Ktor comes with a plug-in to address this. All that's
|
|
needed is to install it like any other plugin:
|
|
|
|
```kotlin
|
|
install(IgnoreTrailingSlash)
|
|
```
|
|
|
|
And now everything should work as expected.
|