ktor icon indicating copy to clipboard operation
ktor copied to clipboard

Static files: Serve "/semantic-name" instead of "/semantic-name.html"

Open loxal opened this issue 6 years ago • 8 comments

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.

loxal avatar Oct 24 '17 11:10 loxal

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)

orangy avatar Oct 24 '17 11:10 orangy

I am using ktor 0.4.0 and got this compile-error

...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.

loxal avatar Oct 24 '17 11:10 loxal

I see. You might need to copy some more code :) We will think about this feature as an out-of-the-box later.

orangy avatar Oct 24 '17 14:10 orangy

This also could be achieved if we implement something similar to mod_rewrite. Unfortunately it's not easy to implement.

cy6erGn0m avatar Oct 29 '17 02:10 cy6erGn0m

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.

oleg-larshin avatar Aug 10 '20 15:08 oleg-larshin

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 avatar Dec 02 '20 14:12 loxal

@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)
}

sickfar avatar Jul 23 '21 09:07 sickfar

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.

loxal avatar Jul 25 '21 19:07 loxal

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 avatar Apr 20 '23 21:04 TWiStErRob

@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

rsinukov avatar Apr 21 '23 10:04 rsinukov

Oh, sorry, it hid between all the other comments :) Note: it's not many issues, you can do it by hand too :)

TWiStErRob avatar Apr 21 '23 10:04 TWiStErRob