Не подтверждена Коммит 69f48b21 создал по автору Usova Daria's avatar Usova Daria Зафиксировано автором GitHub
Просмотр файлов

KTOR-3944 SinglePageApplication plugin returns 404 for non-existent paths (#2871)

владелец 67d70cf7
......@@ -356,6 +356,31 @@ public final class io/ktor/server/http/content/LocalFileContentKt {
public static synthetic fun LocalFileContent$default (Ljava/nio/file/Path;Ljava/nio/file/Path;Lio/ktor/http/ContentType;ILjava/lang/Object;)Lio/ktor/server/http/content/LocalFileContent;
}
public final class io/ktor/server/http/content/SPAConfig {
public fun <init> ()V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/util/List;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getApplicationRoute ()Ljava/lang/String;
public final fun getDefaultPage ()Ljava/lang/String;
public final fun getFilesPath ()Ljava/lang/String;
public final fun getUseResources ()Z
public final fun setApplicationRoute (Ljava/lang/String;)V
public final fun setDefaultPage (Ljava/lang/String;)V
public final fun setFilesPath (Ljava/lang/String;)V
public final fun setUseResources (Z)V
}
public final class io/ktor/server/http/content/SinglePageApplicationKt {
public static final fun angular (Lio/ktor/server/http/content/SPAConfig;Ljava/lang/String;)V
public static final fun backbone (Lio/ktor/server/http/content/SPAConfig;Ljava/lang/String;)V
public static final fun ember (Lio/ktor/server/http/content/SPAConfig;Ljava/lang/String;)V
public static final fun ignoreFiles (Lio/ktor/server/http/content/SPAConfig;Lkotlin/jvm/functions/Function1;)V
public static final fun react (Lio/ktor/server/http/content/SPAConfig;Ljava/lang/String;)V
public static final fun singlePageApplication (Lio/ktor/server/routing/Route;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun singlePageApplication$default (Lio/ktor/server/routing/Route;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static final fun vue (Lio/ktor/server/http/content/SPAConfig;Ljava/lang/String;)V
}
public final class io/ktor/server/http/content/StaticContentKt {
public static final fun default (Lio/ktor/server/routing/Route;Ljava/io/File;)V
public static final fun default (Lio/ktor/server/routing/Route;Ljava/lang/String;)V
......
......@@ -2,69 +2,54 @@
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.plugins.spa
package io.ktor.server.http.content
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.http.content.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.*
import io.ktor.util.pipeline.*
import java.io.*
/**
* A plugin that allows you to serve a single-page application
* Serves a single-page application.
* You can learn more from [Serving single-page applications](https://ktor.io/docs/serving-spa.html).
*
* A basic plugin configuration for the application served from the filesPath folder
* with index.html as a default file:
* A basic configuration for the application served from the `filesPath` folder
* with `index.html` as a default file:
*
* ```
* install(SinglePageApplication) {
* filesPath = "application/project_path"
* application {
* routing {
* singlePageApplication {
* filesPath = "application/project_path"
* }
* }
* }
* ```
*/
public val SinglePageApplication: ApplicationPlugin<SPAConfig> = createApplicationPlugin(
"SinglePageApplication",
{ SPAConfig() }
) {
val defaultPage: String = pluginConfig.defaultPage
val applicationRoute: String = pluginConfig.applicationRoute
val filesPath: String = pluginConfig.filesPath
val ignoredFiles: MutableList<(String) -> Boolean> = pluginConfig.ignoredFiles
val usePackageNames: Boolean = pluginConfig.useResources
fun isUriStartWith(uri: String) =
uri.startsWith(applicationRoute) || uri.startsWith("/$applicationRoute")
application.routing {
static(applicationRoute) {
if (usePackageNames) {
resources(filesPath)
defaultResource(defaultPage, filesPath)
} else {
staticRootFolder = File(filesPath)
files(".")
default(defaultPage)
}
}
}
public fun Route.singlePageApplication(configBuilder: SPAConfig.() -> Unit = {}) {
val config = SPAConfig()
configBuilder.invoke(config)
onCall { call ->
val requestUrl = call.request.uri
if (!isUriStartWith(requestUrl)) return@onCall
static(config.applicationRoute) {
val shouldFileBeIgnored = { filePath: String ->
config.ignoredFiles.firstOrNull { it.invoke(filePath) } != null
}
if (ignoredFiles.firstOrNull { it.invoke(requestUrl) } != null) {
call.respond(HttpStatusCode.Forbidden)
if (config.useResources) {
resourceWithDefault(
config.filesPath,
config.defaultPage,
shouldFileBeIgnored
)
} else {
staticRootFolder = File(config.filesPath)
filesWithDefault(".", config.defaultPage, shouldFileBeIgnored)
}
}
}
/**
* Configuration for the [SinglePageApplication] plugin
* Configuration for the [Route.singlePageApplication] plugin.
*/
public class SPAConfig(
/**
......
......@@ -125,6 +125,18 @@ public fun Route.file(remotePath: String, localPath: File) {
*/
public fun Route.files(folder: String): Unit = files(File(folder))
/**
* Sets up routing to serve all files from [folder].
* Serves [defaultFile] if a missing file is requested.
* Serves [defaultFile] if the requested file should be ignored by [shouldFileBeIgnored].
*/
internal fun Route.filesWithDefault(
folder: String,
defaultFile: String,
shouldFileBeIgnored: (String) -> Boolean
): Unit =
filesWithDefaultFile(File(folder), File(defaultFile), shouldFileBeIgnored)
/**
* Sets up routing to serve all files from [folder]
*/
......@@ -138,6 +150,35 @@ public fun Route.files(folder: File) {
}
}
/**
* Sets up routing to serve all files from [folder].
* Serves [defaultFile] if a missing file is requested.
* Serves [defaultFile] if the requested file should be ignored by [shouldFileBeIgnored].
*/
internal fun Route.filesWithDefaultFile(
folder: File,
defaultFile: File,
shouldFileBeIgnored: (String) -> Boolean
) {
val dir = staticRootFolder.combine(folder)
val compressedTypes = staticContentEncodedTypes
get("{$pathParameterName...}") {
val relativePath = call.parameters.getAll(pathParameterName)?.joinToString(File.separator) ?: return@get
if (shouldFileBeIgnored.invoke(relativePath)) {
call.respondStaticFile(dir.combine(defaultFile), compressedTypes)
}
val file = dir.combineSafe(relativePath)
call.respondStaticFile(file, compressedTypes)
if (!call.isHandled) {
call.respondStaticFile(dir.combine(defaultFile), compressedTypes)
}
}
}
private suspend inline fun ApplicationCall.respondStaticFile(
requestedFile: File,
compressedTypes: List<CompressedFileType>?
......@@ -220,6 +261,37 @@ public fun Route.resource(remotePath: String, resource: String = remotePath, res
}
}
/**
* Sets up routing to serve all resources in [resourcePackage].
* Serves [defaultFile] if a missing file is requested.
* Serves [defaultFile] if the requested file should be ignored by [shouldFileBeIgnored].
*/
internal fun Route.resourceWithDefault(
resourcePackage: String? = null,
defaultResource: String,
shouldFileBeIgnored: (String) -> Boolean
) {
val packageName = staticBasePackage.combinePackage(resourcePackage)
get("{$pathParameterName...}") {
val relativePath = call.parameters.getAll(pathParameterName)?.joinToString(File.separator) ?: return@get
if (shouldFileBeIgnored.invoke(relativePath)) {
call.resolveResource(defaultResource, packageName)?.let {
call.respond(it)
}
}
val content = call.resolveResource(relativePath, packageName)
if (content != null) {
call.respond(content)
} else {
call.resolveResource(defaultResource, packageName)?.let {
call.respond(it)
}
}
}
}
/**
* Sets up routing to serve all resources in [resourcePackage]
*/
......
public final class io/ktor/plugins/spa/SPAConfig {
public fun <init> ()V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/util/List;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getApplicationRoute ()Ljava/lang/String;
public final fun getDefaultPage ()Ljava/lang/String;
public final fun getFilesPath ()Ljava/lang/String;
public final fun getUseResources ()Z
public final fun setApplicationRoute (Ljava/lang/String;)V
public final fun setDefaultPage (Ljava/lang/String;)V
public final fun setFilesPath (Ljava/lang/String;)V
public final fun setUseResources (Z)V
}
public final class io/ktor/plugins/spa/SinglePageApplicationKt {
public static final fun angular (Lio/ktor/plugins/spa/SPAConfig;Ljava/lang/String;)V
public static final fun backbone (Lio/ktor/plugins/spa/SPAConfig;Ljava/lang/String;)V
public static final fun ember (Lio/ktor/plugins/spa/SPAConfig;Ljava/lang/String;)V
public static final fun getSinglePageApplication ()Lio/ktor/server/application/ApplicationPlugin;
public static final fun ignoreFiles (Lio/ktor/plugins/spa/SPAConfig;Lkotlin/jvm/functions/Function1;)V
public static final fun react (Lio/ktor/plugins/spa/SPAConfig;Ljava/lang/String;)V
public static final fun vue (Lio/ktor/plugins/spa/SPAConfig;Ljava/lang/String;)V
}
/*
* Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
description = ""
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.tests.server.plugins
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.plugins.spa.*
import io.ktor.server.testing.*
import kotlin.test.*
class SinglePageApplicationTest {
@Test
fun testPageGet() = testApplication {
install(SinglePageApplication) {
filesPath = "jvm/test/io/ktor/tests/server/plugins"
applicationRoute = "selected"
defaultPage = "Empty3.kt"
}
assertEquals(client.get("/selected/Empty1.kt").status, HttpStatusCode.OK)
assertEquals(client.get("/selected").status, HttpStatusCode.OK)
}
@Test
fun testIgnoreRoutes() = testApplication {
install(SinglePageApplication) {
filesPath = "jvm/test/io/ktor/tests/server/plugins"
defaultPage = "SinglePageApplicationTest.kt"
ignoreFiles { it.contains("Empty1.kt") }
ignoreFiles { it.endsWith("Empty2.kt") }
}
assertEquals(HttpStatusCode.OK, client.get("/Empty3.kt").status)
assertEquals(HttpStatusCode.OK, client.get("/").status)
assertEquals(HttpStatusCode.Forbidden, client.get("/Empty1.kt").status)
assertEquals(HttpStatusCode.Forbidden, client.get("/Empty2.kt").status)
}
@Test
fun testIgnoreAllRoutes() = testApplication {
install(SinglePageApplication) {
filesPath = "jvm/test/io/ktor/tests/server/plugins"
defaultPage = "SinglePageApplicationTest.kt"
ignoreFiles { true }
}
assertEquals(HttpStatusCode.Forbidden, client.get("/Empty1.kt").status)
assertEquals(HttpStatusCode.Forbidden, client.get("/Empty1.kt").status)
}
@Test
fun testResources() = testApplication {
install(SinglePageApplication) {
useResources = true
filesPath = "io.ktor.tests.server.plugins"
defaultPage = "SinglePageApplicationTest.class"
}
assertEquals(HttpStatusCode.OK, client.get("/Empty1.class").status)
assertEquals(HttpStatusCode.OK, client.get("/").status)
}
@Test
fun testIgnoreResourceRoutes() = testApplication {
install(SinglePageApplication) {
useResources = true
filesPath = "io.ktor.tests.server.plugins"
defaultPage = "SinglePageApplicationTest.class"
ignoreFiles { it.contains("Empty1.class") }
ignoreFiles { it.endsWith("Empty2.class") }
}
assertEquals(HttpStatusCode.OK, client.get("/Empty3.class").status)
assertEquals(HttpStatusCode.OK, client.get("/").status)
assertEquals(HttpStatusCode.Forbidden, client.get("/Empty1.class").status)
assertEquals(HttpStatusCode.Forbidden, client.get("/Empty2.class").status)
}
@Test
fun testIgnoreAllResourceRoutes() = testApplication {
install(SinglePageApplication) {
useResources = true
filesPath = "io.ktor.tests.server.plugins"
defaultPage = "SinglePageApplicationTest.kt"
ignoreFiles { true }
}
assertEquals(HttpStatusCode.Forbidden, client.get("/Empty1.class").status)
assertEquals(HttpStatusCode.Forbidden, client.get("/").status)
}
@Test
fun testShortcut() = testApplication {
install(SinglePageApplication) {
angular("jvm/test/io/ktor/tests/server/plugins")
}
assertEquals(HttpStatusCode.OK, client.get("/Empty1.kt").status)
}
}
package io.ktor.tests.server.plugins/*
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.server.http.spa
// required for tests
class Empty1
package io.ktor.tests.server.plugins/*
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.server.http.spa
// required for tests
class Empty2
package io.ktor.tests.server.plugins/*
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.server.http.spa
// required for tests
class Empty3
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.server.http.spa
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.http.content.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import kotlin.test.*
class SinglePageApplicationTest {
@Test
fun fullWithFilesTest() = testApplication {
application {
routing {
singlePageApplication {
filesPath = "jvm/test/io/ktor/server/http/spa"
applicationRoute = "selected"
defaultPage = "Empty3.kt"
ignoreFiles { it.contains("Empty2.kt") }
}
}
}
client.get("/selected").let {
assertEquals(it.status, HttpStatusCode.OK)
assertEquals(it.bodyAsText().trimIndent(), empty3)
}
client.get("/selected/a").let {
assertEquals(it.status, HttpStatusCode.OK)
assertEquals(it.bodyAsText().trimIndent(), empty3)
}
client.get("/selected/Empty2.kt").let {
assertEquals(it.status, HttpStatusCode.OK)
assertEquals(it.bodyAsText().trimIndent(), empty3)
}
client.get("/selected/Empty1.kt").let {
assertEquals(it.status, HttpStatusCode.OK)
assertEquals(it.bodyAsText().trimIndent(), empty1)
}
}
@Test
fun testPageGet() = testApplication {
application {
routing {
singlePageApplication {
filesPath = "jvm/test/io/ktor/server/http/spa"
applicationRoute = "selected"
defaultPage = "Empty3.kt"
}
}
}
client.get("/selected/Empty1.kt").let {
assertEquals(it.status, HttpStatusCode.OK)
assertEquals(it.bodyAsText().trimIndent(), empty1)
}
client.get("/selected").let {
assertEquals(it.status, HttpStatusCode.OK)
assertEquals(it.bodyAsText().trimIndent(), empty3)
}
}
@Test
fun testIgnoreAllRoutes() = testApplication {
application {
routing {
singlePageApplication {
filesPath = "jvm/test/io/ktor/server/http/spa"
defaultPage = "Empty3.kt"
ignoreFiles { true }
}
}
}
client.get("/").let {
assertEquals(it.status, HttpStatusCode.OK)
assertEquals(it.bodyAsText().trimIndent(), empty3)
}
client.get("/a").let {
assertEquals(it.status, HttpStatusCode.OK)
assertEquals(it.bodyAsText().trimIndent(), empty3)
}
client.get("/Empty1.kt").let {
assertEquals(it.status, HttpStatusCode.OK)
assertEquals(it.bodyAsText().trimIndent(), empty3)
}
}
@Test
fun fullWithResourcesTest() = testApplication {
application {
routing {
singlePageApplication {
useResources = true
filesPath = "io.ktor.server.http.spa"
applicationRoute = "selected"
defaultPage = "Empty3.class"
ignoreFiles { it.contains("Empty2.class") }
}
}
}
assertEquals(client.get("/selected").status, HttpStatusCode.OK)
assertEquals(client.get("/selected/a").status, HttpStatusCode.OK)
assertEquals(client.get("/selected/Empty2.kt").status, HttpStatusCode.OK)
assertEquals(client.get("/selected/Empty1.kt").status, HttpStatusCode.OK)
}
@Test
fun testResources() = testApplication {
application {
routing {
singlePageApplication {
useResources = true
filesPath = "io.ktor.server.http.spa"
defaultPage = "SinglePageApplicationTest.class"
}
}
}
assertEquals(HttpStatusCode.OK, client.get("/Empty1.class").status)
assertEquals(HttpStatusCode.OK, client.get("/SinglePageApplicationTest.class").status)
assertEquals(HttpStatusCode.OK, client.get("/a").status)
assertEquals(HttpStatusCode.OK, client.get("/").status)
}
@Test
fun testIgnoreResourceRoutes() = testApplication {
application {
routing {
singlePageApplication {
useResources = true
filesPath = "io.ktor.server.http.spa"
defaultPage = "SinglePageApplicationTest.class"
ignoreFiles { it.contains("Empty1.class") }
ignoreFiles { it.endsWith("Empty2.class") }
}
}
}
assertEquals(HttpStatusCode.OK, client.get("/Empty3.class").status)
assertEquals(HttpStatusCode.OK, client.get("/").status)
assertEquals(HttpStatusCode.OK, client.get("/Empty1.class").status)
assertEquals(HttpStatusCode.OK, client.get("/Empty2.class").status)
}
@Test
fun testIgnoreAllResourceRoutes() = testApplication {
application {
routing {
singlePageApplication {
useResources = true
filesPath = "io.ktor.server.http.spa"
defaultPage = "SinglePageApplicationTest.class"
ignoreFiles { true }
}
}
}
assertEquals(HttpStatusCode.OK, client.get("/SinglePageApplicationTest.class").status)
assertEquals(HttpStatusCode.OK, client.get("/Empty1.class").status)
assertEquals(HttpStatusCode.OK, client.get("/a").status)
assertEquals(HttpStatusCode.OK, client.get("/").status)
}
@Test
fun testShortcut() = testApplication {
application {
routing {
singlePageApplication {
angular("jvm/test/io/ktor/server/http/spa")
}
}
}
assertEquals(HttpStatusCode.OK, client.get("/Empty1.kt").status)
}
private val empty1 = """
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.server.http.spa
// required for tests
class Empty1
""".trimIndent()
private val empty3 = """
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.server.http.spa
// required for tests
class Empty3
""".trimIndent()
}
......@@ -116,7 +116,6 @@ include(":ktor-server:ktor-server-plugins:ktor-server-partial-content")
include(":ktor-server:ktor-server-plugins:ktor-server-pebble")
include(":ktor-server:ktor-server-plugins:ktor-server-resources")
include(":ktor-server:ktor-server-plugins:ktor-server-sessions")
include(":ktor-server:ktor-server-plugins:ktor-server-single-page")
include(":ktor-server:ktor-server-plugins:ktor-server-status-pages")
include(":ktor-server:ktor-server-plugins:ktor-server-thymeleaf")
include(":ktor-server:ktor-server-plugins:ktor-server-velocity")
......
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать