diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt index 89e8cb7c..73f2cf95 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt @@ -19,7 +19,6 @@ import io.javalin.http.HttpStatus import io.javalin.http.NotFoundResponse import io.javalin.http.RedirectResponse import io.javalin.http.UnauthorizedResponse -import io.javalin.http.staticfiles.Location import io.javalin.rendering.template.JavalinJte import io.javalin.websocket.WsContext import kotlinx.coroutines.CoroutineScope @@ -27,7 +26,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.combine import kotlinx.coroutines.future.future -import kotlinx.coroutines.runBlocking import org.eclipse.jetty.server.ServerConnector import suwayomi.tachidesk.global.GlobalAPI import suwayomi.tachidesk.graphql.GraphQL @@ -41,9 +39,8 @@ import suwayomi.tachidesk.server.user.UserType import suwayomi.tachidesk.server.user.getUserFromContext import suwayomi.tachidesk.server.user.getUserFromWsContext import suwayomi.tachidesk.server.util.Browser +import suwayomi.tachidesk.server.util.ServerSubpath import suwayomi.tachidesk.server.util.WebInterfaceManager -import uy.kohesive.injekt.injectLazy -import java.io.File import java.io.IOException import java.net.URLEncoder import java.util.Locale @@ -54,8 +51,6 @@ import kotlin.time.Duration.Companion.days object JavalinSetup { private val logger = KotlinLogging.logger {} - private val applicationDirs: ApplicationDirs by injectLazy() - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) fun future(block: suspend CoroutineScope.() -> T): CompletableFuture = scope.future(block = block) @@ -65,82 +60,10 @@ object JavalinSetup { Javalin.create { config -> val templateEngine = TemplateEngine.createPrecompiled(ContentType.Html) config.fileRenderer(JavalinJte(templateEngine)) - if (serverConfig.webUIEnabled.value) { - val subpath = serverConfig.webUISubpath.value - val rootPath = if (subpath.isNotBlank()) "$subpath/" else "/" - runBlocking { - WebInterfaceManager.setupWebUI() - } + WebInterfaceManager.setup(config) - // Helper function to create a servable WebUI directory with subpath injection - fun createServableWebUIRoot(): String = - if (subpath.isNotBlank()) { - val tempWebUIRoot = WebInterfaceManager.createServableWebUIDirectory() - - // Inject subpath configuration - val indexHtmlFile = File("$tempWebUIRoot/index.html") - - if (indexHtmlFile.exists()) { - val originalIndexHtml = indexHtmlFile.readText() - - // Only inject if not already injected - if (!originalIndexHtml.contains("window.__SUWAYOMI_CONFIG__")) { - val configScript = - """ - - """.trimIndent() - - val modifiedIndexHtml = - originalIndexHtml.replace( - "", - "$configScript", - ) - - indexHtmlFile.writeText(modifiedIndexHtml) - } - } - - tempWebUIRoot - } else { - // Use the original webUI root when no subpath - applicationDirs.webUIRoot - } - - // Initial setup of a servable WebUI directory - val servableWebUIRoot = createServableWebUIRoot() - - // Configure static files once during initialization - config.spaRoot.addFile(rootPath, "$servableWebUIRoot/index.html", Location.EXTERNAL) - - if (subpath.isNotBlank()) { - config.staticFiles.add { staticFiles -> - staticFiles.hostedPath = subpath - staticFiles.directory = servableWebUIRoot - staticFiles.location = Location.EXTERNAL - } - } else { - config.staticFiles.add(servableWebUIRoot, Location.EXTERNAL) - } - - // Set up callback for WebUI updates (only updates the SPA root, not static files) - val serveWebUI = { - val updatedServableRoot = createServableWebUIRoot() - config.spaRoot.addFile(rootPath, "$updatedServableRoot/index.html", Location.EXTERNAL) - } - WebInterfaceManager.setServeWebUI(serveWebUI) - - logger.info { - "Serving web static files for ${serverConfig.webUIFlavor.value}" + - if (subpath.isNotBlank()) " under subpath '$subpath'" else "" - } - - // config.registerPlugin(OpenApiPlugin(getOpenApiOptions())) - } + // config.registerPlugin(OpenApiPlugin(getOpenApiOptions())) var connectorAdded = false config.jetty.modifyServer { server -> @@ -181,10 +104,7 @@ object JavalinSetup { } config.router.apiBuilder { - val subpath = serverConfig.webUISubpath.value - val apiPath = if (subpath.isNotBlank()) "$subpath/api/" else "api/" - - path(apiPath) { + path(ServerSubpath.maybeAddAsPrefix("api/")) { path("v1/") { GlobalAPI.defineEndpoints() MangaAPI.defineEndpoints() @@ -204,8 +124,7 @@ object JavalinSetup { } } - val subpath = serverConfig.webUISubpath.value - val loginPath = if (subpath.isNotBlank()) "$subpath/login.html" else "/login.html" + val loginPath = ServerSubpath.maybeAddAsPrefix("/login.html") app.get(loginPath) { ctx -> val locale: Locale = LocalizationHelper.ctxToLocale(ctx) @@ -229,8 +148,7 @@ object JavalinSetup { password == serverConfig.authPassword.value if (isValid) { - val defaultRedirect = if (subpath.isNotBlank()) "$subpath/" else "/" - val redirect = ctx.queryParam("redirect") ?: defaultRedirect + val redirect = ctx.queryParam("redirect") ?: ServerSubpath.maybeAddAsPrefix("/") // NOTE: We currently have no session handler attached. // Thus, all sessions are stored in memory and not persisted. // Furthermore, default session timeout appears to be 30m @@ -259,8 +177,7 @@ object JavalinSetup { !ctx.path().substring(1).contains('/') && listOf(".png", ".jpg", ".ico").any { ctx.path().endsWith(it) } val isPreFlight = ctx.method() == HandlerType.OPTIONS - val apiPath = if (subpath.isNotBlank()) "$subpath/api/" else "/api/" - val isApi = ctx.path().startsWith(apiPath) + val isApi = ctx.path().startsWith(ServerSubpath.maybeAddAsPrefix("/api/")) val requiresAuthentication = !isPreFlight && !isPageIcon && !isWebManifest if (!requiresAuthentication) { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/Browser.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/Browser.kt index 55170640..aef9e61f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/Browser.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/Browser.kt @@ -19,8 +19,8 @@ object Browser { private fun getAppBaseUrl(): String { val appIP = if (serverConfig.ip.value == "0.0.0.0") "127.0.0.1" else serverConfig.ip.value val baseUrl = "http://$appIP:${serverConfig.port.value}" - val subpath = serverConfig.webUISubpath.value - return if (subpath.isNotBlank()) "$baseUrl$subpath/" else baseUrl + + return ServerSubpath.maybeAddAsSuffix(baseUrl) } fun openInBrowser() { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/ServerSubpath.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/ServerSubpath.kt new file mode 100644 index 00000000..e81e80f2 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/ServerSubpath.kt @@ -0,0 +1,35 @@ +package suwayomi.tachidesk.server.util + +import suwayomi.tachidesk.server.serverConfig + +object ServerSubpath { + fun isDefined(): Boolean = raw().isNotBlank() + + private fun raw(): String = serverConfig.webUISubpath.value.trim('/') + + fun normalized(): String = "/${raw()}" + + fun maybeAddAsPrefix(path: String): String { + if (!isDefined()) { + return path + } + + return "${normalized()}/${path.removePrefix("/")}" + } + + fun maybeAddAsSuffix(path: String): String { + if (!isDefined()) { + return path + } + + return "${path.removeSuffix("/")}/${raw()}/" + } + + fun asRootPath(): String { + if (!isDefined()) { + return "/" + } + + return "${normalized()}/" + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt index f2ed247a..50dc0ee2 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt @@ -14,6 +14,8 @@ import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.awaitSuccess import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging +import io.javalin.config.JavalinConfig +import io.javalin.http.staticfiles.Location import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -151,22 +153,79 @@ object WebInterfaceManager { private var serveWebUI: () -> Unit = {} - fun setServeWebUI(serveWebUI: () -> Unit) { - this.serveWebUI = serveWebUI + fun setup(config: JavalinConfig) { + if (!serverConfig.webUIEnabled.value) { + return + } + + runBlocking { + setupWebUI() + } + + val rootPath = ServerSubpath.asRootPath() + val servableWebUIRoot = createServableRoot() + + config.spaRoot.addFile(rootPath, "$servableWebUIRoot/index.html", Location.EXTERNAL) + + if (ServerSubpath.isDefined()) { + config.staticFiles.add { staticFiles -> + staticFiles.hostedPath = ServerSubpath.normalized() + staticFiles.directory = servableWebUIRoot + staticFiles.location = Location.EXTERNAL + } + } else { + config.staticFiles.add(servableWebUIRoot, Location.EXTERNAL) + } + + serveWebUI = { + val updatedServableRoot = createServableRoot() + config.spaRoot.addFile(rootPath, "$updatedServableRoot/index.html", Location.EXTERNAL) + } + + logger.info { + "Serving web static files for ${serverConfig.webUIFlavor.value}" + + if (ServerSubpath.isDefined()) " under subpath '${ServerSubpath.normalized()}'" else "" + } } - fun createServableWebUIDirectory(): String { + private fun createServableRoot(): String { + val tempWebUIRoot = createServableDirectory() + val orgIndexHtml = File("$tempWebUIRoot/index.html") + + if (orgIndexHtml.exists()) { + val originalIndexHtml = orgIndexHtml.readText() + val subpathInjectionScript = + """ + + """.trimIndent() + + val indexHtmlWithSubpathInjection = + originalIndexHtml.replace( + "", + "$subpathInjectionScript", + ) + + orgIndexHtml.writeText(indexHtmlWithSubpathInjection) + } + + return tempWebUIRoot + } + + private fun createServableDirectory(): String { val originalWebUIRoot = applicationDirs.webUIRoot val tempWebUIRoot = "${applicationDirs.tempRoot}/webui-serve" - // Clean and create temp directory File(tempWebUIRoot).deleteRecursively() File(tempWebUIRoot).mkdirs() - // Copy entire WebUI directory to temp location File(originalWebUIRoot).copyRecursively(File(tempWebUIRoot)) - logger.info { "Created servable WebUI directory at: $tempWebUIRoot" } + logger.debug { "Created servable WebUI directory at: $tempWebUIRoot" } // Return canonical path to avoid Jetty alias issues return File(tempWebUIRoot).canonicalPath