FileCallback.kt

package com.hexagonkt.http.server.callbacks

import com.hexagonkt.core.logging.Logger
import com.hexagonkt.core.media.mediaTypeOfOrNull
import com.hexagonkt.core.require
import com.hexagonkt.http.model.ContentType
import com.hexagonkt.http.handlers.HttpContext
import java.io.File

/**
 * Callback that resolves requests' path parameters to files based on a base file. Requests path
 * parameters are not allowed to contain `..` (references to [file] parent directories are not
 * permitted).
 *
 * If request does not have path parameters [file] will be returned (or not found if [file] is a
 * directory).
 *
 * @param file Base file used to resolve paths passed on the request.
 */
class FileCallback(private val file: File) : (HttpContext) -> HttpContext {

    constructor(file: String) : this(File(file))

    private companion object {
        val logger: Logger = Logger(FileCallback::class)
    }

    override fun invoke(context: HttpContext): HttpContext {
        val file = when (context.pathParameters.size) {
            0 -> file.absoluteFile
            1 -> {
                val relativePath = context.pathParameters.require("0")
                check(!relativePath.contains("..")) {
                    "Requested path cannot contain '..': $relativePath"
                }
                file.resolve(relativePath).absoluteFile
            }
            else -> error("File loading require a single path parameter or none")
        }
        val fileName = file.name

        logger.debug { "Resolving file: $fileName" }

        return if (file.exists() && file.isFile) {
            val bytes = file.readBytes()
            val mediaType = mediaTypeOfOrNull(file)
            context.ok(bytes, contentType = mediaType?.let { ContentType(it) })
        }
        else {
            context.notFound("File '$fileName' not found")
        }
    }
}