mel4jdev
mel4jdev

Reputation: 23

Serving static resources with Ktor GraalVM

I found example of Ktor GraalVM https://ktor.io/docs/graalvm.html which works perfectly well. But how to serve static resources like images or even more important js (javascript) files?

I have:

    routing {
        get("/") {
            call.respondText("Hello GraalVM!")
            call.application.environment.log.info("Call made to /")
        }

        static("/") {
            resources("img")
        }
    }

The new code is 'static("/")...' part and the question again: Is it possible to serve static content? Of course I still want to have native GraalVM output (executable).

http://0.0.0.0:8080/ works and serves: "Hello GraalVM!" output. But http://0.0.0.0:8080/img/test.png (in resources -> img folder I have a file test.png) returns 404 not found.

Upvotes: 0

Views: 442

Answers (1)

mel4jdev
mel4jdev

Reputation: 23

I've made it work... and found why it is not working with only classical code:

    static("/") {
        staticBasePackage = "static"
        static("img") {
            resources("img")
        }
    }

...but first, fragments of the call, i.e. logs:

>>> url.protocol = resource

and here's the answer ... it normally works as a file, but in native GraalVM this is sought as a resource. Original code:

@InternalAPI
public fun resourceClasspathResource(url: URL, path: String, mimeResolve: (String) -> ContentType): OutgoingContent? {
    return when (url.protocol) {
        "file" -> {
            val file = File(url.path.decodeURLPart())
            if (file.isFile) LocalFileContent(file, mimeResolve(file.extension)) else null
        }
        "jar" -> {
            if (path.endsWith("/")) {
                null
            } else {
                val zipFile = findContainingJarFile(url.toString())
                val content = JarFileContent(zipFile, path, mimeResolve(url.path.extension()))
                if (content.isFile) content else null
            }
        }
        "jrt" -> {
            URIFileContent(url, mimeResolve(url.path.extension()))
        }
        else -> null
    }
}

and here, as you can see, the resource is not supported here. I had to rewrite the code like this:

// "new version" of: io.ktor.server.http.content.StaticContentResolutionKt.resourceClasspathResource
    fun resourceClasspathResourceVersion2(url: URL, path: String, mimeResolve: (String) -> ContentType, classLoader: ClassLoader): OutgoingContent? {
        println(">>> url.protocol = ${url.protocol}")
        return when (url.protocol) {
            "file" -> {
                val file = File(url.path.decodeURLPart())
                println(">>> file = $file")
                if (file.isFile) {
                    val localFileContent = LocalFileContent(file, mimeResolve(file.extension))
                    println(">>> localFileContent = $localFileContent")
                    localFileContent
                } else null
            }
            // ... here are other things which are in original version
            "resource" -> {
                println(">>> in resource")
                val resourceName = url.path.substring(1)
                println(">>> resourceName = $resourceName")
                val resourceAsStream = classLoader.getResourceAsStream(resourceName)
                val cnt = ByteArrayContent(resourceAsStream.readAllBytes(), ContentType.parse("image/gif"), HttpStatusCode.OK)
                cnt
            }
            else -> null
        }
    }

All the code for the Routing class (Routing.kt) looks like this:

package io.ktorgraal.plugins

import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.http.content.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.io.File
import java.net.URL

fun Application.configureRouting() {

    // I use println not some Logger to make it simple... in native also
    // (there are different problems in native compilations with loggers)

    // "new version" of: io.ktor.server.http.content.StaticContentResolutionKt.resourceClasspathResource
    fun resourceClasspathResourceVersion2(url: URL, path: String, mimeResolve: (String) -> ContentType, classLoader: ClassLoader): OutgoingContent? {
        println(">>> url.protocol = ${url.protocol}")
        return when (url.protocol) {
            "file" -> {
                val file = File(url.path.decodeURLPart())
                println(">>> file = $file")
                if (file.isFile) {
                    val localFileContent = LocalFileContent(file, mimeResolve(file.extension))
                    println(">>> localFileContent = $localFileContent")
                    localFileContent
                } else null
            }
            // ... here are other things which are in original version
            "resource" -> {
                println(">>> in resource")
                val resourceName = url.path.substring(1)
                println(">>> resourceName = $resourceName")
                val resourceAsStream = classLoader.getResourceAsStream(resourceName)
                val cnt = ByteArrayContent(resourceAsStream.readAllBytes(), ContentType.parse("image/gif"), HttpStatusCode.OK)
                cnt
            }
            else -> null
        }
    }

    // Starting point for a Ktor app:
    routing {
        get("/") {
            call.respondText("Hello GraalVM!")
            call.application.environment.log.info("Call made to /")
        }

        // not working... so we need below "reimplementation"
        /*static("/") {
            staticBasePackage = "static"
            static("img") {
                resources("img")
            }
        }*/

        // "new version" of: io.ktor.server.http.content.StaticContentKt.resources
        // ... because I need to call different function no resourceClasspathResource, but resourceClasspathResourceVersion2
        get("/{pathParameterName...}") {
            println(">>> this endpoint was called")
            val relativePath = call.parameters.getAll("pathParameterName")?.joinToString(File.separator) ?: return@get
            val mimeResolve: (String) -> ContentType = { ContentType.defaultForFileExtension(it) }
            val classLoader: ClassLoader = application.environment.classLoader
            val normalizedPath = relativePath // here I do not do normalization although in Ktor code such code exists
            println(">>> normalizedPath = $normalizedPath")
            val resources = classLoader.getResources(normalizedPath)
            for (url in resources.asSequence()) {
                println(">>> url = $url")
                resourceClasspathResourceVersion2(url, normalizedPath, mimeResolve, classLoader)?.let { content ->
                    if (content != null) {
                        println(">>> about to return content")
                        call.respond(content)
                    }
                }
            }
        }
    }

}

And here, too, I had to have a "new version" of io.ktor.server.http.content.StaticContentKt.resources not to change the entire library. Then the

http://localhost:8080/static/img/test/test.gif

call works and returns a file.

The whole code change "https://github.com/ktorio/ktor-samples/tree/main/graalvm" is the above listed Routing.kt class. You should also add a file such as gif to resources -> static -> img -> test -> test.gif (nesting only to show the structure).

Upvotes: 0

Related Questions