diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 7abbfdf9..25c53c23 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -66,6 +66,9 @@ dependencies { // asm for fixing SimpleDateFormat (must match Dex2Jar version) implementation("org.ow2.asm:asm-debug-all:5.0.3") + // extracting zip files + implementation("net.lingala.zip4j:zip4j:2.9.0") + // Source models and interfaces from Tachiyomi 1.x // using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi // implementation("tachiyomi.sourceapi:source-api:1.1") @@ -99,6 +102,7 @@ sourceSets { // should be bumped with each stable release val tachideskVersion = System.getenv("ProductVersion") ?: "v0.4.3" +val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r19" // counts commit count on master val tachideskRevision = runCatching { @@ -126,6 +130,11 @@ buildConfig { buildConfigField("String", "BUILD_TYPE", if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview") buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString()) + + buildConfigField("String", "WEBUI_REPO", "https://github.com/Suwayomi/Tachidesk-WebUI-preview") + buildConfigField("String", "WEBUI_TAG", webUIRevisionTag) + + buildConfigField("String", "GITHUB", "https://github.com/Suwayomi/Tachidesk") buildConfigField("String", "DISCORD", "https://discord.gg/DDZdqZWaHA") } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt index b7331fe7..1cbc04d1 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt @@ -8,15 +8,20 @@ package suwayomi.tachidesk.server * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import io.javalin.Javalin +import io.javalin.http.staticfiles.Location import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.future.future import mu.KotlinLogging +import org.kodein.di.DI +import org.kodein.di.conf.global +import org.kodein.di.instance import suwayomi.tachidesk.anime.AnimeAPI import suwayomi.tachidesk.global.GlobalAPI import suwayomi.tachidesk.manga.MangaAPI import suwayomi.tachidesk.server.util.Browser +import suwayomi.tachidesk.server.util.setupWebUI import java.io.IOException import java.util.concurrent.CompletableFuture import kotlin.concurrent.thread @@ -24,6 +29,8 @@ import kotlin.concurrent.thread object JavalinSetup { private val logger = KotlinLogging.logger {} + private val applicationDirs by DI.global.instance() + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) fun future(block: suspend CoroutineScope.() -> T): CompletableFuture { @@ -31,25 +38,19 @@ object JavalinSetup { } fun javalinSetup() { - var hasWebUiBundled = false - val app = Javalin.create { config -> - try { - // if the bellow line throws an exception then webUI is not bundled - this::class.java.getResource("/webUI/index.html") + if (serverConfig.webUIEnabled) { + setupWebUI() - // no exception so we can tell javalin to serve webUI - hasWebUiBundled = true - config.addStaticFiles("/webUI") - config.addSinglePageRoot("/", "/webUI/index.html") - } catch (e: RuntimeException) { - logger.warn("react build files are missing.") - hasWebUiBundled = false + logger.info { "Serving webUI static files" } + config.addStaticFiles(applicationDirs.webUIRoot, Location.EXTERNAL) + config.addSinglePageRoot("/", applicationDirs.webUIRoot + "/index.html", Location.EXTERNAL) } + config.enableCorsForAllOrigins() }.events { event -> event.serverStarted { - if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) { + if (serverConfig.webUIEnabled && serverConfig.initialOpenInBrowserEnabled) { Browser.openInBrowser() } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt index b6c52801..75872535 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt @@ -33,6 +33,7 @@ class ApplicationDirs( val mangaThumbnailsRoot = "$dataRoot/manga-thumbnails" val animeThumbnailsRoot = "$dataRoot/anime-thumbnails" val mangaRoot = "$dataRoot/manga" + val webUIRoot = "$dataRoot/webUI" } val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebUIManager.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebUIManager.kt new file mode 100644 index 00000000..4a4b7c74 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebUIManager.kt @@ -0,0 +1,92 @@ +package suwayomi.tachidesk.server.util + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import mu.KotlinLogging +import net.lingala.zip4j.ZipFile +import org.kodein.di.DI +import org.kodein.di.conf.global +import org.kodein.di.instance +import suwayomi.tachidesk.server.ApplicationDirs +import suwayomi.tachidesk.server.BuildConfig +import java.io.BufferedInputStream +import java.io.File +import java.net.URL +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + +private val logger = KotlinLogging.logger {} +private val applicationDirs by DI.global.instance() +private val tmpDir = System.getProperty("java.io.tmpdir") + +private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) } + +private fun directoryMD5(fileDir: String): String { + var sum = "" + File(fileDir).walk().toList().sortedBy { it.path }.forEach { file -> + if (file.isFile) { + val md5 = MessageDigest.getInstance("MD5") + md5.update(file.readBytes()) + val digest = md5.digest() + sum += digest.toHex() + } + } + + val md5 = MessageDigest.getInstance("MD5") + md5.update(sum.toByteArray(StandardCharsets.UTF_8)) + val digest = md5.digest() + return digest.toHex() +} + +fun setupWebUI() { + // check if we have webUI installed and is correct version + val webUIRevisionFile = File(applicationDirs.webUIRoot + "/revision") + if (webUIRevisionFile.exists() && webUIRevisionFile.readText().trim() == BuildConfig.WEBUI_TAG) { + logger.info { "WebUI Static files exists and is the correct revision" } + logger.info { "Verifying WebUI Static files..." } + logger.info { "md5: " + directoryMD5(applicationDirs.webUIRoot) } + } else { + File(applicationDirs.webUIRoot).deleteRecursively() + + // download webUI zip + val webUIZip = "Tachidesk-WebUI-${BuildConfig.WEBUI_TAG}.zip" + val webUIZipPath = "$tmpDir/$webUIZip" + val webUIZipURL = "${BuildConfig.WEBUI_REPO}/releases/download/${BuildConfig.WEBUI_TAG}/$webUIZip" + val webUIZipFile = File(webUIZipPath) + webUIZipFile.delete() + + logger.info { "Downloading WebUI zip from the Internet..." } + val data = ByteArray(1024) + + webUIZipFile.outputStream().use { webUIZipFileOut -> + BufferedInputStream(URL(webUIZipURL).openStream()).use { inp -> + var totalCount = 0 + var tresh = 0 + while (true) { + val count = inp.read(data, 0, 1024) + totalCount += count + if (totalCount > tresh + 10 * 1024) { + tresh = totalCount + print(" *") + } + if (count == -1) + break + webUIZipFileOut.write(data, 0, count) + } + println() + logger.info { "Downloading WebUI Done." } + } + } + + // extract webUI zip + logger.info { "Extracting downloaded WebUI zip..." } + File(applicationDirs.webUIRoot).mkdirs() + ZipFile(webUIZipPath).extractAll(applicationDirs.webUIRoot) + logger.info { "Extracting downloaded WebUI zip Done." } + } +}