diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt index f9c3b4542..dc6f2689f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt @@ -8,6 +8,7 @@ import suwayomi.tachidesk.graphql.types.UpdateState.IDLE import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.util.WebInterfaceManager +import suwayomi.tachidesk.server.util.WebUIFlavor import java.util.concurrent.CompletableFuture import kotlin.time.Duration.Companion.seconds @@ -28,7 +29,9 @@ class InfoMutation { return@withTimeout WebUIUpdatePayload(input.clientMutationId, WebInterfaceManager.status.value) } - val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable() + val flavor = WebUIFlavor.current + + val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable(flavor) if (!updateAvailable) { val didUpdateCheckFail = version.isEmpty() @@ -39,7 +42,7 @@ class InfoMutation { ) } try { - WebInterfaceManager.startDownloadInScope(version) + WebInterfaceManager.startDownloadInScope(flavor, version) } catch (e: Exception) { // ignore since we use the status anyway } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt index 160614abf..5973bd0cd 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt @@ -8,6 +8,7 @@ import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.generated.BuildConfig import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.util.WebInterfaceManager +import suwayomi.tachidesk.server.util.WebUIFlavor import java.util.concurrent.CompletableFuture class InfoQuery { @@ -60,7 +61,7 @@ class InfoQuery { fun checkForWebUIUpdate(): CompletableFuture { return future { - val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable(raiseError = true) + val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable(WebUIFlavor.current, raiseError = true) WebUIUpdateCheck( channel = serverConfig.webUIChannel.value, tag = version, 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 94826a56d..b60f85364 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt @@ -119,7 +119,12 @@ enum class WebUIFlavor( ; companion object { - fun from(value: String): WebUIFlavor = entries.find { it.uiName == value } ?: WEBUI + val default: WebUIFlavor = WEBUI + + fun from(value: String): WebUIFlavor = entries.find { it.uiName == value } ?: default + + val current: WebUIFlavor + get() = from(serverConfig.webUIFlavor.value) } } @@ -128,6 +133,7 @@ object WebInterfaceManager { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private const val LAST_WEBUI_UPDATE_CHECK_KEY = "lastWebUIUpdateCheck" + private const val SERVED_WEBUI_FLAVOR_KEY = "servedWebUIFlavor" private val preferences = Injekt.get().getSharedPreferences("server_util", Context.MODE_PRIVATE) private var currentUpdateTaskId: String = "" @@ -206,6 +212,14 @@ object WebInterfaceManager { this.serveWebUI = serveWebUI } + private fun setServedWebUIFlavor(flavor: WebUIFlavor) { + preferences.edit().putString(SERVED_WEBUI_FLAVOR_KEY, flavor.uiName).apply() + } + + private fun getServedWebUIFlavor(): WebUIFlavor { + return WebUIFlavor.from(preferences.getString(SERVED_WEBUI_FLAVOR_KEY, WebUIFlavor.default.uiName)!!) + } + private fun isAutoUpdateEnabled(): Boolean { return serverConfig.webUIUpdateCheckInterval.value.toInt() != 0 } @@ -224,6 +238,7 @@ object WebInterfaceManager { val task = { logger.debug { "Checking for webUI update (" + + "flavor= ${WebUIFlavor.current.uiName}, " + "channel= ${serverConfig.webUIChannel.value}, " + "interval= ${serverConfig.webUIUpdateCheckInterval.value}h, " + "lastAutomatedUpdate= ${ @@ -234,7 +249,7 @@ object WebInterfaceManager { } runBlocking { - checkForUpdate() + checkForUpdate(WebUIFlavor.current) } } @@ -253,49 +268,66 @@ object WebInterfaceManager { return } + val flavor = WebUIFlavor.current + val servedFlavor = getServedWebUIFlavor() + + val log = + KotlinLogging.logger("${logger.name} setupWebUI(flavor= ${flavor.uiName}, servedFlavor= ${servedFlavor.uiName})") + if (doesLocalWebUIExist(applicationDirs.webUIRoot)) { val currentVersion = getLocalVersion() - logger.info { "setupWebUI: found webUI files - flavor= ${serverConfig.webUIFlavor.value}, version= $currentVersion" } + log.info { "found webUI files - version= $currentVersion" } + + val hasFlavorChanged = flavor.uiName != servedFlavor.uiName + if (hasFlavorChanged) { + try { + doInitialSetup(flavor) + return + } catch (e: Exception) { + log.warn(e) { "Failed to install the version of the new flavor, proceeding with version of previous flavor" } + } + } - if (!isLocalWebUIValid(applicationDirs.webUIRoot)) { + val flavorToValidate = if (hasFlavorChanged) servedFlavor else flavor + if (!isLocalWebUIValid(flavorToValidate, applicationDirs.webUIRoot)) { try { - doInitialSetup() + doInitialSetup(flavorToValidate, isInvalid = true) } catch (e: Exception) { - logger.warn(e) { "WebUI is invalid and failed to install a valid version, proceeding with invalid version" } + log.warn(e) { "WebUI is invalid and failed to install a valid version, proceeding with invalid version" } } return } if (isAutoUpdateEnabled()) { - checkForUpdate() + checkForUpdate(flavor) } // check if the bundled webUI version is a newer version than the current used version // this could be the case in case no compatible webUI version is available and a newer server version was installed val shouldUpdateToBundledVersion = - serverConfig.webUIFlavor.value == WebUIFlavor.WEBUI.uiName && extractVersion(getLocalVersion()) < + flavor.uiName == WebUIFlavor.default.uiName && extractVersion(getLocalVersion()) < extractVersion( BuildConfig.WEBUI_TAG, ) if (shouldUpdateToBundledVersion) { - logger.debug { "setupWebUI: update to bundled version \"${BuildConfig.WEBUI_TAG}\"" } + log.debug { "update to bundled version \"${BuildConfig.WEBUI_TAG}\"" } try { setupBundledWebUI() } catch (e: Exception) { - logger.error(e) { "setupWebUI: failed the update to the bundled webUI" } + log.error(e) { "failed the update to the bundled webUI" } } } return } - logger.warn { "setupWebUI: no webUI files found, starting download..." } + log.warn { "no webUI files found, starting download..." } try { - doInitialSetup() + doInitialSetup(flavor) } catch (e: Exception) { - logger.error(e) { + log.error(e) { "Failed to setup the webUI. Unable to start the server with a served webUI, change the settings to start" + "without one. Stopping the server now..." } @@ -306,8 +338,14 @@ object WebInterfaceManager { /** * Tries to download the latest compatible version for the selected webUI and falls back to the default webUI in case of errors. */ - private suspend fun doInitialSetup() { - val isLocalWebUIValid = isLocalWebUIValid(applicationDirs.webUIRoot) + private suspend fun doInitialSetup( + flavor: WebUIFlavor, + isInvalid: Boolean = false, + ) { + val log = + KotlinLogging.logger("${logger.name} doInitialSetup(flavor= ${flavor.uiName})") + + val isLocalWebUIValid = !isInvalid && isLocalWebUIValid(flavor, applicationDirs.webUIRoot) /** * Performs the download and returns if the download was successful. @@ -316,7 +354,7 @@ object WebInterfaceManager { */ val doDownload: suspend (getVersion: suspend () -> String) -> Boolean = { getVersion -> try { - downloadVersion(getVersion()) + downloadVersion(flavor, getVersion()) true } catch (e: Exception) { false @@ -324,23 +362,23 @@ object WebInterfaceManager { } // download the latest compatible version for the current selected webUI - val fallbackToDefaultWebUI = !doDownload { getLatestCompatibleVersion() } + val fallbackToDefaultWebUI = !doDownload { getLatestCompatibleVersion(flavor) } if (!fallbackToDefaultWebUI) { return } - if (serverConfig.webUIFlavor.value != WebUIFlavor.WEBUI.uiName) { - logger.warn { "doInitialSetup: fallback to default webUI \"${WebUIFlavor.WEBUI.uiName}\"" } + if (flavor.uiName != WebUIFlavor.default.uiName) { + log.warn { "fallback to default webUI \"${WebUIFlavor.default.uiName}\"" } - serverConfig.webUIFlavor.value = WebUIFlavor.WEBUI.uiName + serverConfig.webUIFlavor.value = WebUIFlavor.default.uiName - val fallbackToBundledVersion = !doDownload { getLatestCompatibleVersion() } + val fallbackToBundledVersion = !doDownload { getLatestCompatibleVersion(flavor) } if (!fallbackToBundledVersion) { return } } - logger.warn { "doInitialSetup: fallback to bundled default webUI \"${WebUIFlavor.WEBUI.uiName}\"" } + log.warn { "fallback to bundled default webUI \"${WebUIFlavor.default.uiName}\"" } try { setupBundledWebUI() @@ -352,12 +390,13 @@ object WebInterfaceManager { private suspend fun setupBundledWebUI() { try { extractBundledWebUI() + setServedWebUIFlavor(WebUIFlavor.default) return } catch (e: BundledWebUIMissing) { logger.warn(e) { "setupBundledWebUI: fallback to downloading the version of the bundled webUI" } } - downloadVersion(BuildConfig.WEBUI_TAG) + downloadVersion(WebUIFlavor.default, BuildConfig.WEBUI_TAG) } private fun extractBundledWebUI() { @@ -366,7 +405,7 @@ object WebInterfaceManager { logger.info { "extractBundledWebUI: Using the bundled WebUI zip..." } - val webUIZip = WebUIFlavor.WEBUI.baseFileName + val webUIZip = WebUIFlavor.default.baseFileName val webUIZipPath = "$tmpDir/$webUIZip" val webUIZipFile = File(webUIZipPath) resourceWebUI.use { input -> @@ -379,25 +418,31 @@ object WebInterfaceManager { extractDownload(webUIZipPath, applicationDirs.webUIRoot) } - private suspend fun checkForUpdate() { + private suspend fun checkForUpdate(flavor: WebUIFlavor) { preferences.edit().putLong(LAST_WEBUI_UPDATE_CHECK_KEY, System.currentTimeMillis()).apply() val localVersion = getLocalVersion() - if (!isUpdateAvailable(localVersion).second) { - logger.debug { "checkForUpdate(${serverConfig.webUIFlavor.value}, $localVersion): local version is the latest one" } + val log = + KotlinLogging.logger("${logger.name} checkForUpdate(flavor= ${flavor.uiName}, localVersion= $localVersion)") + + if (!isUpdateAvailable(flavor, localVersion).second) { + log.debug { "local version is the latest one" } return } - logger.info { "checkForUpdate(${serverConfig.webUIFlavor.value}, $localVersion): An update is available, starting download..." } + log.info { "An update is available, starting download..." } try { - downloadVersion(getLatestCompatibleVersion()) + downloadVersion(flavor, getLatestCompatibleVersion(flavor)) } catch (e: Exception) { - logger.warn(e) { "checkForUpdate: failed due to" } + log.warn(e) { "failed due to" } } } - private fun getDownloadUrlFor(version: String): String { - val baseReleasesUrl = "${WebUIFlavor.WEBUI.repoUrl}/releases" + private fun getDownloadUrlFor( + flavor: WebUIFlavor, + version: String, + ): String { + val baseReleasesUrl = "${flavor.repoUrl}/releases" val downloadSpecificVersionBaseUrl = "$baseReleasesUrl/download" return "$downloadSpecificVersionBaseUrl/$version" @@ -417,20 +462,25 @@ object WebInterfaceManager { return webUIRevisionFile.exists() } - private suspend fun isLocalWebUIValid(path: String): Boolean { + private suspend fun isLocalWebUIValid( + flavor: WebUIFlavor, + path: String, + ): Boolean { if (!doesLocalWebUIExist(path)) { return false } - logger.info { "isLocalWebUIValid: Verifying WebUI files..." } + val log = + KotlinLogging.logger("${logger.name} isLocalWebUIValid(flavor= ${flavor.uiName}, path= $path)") + log.info { "Verifying WebUI files..." } val currentVersion = getLocalVersion(path) val localMD5Sum = getLocalMD5Sum(path) - val currentVersionMD5Sum = fetchMD5SumFor(currentVersion) + val currentVersionMD5Sum = fetchMD5SumFor(flavor, currentVersion) val validationSucceeded = currentVersionMD5Sum == localMD5Sum - logger.info { - "isLocalWebUIValid: Validation " + + log.info { + "Validation " + "${if (validationSucceeded) "succeeded" else "failed"} - " + "md5: local= $localMD5Sum; expected= $currentVersionMD5Sum" } @@ -474,10 +524,13 @@ object WebInterfaceManager { } } - private suspend fun fetchMD5SumFor(version: String): String { + private suspend fun fetchMD5SumFor( + flavor: WebUIFlavor, + version: String, + ): String { return try { - executeWithRetry(KotlinLogging.logger("${logger.name} fetchMD5SumFor($version)"), { - network.client.newCall(GET("${getDownloadUrlFor(version)}/md5sum")).awaitSuccess().body.string().trim() + executeWithRetry(KotlinLogging.logger("${logger.name} fetchMD5SumFor(flavor= ${flavor.uiName}, version= $version)"), { + network.client.newCall(GET("${getDownloadUrlFor(flavor, version)}/md5sum")).awaitSuccess().body.string().trim() }) } catch (e: Exception) { "" @@ -489,36 +542,37 @@ object WebInterfaceManager { return versionString.substring(1).toInt() } - private suspend fun fetchPreviewVersion(): String { - return executeWithRetry(KotlinLogging.logger("${logger.name} fetchPreviewVersion"), { - val releaseInfoJson = network.client.newCall(GET(WebUIFlavor.WEBUI.latestReleaseInfoUrl)).awaitSuccess().body.string() + private suspend fun fetchPreviewVersion(flavor: WebUIFlavor): String { + return executeWithRetry(KotlinLogging.logger("${logger.name} fetchPreviewVersion(${flavor.uiName})"), { + val releaseInfoJson = network.client.newCall(GET(flavor.latestReleaseInfoUrl)).awaitSuccess().body.string() Json.decodeFromString(releaseInfoJson)["tag_name"]?.jsonPrimitive?.content ?: throw Exception("Failed to get the preview version tag") }) } - private suspend fun fetchServerMappingFile(): JsonArray { + private suspend fun fetchServerMappingFile(flavor: WebUIFlavor): JsonArray { return executeWithRetry( - KotlinLogging.logger("$logger fetchServerMappingFile"), + KotlinLogging.logger("$logger fetchServerMappingFile(${flavor.uiName})"), { json.parseToJsonElement( - network.client.newCall(GET(WebUIFlavor.WEBUI.versionMappingUrl)).awaitSuccess().body.string(), + network.client.newCall(GET(flavor.versionMappingUrl)).awaitSuccess().body.string(), ).jsonArray }, ) } - private suspend fun getLatestCompatibleVersion(): String { + private suspend fun getLatestCompatibleVersion(flavor: WebUIFlavor): String { if (WebUIChannel.doesConfigChannelEqual(WebUIChannel.BUNDLED)) { logger.debug { "getLatestCompatibleVersion: Channel is \"${WebUIChannel.BUNDLED}\", do not check for update" } return BuildConfig.WEBUI_TAG } val currentServerVersionNumber = extractVersion(BuildConfig.REVISION) - val webUIToServerVersionMappings = fetchServerMappingFile() + val webUIToServerVersionMappings = fetchServerMappingFile(flavor) logger.debug { "getLatestCompatibleVersion: " + + "flavor= ${flavor.uiName}, " + "webUIChannel= ${serverConfig.webUIChannel.value}, " + "currentServerVersion= ${BuildConfig.REVISION}, " + "mappingFile= $webUIToServerVersionMappings" @@ -545,7 +599,7 @@ object WebInterfaceManager { } if (webUIVersion == WebUIChannel.PREVIEW.name) { - webUIVersion = fetchPreviewVersion() + webUIVersion = fetchPreviewVersion(flavor) } val isCompatibleVersion = @@ -579,26 +633,32 @@ object WebInterfaceManager { } } - fun startDownloadInScope(version: String) { + fun startDownloadInScope( + flavor: WebUIFlavor, + version: String, + ) { scope.launch { - downloadVersion(version) + downloadVersion(flavor, version) } } - suspend fun downloadVersion(version: String) { + suspend fun downloadVersion( + flavor: WebUIFlavor, + version: String, + ) { emitStatus(version, DOWNLOADING, 0, immediate = true) try { - val webUIZip = "${WebUIFlavor.WEBUI.baseFileName}-$version.zip" + val webUIZip = "${flavor.baseFileName}-$version.zip" val webUIZipPath = "$tmpDir/$webUIZip" - val webUIZipURL = "${getDownloadUrlFor(version)}/$webUIZip" + val webUIZipURL = "${getDownloadUrlFor(flavor, version)}/$webUIZip" val log = - KotlinLogging.logger("${logger.name} downloadVersion(version= $version, flavor= ${serverConfig.webUIFlavor.value})") + KotlinLogging.logger("${logger.name} downloadVersion(version= $version, flavor= ${flavor.uiName})") log.info { "Downloading WebUI zip from the Internet..." } executeWithRetry(log, { - downloadVersionZipFile(webUIZipURL, webUIZipPath) { progress -> + downloadVersionZipFile(flavor, webUIZipURL, webUIZipPath) { progress -> emitStatus( version, DOWNLOADING, @@ -613,6 +673,8 @@ object WebInterfaceManager { extractDownload(webUIZipPath, applicationDirs.webUIRoot) log.info { "Extracting WebUI zip Done." } + setServedWebUIFlavor(flavor) + emitStatus(version, FINISHED, 100, immediate = true) serveWebUI() @@ -623,6 +685,7 @@ object WebInterfaceManager { } private suspend fun downloadVersionZipFile( + flavor: WebUIFlavor, url: String, filePath: String, updateProgress: (progress: Int) -> Unit, @@ -641,7 +704,7 @@ object WebInterfaceManager { connection.inputStream.buffered().use { inp -> var totalCount = 0 - print("downloadVersionZipFile: Download progress: % 00") + print("downloadVersionZipFile(${flavor.uiName}): Download progress: % 00") while (true) { val count = inp.read(data, 0, 1024) @@ -659,21 +722,24 @@ object WebInterfaceManager { updateProgress(percentage) } println() - logger.info { "downloadVersionZipFile: Downloading WebUI Done." } + logger.info { "downloadVersionZipFile(${flavor.uiName}): Downloading WebUI Done." } } } - if (!isDownloadValid(filePath)) { + if (!isDownloadValid(flavor, filePath)) { throw Exception("Download is invalid") } } - private suspend fun isDownloadValid(zipFilePath: String): Boolean { + private suspend fun isDownloadValid( + flavor: WebUIFlavor, + zipFilePath: String, + ): Boolean { val tempUnzippedWebUIFolderPath = zipFilePath.replace(".zip", "") extractDownload(zipFilePath, tempUnzippedWebUIFolderPath) - val isDownloadValid = isLocalWebUIValid(tempUnzippedWebUIFolderPath) + val isDownloadValid = isLocalWebUIValid(flavor, tempUnzippedWebUIFolderPath) File(tempUnzippedWebUIFolderPath).deleteRecursively() @@ -689,13 +755,16 @@ object WebInterfaceManager { } suspend fun isUpdateAvailable( + flavor: WebUIFlavor, currentVersion: String = getLocalVersion(), raiseError: Boolean = false, ): Pair { return try { - val latestCompatibleVersion = getLatestCompatibleVersion() - val isUpdateAvailable = latestCompatibleVersion != currentVersion + val isServedWebUIForCurrentFlavor = flavor.uiName == getServedWebUIFlavor().uiName + val latestCompatibleVersion = getLatestCompatibleVersion(flavor) + val isVersionUpdateAvailable = latestCompatibleVersion != currentVersion + val isUpdateAvailable = !isServedWebUIForCurrentFlavor || isVersionUpdateAvailable Pair(latestCompatibleVersion, isUpdateAvailable) } catch (e: Exception) { logger.warn(e) { "isUpdateAvailable: check failed due to" }