iT邦幫忙

2023 iThome 鐵人賽

DAY 8
0

看了 Ktor 預設提供的程式怎麼實作的之後,我們開始看看撰寫其他功能的背後實作。

首先,我們來看看如果要處理靜態檔案, Ktor 要怎麼做。

Ktor 可以使用 staticFiles 這個函數來處理檔案

fun Application.configureRouting() {
    routing {
        staticFiles(remotePath = "/static", dir = File("files"))
    }
}

我們來看看 staticFiles 背後的實作

/**
 * Sets up [Routing] to serve static files.
 * All files inside [dir] will be accessible recursively at "[remotePath]/path/to/file".
 * If the requested file is a directory and [index] is not `null`,
 * then response will be [index] file in the requested directory.
 *
 * If the requested file doesn't exist, or it is a directory and no [index] specified, response will be 404 Not Found.
 *
 * You can use [block] for additional set up.
 */
public fun Route.staticFiles(
    remotePath: String,
    dir: File,
    index: String? = "index.html",
    block: StaticContentConfig<File>.() -> Unit = {}
): Route {
    val staticRoute = StaticContentConfig<File>().apply(block)
    val autoHead = staticRoute.autoHeadResponse
    val compressedTypes = staticRoute.preCompressedFileTypes
    val contentType = staticRoute.contentType
    val cacheControl = staticRoute.cacheControl
    val extensions = staticRoute.extensions
    val modify = staticRoute.modifier
    val exclude = staticRoute.exclude
    val defaultPath = staticRoute.defaultPath
    return staticContentRoute(remotePath, autoHead) {
        respondStaticFile(
            index = index,
            dir = dir,
            compressedTypes = compressedTypes,
            contentType = contentType,
            cacheControl = cacheControl,
            modify = modify,
            exclude = exclude,
            extensions = extensions,
            defaultPath = defaultPath
        )
    }
}

前面進行了許多的設定後,呼叫了 staticContentRoute

