看了 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(", ")
準備好 bestCompressionFit
和 cacheControlValues
之後,就準備要處理檔案了
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
得到的禁止存取名單。
希望看完今天的程式段落,各位讀者會有所收穫,我們明天見!