如何使用Ktor实现类似Apache的目录列表与文件服务?
实现Ktor自定义Apache风格目录列表服务
核心思路
绕过Ktor自带的静态内容服务,手动拦截请求:
- 将请求路径映射到本地文件系统的根目录
- 若请求目标是文件,返回文件内容并自动匹配MIME类型
- 若请求目标是目录,生成带链接的HTML列表页面,模拟Apache的目录展示效果
实现步骤
1. 基础依赖配置
确保build.gradle.kts中包含Ktor核心依赖:
dependencies { implementation("io.ktor:ktor-server-core-jvm") implementation("io.ktor:ktor-server-netty-jvm") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") testImplementation("io.ktor:ktor-server-tests-jvm") }
2. 核心路由逻辑
在Ktor的Application模块中编写处理逻辑,重点处理路径映射、文件/目录判断、响应生成:
import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.response.* import io.ktor.server.routing.* import java.io.File import java.nio.file.Files fun Application.module() { // 定义服务的根目录,可根据实际场景修改 val rootDir = File(System.getProperty("user.dir") + "/web-root").apply { if (!exists()) mkdirs() } routing { get("{...}") { // 解析请求路径,去除前缀斜杠 val requestPath = call.request.path().removePrefix("/") val targetFile = File(rootDir, requestPath).canonicalFile // 安全校验:防止路径穿越攻击,确保目标文件在根目录范围内 if (!targetFile.startsWith(rootDir.canonicalFile)) { call.respond(HttpStatusCode.Forbidden, "Access denied") return@get } if (!targetFile.exists()) { call.respond(HttpStatusCode.NotFound, "File not found") return@get } if (targetFile.isFile) { // 返回文件内容,并自动匹配MIME类型 val mimeType = Files.probeContentType(targetFile.toPath()) ?: ContentType.Application.OctetStream call.respondFile(targetFile, contentType = ContentType.parse(mimeType)) } else if (targetFile.isDirectory) { // 生成目录列表HTML页面 val dirContent = targetFile.listFiles()?.sortedWith(compareBy({ !it.isDirectory }, { it.name })) ?: emptyArray() val htmlContent = buildString { appendln("<!DOCTYPE html>") appendln("<html>") appendln("<head><title>Index of /$requestPath</title></head>") appendln("<body>") appendln("<h1>Index of /$requestPath</h1>") appendln("<hr>") appendln("<ul>") // 非根目录时添加上级目录链接 if (requestPath.isNotEmpty()) { val parentPath = requestPath.substringBeforeLast("/", "") appendln("<li><a href=\"/$parentPath\">../</a></li>") } // 遍历目录内的文件和子目录,生成链接 dirContent.forEach { file -> val fileUrl = "/${requestPath.takeIf { it.isNotEmpty() }?.let { "$it/" }.orEmpty()}${file.name}" val displayName = if (file.isDirectory) "${file.name}/" else file.name appendln("<li><a href=\"$fileUrl\">$displayName</a></li>") } appendln("</ul>") appendln("<hr>") appendln("</body></html>") } call.respondText(htmlContent, contentType = ContentType.Text.Html) } } } }
3. 关键细节说明
- 路径安全:通过
canonicalFile和startsWith校验,彻底避免恶意路径穿越 - MIME类型匹配:使用
Files.probeContentType自动识别文件类型,无法识别时 fallback到二进制流 - 目录排序:先显示子目录(带/后缀),再按文件名排序,和Apache默认行为一致
- 导航优化:非根目录时显示
../上级目录链接,提升操作便捷性
启动测试
- 在项目根目录创建
web-root文件夹,放入测试文件或子目录 - 启动Ktor服务器,访问
http://localhost:8080即可看到目录列表,点击文件可直接查看内容
内容的提问来源于stack exchange,提问作者Aleksei Tirman
相关产品推荐
相关产品推荐