private suspend fun ApplicationCall.respondStaticFile(
    index: String?,
    dir: File,
    compressedTypes: List<CompressedFileType>?,
    contentType: (File) -> ContentType,
    cacheControl: (File) -> List<CacheControl>,
    modify: suspend (File, ApplicationCall) -> Unit,
    exclude: (File) -> Boolean,
    extensions: List<String>,
    defaultPath: String?
) {

裡面的內容我們分段閱讀

首先取得相對路徑

val relativePath = parameters.getAll(pathParameterName)?.joinToString(File.separator) ?: return

接著透過相對路徑,搭配檔案資料夾取得檔案完整路徑

val requestedFile = dir.combineSafe(relativePath)

接著利用 Kotlin 的寫法,定義一個僅在這個函數內使用的函數 checkExclude

suspend fun checkExclude(file: File): Boolean {
	if (!exclude(file)) return false
	respond(HttpStatusCode.Forbidden)
	return true
}

這個函數在裡面會用到好幾次。

val isDirectory = requestedFile.isDirectory

if (index != null && isDirectory) {
	respondStaticFile(File(requestedFile, index), compressedTypes, contentType, cacheControl, modify)
}

如果不是資料夾,繼續往下

 else if (!isDirectory) {
	if (checkExclude(requestedFile)) return

	respondStaticFile(requestedFile, compressedTypes, contentType, cacheControl, modify)
	if (isHandled) return
	for (extension in extensions) {
		val fileWithExtension = File("${requestedFile.path}.$extension")
		if (checkExclude(fileWithExtension)) return
		respondStaticFile(fileWithExtension, compressedTypes, contentType, cacheControl, modify)
		if (isHandled) return
	}
}

這邊的 for (extension in extensions) 會試著將給出的副檔名都嘗試過一輪,然後看看能否取到檔案。

如果這樣還沒回傳檔案,就繼續往下

if (isHandled) return
if (defaultPath != null) {
	respondStaticFile(File(dir, defaultPath), compressedTypes, contentType, cacheControl, modify)
}

整個函數到這邊結束。

看到這裡,我們首先注意到 respondStaticFile 出現了許多次

internal suspend fun ApplicationCall.respondStaticFile(
    requestedFile: File,
    compressedTypes: List<CompressedFileType>?,
    contentType: (File) -> ContentType = { ContentType.defaultForFile(it) },
    cacheControl: (File) -> List<CacheControl> = { emptyList() },
    modify: suspend (File, ApplicationCall) -> Unit = { _, _ -> }
)

根據參數名稱,我們推測這邊會處理壓縮、檔案型態、快取。

我們看看實作內容是不是真的如此

val bestCompressionFit = bestCompressionFit(requestedFile, request.acceptEncodingItems(), compressedTypes)

這邊透過 bestCompressionFit 取得 CompressedFileType

internal fun bestCompressionFit(
    file: File,
    acceptEncoding: List<HeaderValue>,
    compressedTypes: List<CompressedFileType>?
): CompressedFileType? {
    val acceptedEncodings = acceptEncoding.map { it.value }.toSet()
    // We respect the order in compressedTypes, not the one on Accept header
    @Suppress("DEPRECATION")
    return compressedTypes
        ?.filter { it.encoding in acceptedEncodings }
        ?.firstOrNull { it.file(file).isFile }
}

接著往下看

val cacheControlValues = cacheControl(requestedFile).joinToString(", ")

準備好 bestCompressionFitcacheControlValues 之後,就準備要處理檔案了

if (bestCompressionFit == null) {
	if (requestedFile.isFile) {
		if (cacheControlValues.isNotEmpty()) response.header(HttpHeaders.CacheControl, cacheControlValues)
		modify(requestedFile, this)
		respond(LocalFileContent(requestedFile, contentType(requestedFile)))
	}
	return
}

這邊採取單行 if 的寫法,省略掉一個大括弧。

如果沒有對應壓縮方式,快取不是空,則從快取取出內容並且回傳。

如果快取為空,則先運行 modify,然後透過 respond 回傳檔案。

這邊的 respond 我們在處理純文字回傳的時候已經看過,只是這邊改成回傳 LocalFileContent

public class LocalFileContent(
    public val file: File,
    override val contentType: ContentType = ContentType.defaultForFile(file)
) : OutgoingContent.ReadChannelContent()

接著往下看

我們先設置了避免回傳再次被壓縮的參數

attributes.put(SuppressionAttribute, true)

然後開始處理回傳

@Suppress("DEPRECATION")
val compressedFile = bestCompressionFit.file(requestedFile)
if (compressedFile.isFile) {
	if (cacheControlValues.isNotEmpty()) response.header(HttpHeaders.CacheControl, cacheControlValues)
	modify(requestedFile, this)
	val localFileContent = LocalFileContent(compressedFile, contentType(requestedFile))
	respond(PreCompressedResponse(localFileContent, bestCompressionFit.encoding))
}

前面的流程大同小異,但是最後的 respond 改放入 PreCompressedResponse

internal class PreCompressedResponse(
    private val original: ReadChannelContent,
    private val encoding: String?,
) : OutgoingContent.ReadChannelContent()

到這邊,我們看完了 staticFiles 底層所有的實作,並發現了裡面包含了許多原先沒想到的邏輯!

比方說,從 staticRoute.preCompressedFileTypes 得到的壓縮方式,從 staticRoute.extensions 得到會依次嘗試的副檔名,以及從 staticRoute.exclude 得到的禁止存取名單。

希望看完今天的程式段落,各位讀者會有所收穫,我們明天見!


上一篇
Day 07:call.respondText() 後段:如何使用協程善用資源
下一篇
Day 09:生成 HTML 內容的 call.respondHtml()
系列文
深入解析 Kotlin 專案 Ktor 的程式碼,探索 Ktor 的強大功能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言