Migrate from Bazel
This commit is contained in:
commit
016dbd0814
59 changed files with 7044 additions and 0 deletions
142
content/posts/serving-static-files-with-ktor.md
Normal file
142
content/posts/serving-static-files-with-ktor.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
+++
|
||||
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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue