ktor
ktor copied to clipboard
Static files: Serve "/semantic-name" instead of "/semantic-name.html"
I want Ktor to drive my website. I have a bunch of HTML files but I want to strip the HTML suffix from those files when they are being served as the HTML suffix is just a technical detail that I want to strip.
There is no built-in functionality like this yet, but it's quite easy to achieve by copying part of static source code (as a workaround):
fun Route.htmlFiles(folder: File) {
val dir = staticRootFolder.combine(folder)
get("{$pathParameterName...}") {
val relativePath = call.parameters.getAll(pathParameterName)?.joinToString(File.separator) ?: return@get
val file = dir.combineSafe(relativePath) + ".html" // added this
if (file.isFile) {
call.respond(LocalFileContent(file))
}
}
}
In general that is interesting request and we might need to introduce some kind of "static files filters" that can locate files with some "map" function (e.g. path -> path + ".html" like here), or even transform a file content while sending (e.g. markdown -> html)
I am using ktor 0.4.0 and got this
...the compile errors are all due to "private in file". This is the file https://github.com/loxal/muctool/blob/master/service/src/main/kotlin/net/loxal/muctool/App.kt where I tried to incorporate your snippet.
I see. You might need to copy some more code :) We will think about this feature as an out-of-the-box later.
This also could be achieved if we implement something similar to mod_rewrite. Unfortunately it's not easy to implement.
Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.
Just wanted to revisit this case after a few years. I there a solution available? I tried to achieve it using Spring Boot filters w/o Ktor but it requires a dee response rewrite. Unfortunately there is not setLocationHeader solution available.
Using Nginx' mod_rewrite could be also a valid approach but then I'd need to split up static files and endpoint logic into different sub-projects which would introduce additional management overhead.
@loxal Checkout this. Yeah, it's dirty copy from ktor, but it works. I will try to make it as a pull request if I have time
The only limitation is dots - those will be recognized as file extensions and will lead to a strict match resolution. But if you use a route without dots in it, then it will try $route.html
and $route/index.html
if first was not successful
const val pathParameterName = "static-content-path-parameter"
private fun String?.combinePackage(resourcePackage: String?) = when {
this == null -> resourcePackage
resourcePackage == null -> this
else -> "$this.$resourcePackage"
}
fun Route.resources(resourcePackage: String? = null, plainRouteToHtmlFile: Boolean = false) {
val packageName = staticBasePackage.combinePackage(resourcePackage)
get("{$pathParameterName...}") {
val path = call.parameters.getAll(pathParameterName) ?: return@get
if (isPlainRoute(path) && plainRouteToHtmlFile) {
serveHtmlFile(call, path, packageName)
} else {
val relativePath = path.joinToString(File.separator)
val content = call.resolveResource(relativePath, packageName)
if (content != null) {
call.respond(content)
}
}
}
}
private fun isPlainRoute(path: List<String>): Boolean {
val fileExtensionRegex = Regex("^.*\\.[^\\\\]+\$")
return !fileExtensionRegex.matches(path.last())
}
private suspend fun serveHtmlFile(call: ApplicationCall, path: List<String>, packageName: String?) {
val content = tryJustHtml(call, path, packageName) ?: tryIndexHtml(call, path, packageName)
if (content != null) {
call.respond(content)
}
}
private fun tryJustHtml(call: ApplicationCall, path: List<String>, packageName: String?): OutgoingContent? {
val lastFile = path.last()
val tryWithHtmlPath = if (path.size > 1) {
path.subList(0, path.lastIndex - 1) + "$lastFile.html"
} else {
listOf("$lastFile.html")
}
val relativePath = tryWithHtmlPath.joinToString(File.separator)
return call.resolveResource(relativePath, packageName)
}
private fun tryIndexHtml(call: ApplicationCall, path: List<String>, packageName: String?): OutgoingContent? {
val tryWithIndexHtmlPath = path + "index.html"
val relativePath = tryWithIndexHtmlPath.joinToString(File.separator)
return call.resolveResource(relativePath, packageName)
}
It's not a priority for me at the moment as I found an nginx-based solution a while ago already but having this feature mainlined would make me switch from my custom nginx implementation. Having this logic inside the application layer and not at the edge simplifies testing greatly.
This is now fixed as https://youtrack.jetbrains.com/issue/KTOR-818 in https://github.com/ktorio/ktor/pull/3443, @rsinukov are there any plans to close these issues when their migrated parts are, or at least add a comment which issue they were moved to?
@TWiStErRob thanks for the heads-up. There are no plans to sync the two, but probably we should at some point. There are comment though on every migrated issue, see https://github.com/ktorio/ktor/issues/233#issuecomment-671424950
Oh, sorry, it hid between all the other comments :) Note: it's not many issues, you can do it by hand too :)