Feature/graphql web UI (#649)

* Add "server" to "checkForUpdate" logic names

* Use "webUIRoot" as default path for "getLocalVersion"

* Use local version as default version for "isUpdateAvailable"

* Return the version with the webUI update check

* Update WebinterfaceManager to be async

* Add query, mutation and subscription for webUI update

* Catch error and return default error value for missing local WebUI version
This commit is contained in:
schroda
2023-08-10 02:46:48 +02:00
committed by GitHub
parent 684bb1875c
commit 74ff112e7a
7 changed files with 308 additions and 61 deletions

View File

@@ -0,0 +1,62 @@
package suwayomi.tachidesk.graphql.mutations
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout
import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING
import suwayomi.tachidesk.graphql.types.UpdateState.FINISHED
import suwayomi.tachidesk.graphql.types.UpdateState.STOPPED
import suwayomi.tachidesk.graphql.types.WebUIUpdateInfo
import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.server.util.WebInterfaceManager
import java.util.concurrent.CompletableFuture
import kotlin.time.Duration.Companion.seconds
class InfoMutation {
data class WebUIUpdateInput(
val clientMutationId: String? = null
)
data class WebUIUpdatePayload(
val clientMutationId: String?,
val updateStatus: WebUIUpdateStatus
)
fun updateWebUI(input: WebUIUpdateInput): CompletableFuture<WebUIUpdatePayload> {
return future {
withTimeout(30.seconds) {
if (WebInterfaceManager.status.value.state === DOWNLOADING) {
return@withTimeout WebUIUpdatePayload(input.clientMutationId, WebInterfaceManager.status.value)
}
val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable()
if (!updateAvailable) {
return@withTimeout WebUIUpdatePayload(
input.clientMutationId,
WebUIUpdateStatus(
info = WebUIUpdateInfo(
channel = serverConfig.webUIChannel,
tag = version,
updateAvailable
),
state = STOPPED,
progress = 0
)
)
}
try {
WebInterfaceManager.startDownloadInScope(version)
} catch (e: Exception) {
// ignore since we use the status anyway
}
WebUIUpdatePayload(
input.clientMutationId,
updateStatus = WebInterfaceManager.status.first { it.state == DOWNLOADING }
)
}
}
}
}

View File

@@ -1,8 +1,12 @@
package suwayomi.tachidesk.graphql.queries package suwayomi.tachidesk.graphql.queries
import suwayomi.tachidesk.global.impl.AppUpdate import suwayomi.tachidesk.global.impl.AppUpdate
import suwayomi.tachidesk.graphql.types.WebUIUpdateInfo
import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus
import suwayomi.tachidesk.server.BuildConfig import suwayomi.tachidesk.server.BuildConfig
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.server.util.WebInterfaceManager
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
class InfoQuery { class InfoQuery {
@@ -28,17 +32,17 @@ class InfoQuery {
) )
} }
data class CheckForUpdatesPayload( data class CheckForServerUpdatesPayload(
/** [channel] mirrors [suwayomi.tachidesk.server.BuildConfig.BUILD_TYPE] */ /** [channel] mirrors [suwayomi.tachidesk.server.BuildConfig.BUILD_TYPE] */
val channel: String, val channel: String,
val tag: String, val tag: String,
val url: String val url: String
) )
fun checkForUpdates(): CompletableFuture<List<CheckForUpdatesPayload>> { fun checkForServerUpdates(): CompletableFuture<List<CheckForServerUpdatesPayload>> {
return future { return future {
AppUpdate.checkUpdate().map { AppUpdate.checkUpdate().map {
CheckForUpdatesPayload( CheckForServerUpdatesPayload(
channel = it.channel, channel = it.channel,
tag = it.tag, tag = it.tag,
url = it.url url = it.url
@@ -46,4 +50,19 @@ class InfoQuery {
} }
} }
} }
fun checkForWebUIUpdate(): CompletableFuture<WebUIUpdateInfo> {
return future {
val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable()
WebUIUpdateInfo(
channel = serverConfig.webUIChannel,
tag = version,
updateAvailable
)
}
}
fun getWebUIUpdateStatus(): WebUIUpdateStatus {
return WebInterfaceManager.status.value
}
} }

View File

@@ -18,6 +18,7 @@ import suwayomi.tachidesk.graphql.mutations.CategoryMutation
import suwayomi.tachidesk.graphql.mutations.ChapterMutation import suwayomi.tachidesk.graphql.mutations.ChapterMutation
import suwayomi.tachidesk.graphql.mutations.DownloadMutation import suwayomi.tachidesk.graphql.mutations.DownloadMutation
import suwayomi.tachidesk.graphql.mutations.ExtensionMutation import suwayomi.tachidesk.graphql.mutations.ExtensionMutation
import suwayomi.tachidesk.graphql.mutations.InfoMutation
import suwayomi.tachidesk.graphql.mutations.MangaMutation import suwayomi.tachidesk.graphql.mutations.MangaMutation
import suwayomi.tachidesk.graphql.mutations.MetaMutation import suwayomi.tachidesk.graphql.mutations.MetaMutation
import suwayomi.tachidesk.graphql.mutations.SourceMutation import suwayomi.tachidesk.graphql.mutations.SourceMutation
@@ -37,6 +38,7 @@ import suwayomi.tachidesk.graphql.server.primitives.GraphQLCursor
import suwayomi.tachidesk.graphql.server.primitives.GraphQLLongAsString import suwayomi.tachidesk.graphql.server.primitives.GraphQLLongAsString
import suwayomi.tachidesk.graphql.server.primitives.GraphQLUpload import suwayomi.tachidesk.graphql.server.primitives.GraphQLUpload
import suwayomi.tachidesk.graphql.subscriptions.DownloadSubscription import suwayomi.tachidesk.graphql.subscriptions.DownloadSubscription
import suwayomi.tachidesk.graphql.subscriptions.InfoSubscription
import suwayomi.tachidesk.graphql.subscriptions.UpdateSubscription import suwayomi.tachidesk.graphql.subscriptions.UpdateSubscription
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KType import kotlin.reflect.KType
@@ -74,6 +76,7 @@ val schema = toSchema(
TopLevelObject(ChapterMutation()), TopLevelObject(ChapterMutation()),
TopLevelObject(DownloadMutation()), TopLevelObject(DownloadMutation()),
TopLevelObject(ExtensionMutation()), TopLevelObject(ExtensionMutation()),
TopLevelObject(InfoMutation()),
TopLevelObject(MangaMutation()), TopLevelObject(MangaMutation()),
TopLevelObject(MetaMutation()), TopLevelObject(MetaMutation()),
TopLevelObject(SourceMutation()), TopLevelObject(SourceMutation()),
@@ -81,6 +84,7 @@ val schema = toSchema(
), ),
subscriptions = listOf( subscriptions = listOf(
TopLevelObject(DownloadSubscription()), TopLevelObject(DownloadSubscription()),
TopLevelObject(InfoSubscription()),
TopLevelObject(UpdateSubscription()) TopLevelObject(UpdateSubscription())
) )
) )

View File

@@ -0,0 +1,11 @@
package suwayomi.tachidesk.graphql.subscriptions
import kotlinx.coroutines.flow.Flow
import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus
import suwayomi.tachidesk.server.util.WebInterfaceManager
class InfoSubscription {
fun webUIUpdateStatusChange(): Flow<WebUIUpdateStatus> {
return WebInterfaceManager.status
}
}

View File

@@ -0,0 +1,20 @@
package suwayomi.tachidesk.graphql.types
data class WebUIUpdateInfo(
val channel: String,
val tag: String,
val updateAvailable: Boolean
)
enum class UpdateState {
STOPPED,
DOWNLOADING,
FINISHED,
ERROR
}
data class WebUIUpdateStatus(
val info: WebUIUpdateInfo,
val state: UpdateState,
val progress: Int
)

View File

@@ -19,6 +19,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.future.future import kotlinx.coroutines.future.future
import kotlinx.coroutines.runBlocking
import mu.KotlinLogging import mu.KotlinLogging
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
@@ -47,7 +48,9 @@ object JavalinSetup {
fun javalinSetup() { fun javalinSetup() {
val app = Javalin.create { config -> val app = Javalin.create { config ->
if (serverConfig.webUIEnabled) { if (serverConfig.webUIEnabled) {
WebInterfaceManager.setupWebUI() runBlocking {
WebInterfaceManager.setupWebUI()
}
logger.info { "Serving web static files for ${serverConfig.webUIFlavor}" } logger.info { "Serving web static files for ${serverConfig.webUIFlavor}" }
config.addStaticFiles(applicationDirs.webUIRoot, Location.EXTERNAL) config.addStaticFiles(applicationDirs.webUIRoot, Location.EXTERNAL)

View File

@@ -7,21 +7,44 @@ package suwayomi.tachidesk.server.util
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import mu.KLogger import mu.KLogger
import mu.KotlinLogging import mu.KotlinLogging
import net.lingala.zip4j.ZipFile import net.lingala.zip4j.ZipFile
import org.json.JSONArray
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.graphql.types.UpdateState
import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING
import suwayomi.tachidesk.graphql.types.UpdateState.ERROR
import suwayomi.tachidesk.graphql.types.UpdateState.FINISHED
import suwayomi.tachidesk.graphql.types.UpdateState.STOPPED
import suwayomi.tachidesk.graphql.types.WebUIUpdateInfo
import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.BuildConfig import suwayomi.tachidesk.server.BuildConfig
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.util.HAScheduler import suwayomi.tachidesk.util.HAScheduler
import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.net.HttpURLConnection import java.net.HttpURLConnection
@@ -31,6 +54,7 @@ import java.security.MessageDigest
import java.util.Date import java.util.Date
import java.util.prefs.Preferences import java.util.prefs.Preferences
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
private val tmpDir = System.getProperty("java.io.tmpdir") private val tmpDir = System.getProperty("java.io.tmpdir")
@@ -51,7 +75,12 @@ enum class WebUIChannel {
} }
} }
enum class WebUI(val repoUrl: String, val versionMappingUrl: String, val latestReleaseInfoUrl: String, val baseFileName: String) { enum class WebUI(
val repoUrl: String,
val versionMappingUrl: String,
val latestReleaseInfoUrl: String,
val baseFileName: String
) {
WEBUI( WEBUI(
"https://github.com/Suwayomi/Tachidesk-WebUI-preview", "https://github.com/Suwayomi/Tachidesk-WebUI-preview",
"https://raw.githubusercontent.com/Suwayomi/Tachidesk-WebUI/master/versionToServerVersionMapping.json", "https://raw.githubusercontent.com/Suwayomi/Tachidesk-WebUI/master/versionToServerVersionMapping.json",
@@ -64,12 +93,34 @@ const val DEFAULT_WEB_UI = "WebUI"
object WebInterfaceManager { object WebInterfaceManager {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private const val webUIPreviewVersion = "PREVIEW" private const val webUIPreviewVersion = "PREVIEW"
private const val lastWebUIUpdateCheckKey = "lastWebUIUpdateCheckKey" private const val lastWebUIUpdateCheckKey = "lastWebUIUpdateCheckKey"
private val preferences = Preferences.userNodeForPackage(WebInterfaceManager::class.java)
private val preferences = Preferences.userNodeForPackage(WebInterfaceManager::class.java)
private var currentUpdateTaskId: String = "" private var currentUpdateTaskId: String = ""
private val json: Json by injectLazy()
private val network: NetworkHelper by injectLazy()
private val notifyFlow =
MutableSharedFlow<WebUIUpdateStatus>(extraBufferCapacity = 1, onBufferOverflow = DROP_OLDEST)
val status = notifyFlow.sample(1.seconds)
.stateIn(
scope,
SharingStarted.Eagerly,
WebUIUpdateStatus(
info = WebUIUpdateInfo(
channel = serverConfig.webUIChannel,
tag = "",
updateAvailable = false
),
state = STOPPED,
progress = 0
)
)
init { init {
scheduleWebUIUpdateCheck() scheduleWebUIUpdateCheck()
} }
@@ -90,25 +141,36 @@ object WebInterfaceManager {
val lastAutomatedUpdate = preferences.getLong(lastWebUIUpdateCheckKey, System.currentTimeMillis()) val lastAutomatedUpdate = preferences.getLong(lastWebUIUpdateCheckKey, System.currentTimeMillis())
val task = { val task = {
logger.debug { "Checking for webUI update (channel= ${serverConfig.webUIChannel}, interval= ${serverConfig.webUIUpdateCheckInterval}h, lastAutomatedUpdate= ${Date(lastAutomatedUpdate)})" } logger.debug {
checkForUpdate() "Checking for webUI update (channel= ${serverConfig.webUIChannel}, interval= ${serverConfig.webUIUpdateCheckInterval}h, lastAutomatedUpdate= ${
Date(
lastAutomatedUpdate
)
})"
}
runBlocking {
checkForUpdate()
}
} }
val wasPreviousUpdateCheckTriggered = (System.currentTimeMillis() - lastAutomatedUpdate) < updateInterval.inWholeMilliseconds val wasPreviousUpdateCheckTriggered =
(System.currentTimeMillis() - lastAutomatedUpdate) < updateInterval.inWholeMilliseconds
if (!wasPreviousUpdateCheckTriggered) { if (!wasPreviousUpdateCheckTriggered) {
task() task()
} }
currentUpdateTaskId = HAScheduler.scheduleCron(task, "0 */${updateInterval.inWholeHours} * * *", "webUI-update-checker") currentUpdateTaskId =
HAScheduler.scheduleCron(task, "0 */${updateInterval.inWholeHours} * * *", "webUI-update-checker")
} }
fun setupWebUI() { suspend fun setupWebUI() {
if (serverConfig.webUIFlavor == "Custom") { if (serverConfig.webUIFlavor == "Custom") {
return return
} }
if (doesLocalWebUIExist(applicationDirs.webUIRoot)) { if (doesLocalWebUIExist(applicationDirs.webUIRoot)) {
val currentVersion = getLocalVersion(applicationDirs.webUIRoot) val currentVersion = getLocalVersion()
logger.info { "setupWebUI: found webUI files - flavor= ${serverConfig.webUIFlavor}, version= $currentVersion" } logger.info { "setupWebUI: found webUI files - flavor= ${serverConfig.webUIFlavor}, version= $currentVersion" }
@@ -124,7 +186,7 @@ object WebInterfaceManager {
// check if the bundled webUI version is a newer version than the current used version // 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 // this could be the case in case no compatible webUI version is available and a newer server version was installed
val shouldUpdateToBundledVersion = val shouldUpdateToBundledVersion =
serverConfig.webUIFlavor == DEFAULT_WEB_UI && extractVersion(getLocalVersion(applicationDirs.webUIRoot)) < extractVersion( serverConfig.webUIFlavor == DEFAULT_WEB_UI && extractVersion(getLocalVersion()) < extractVersion(
BuildConfig.WEBUI_TAG BuildConfig.WEBUI_TAG
) )
if (shouldUpdateToBundledVersion) { if (shouldUpdateToBundledVersion) {
@@ -147,7 +209,7 @@ object WebInterfaceManager {
/** /**
* Tries to download the latest compatible version for the selected webUI and falls back to the default webUI in case of errors. * Tries to download the latest compatible version for the selected webUI and falls back to the default webUI in case of errors.
*/ */
private fun doInitialSetup() { private suspend fun doInitialSetup() {
val isLocalWebUIValid = isLocalWebUIValid(applicationDirs.webUIRoot) val isLocalWebUIValid = isLocalWebUIValid(applicationDirs.webUIRoot)
/** /**
@@ -155,7 +217,7 @@ object WebInterfaceManager {
* *
* In case the download failed but the local webUI is valid the download is considered a success to prevent the fallback logic * In case the download failed but the local webUI is valid the download is considered a success to prevent the fallback logic
*/ */
val doDownload: (getVersion: () -> String) -> Boolean = { getVersion -> val doDownload: suspend (getVersion: suspend () -> String) -> Boolean = { getVersion ->
try { try {
downloadVersion(getVersion()) downloadVersion(getVersion())
true true
@@ -202,7 +264,8 @@ object WebInterfaceManager {
} }
private fun extractBundledWebUI() { private fun extractBundledWebUI() {
val resourceWebUI: InputStream = BuildConfig::class.java.getResourceAsStream("/WebUI.zip") ?: throw BundledWebUIMissing() val resourceWebUI: InputStream =
BuildConfig::class.java.getResourceAsStream("/WebUI.zip") ?: throw BundledWebUIMissing()
logger.info { "extractBundledWebUI: Using the bundled WebUI zip..." } logger.info { "extractBundledWebUI: Using the bundled WebUI zip..." }
@@ -219,11 +282,11 @@ object WebInterfaceManager {
extractDownload(webUIZipPath, applicationDirs.webUIRoot) extractDownload(webUIZipPath, applicationDirs.webUIRoot)
} }
private fun checkForUpdate() { private suspend fun checkForUpdate() {
preferences.putLong(lastWebUIUpdateCheckKey, System.currentTimeMillis()) preferences.putLong(lastWebUIUpdateCheckKey, System.currentTimeMillis())
val localVersion = getLocalVersion(applicationDirs.webUIRoot) val localVersion = getLocalVersion()
if (!isUpdateAvailable(localVersion)) { if (!isUpdateAvailable(localVersion).second) {
logger.debug { "checkForUpdate(${serverConfig.webUIFlavor}, $localVersion): local version is the latest one" } logger.debug { "checkForUpdate(${serverConfig.webUIFlavor}, $localVersion): local version is the latest one" }
return return
} }
@@ -243,8 +306,12 @@ object WebInterfaceManager {
return "$downloadSpecificVersionBaseUrl/$version" return "$downloadSpecificVersionBaseUrl/$version"
} }
private fun getLocalVersion(path: String): String { private fun getLocalVersion(path: String = applicationDirs.webUIRoot): String {
return File("$path/revision").readText().trim() return try {
File("$path/revision").readText().trim()
} catch (e: Exception) {
"r-1"
}
} }
private fun doesLocalWebUIExist(path: String): Boolean { private fun doesLocalWebUIExist(path: String): Boolean {
@@ -253,7 +320,7 @@ object WebInterfaceManager {
return webUIRevisionFile.exists() return webUIRevisionFile.exists()
} }
private fun isLocalWebUIValid(path: String): Boolean { private suspend fun isLocalWebUIValid(path: String): Boolean {
if (!doesLocalWebUIExist(path)) { if (!doesLocalWebUIExist(path)) {
return false return false
} }
@@ -287,7 +354,12 @@ object WebInterfaceManager {
return digest.toHex() return digest.toHex()
} }
private fun <T> executeWithRetry(log: KLogger, execute: () -> T, maxRetries: Int = 3, retryCount: Int = 0): T { private suspend fun <T> executeWithRetry(
log: KLogger,
execute: suspend () -> T,
maxRetries: Int = 3,
retryCount: Int = 0
): T {
try { try {
return execute() return execute()
} catch (e: Exception) { } catch (e: Exception) {
@@ -301,11 +373,10 @@ object WebInterfaceManager {
} }
} }
private fun fetchMD5SumFor(version: String): String { private suspend fun fetchMD5SumFor(version: String): String {
return try { return try {
executeWithRetry(KotlinLogging.logger("${logger.name} fetchMD5SumFor($version)"), { executeWithRetry(KotlinLogging.logger("${logger.name} fetchMD5SumFor($version)"), {
val url = "${getDownloadUrlFor(version)}/md5sum" network.client.newCall(GET("${getDownloadUrlFor(version)}/md5sum")).await().body.string().trim()
URL(url).readText().trim()
}) })
} catch (e: Exception) { } catch (e: Exception) {
"" ""
@@ -317,18 +388,26 @@ object WebInterfaceManager {
return versionString.substring(1).toInt() return versionString.substring(1).toInt()
} }
private fun fetchPreviewVersion(): String { private suspend fun fetchPreviewVersion(): String {
return executeWithRetry(KotlinLogging.logger("${logger.name} fetchPreviewVersion"), { return executeWithRetry(KotlinLogging.logger("${logger.name} fetchPreviewVersion"), {
val releaseInfoJson = URL(WebUI.WEBUI.latestReleaseInfoUrl).readText() val releaseInfoJson = network.client.newCall(GET(WebUI.WEBUI.latestReleaseInfoUrl)).await().body.string()
Json.decodeFromString<JsonObject>(releaseInfoJson)["tag_name"]?.jsonPrimitive?.content ?: throw Exception("Failed to get the preview version tag") Json.decodeFromString<JsonObject>(releaseInfoJson)["tag_name"]?.jsonPrimitive?.content
?: throw Exception("Failed to get the preview version tag")
}) })
} }
private fun fetchServerMappingFile(): JSONArray { private suspend fun fetchServerMappingFile(): JsonArray {
return executeWithRetry(KotlinLogging.logger("$logger fetchServerMappingFile"), { JSONArray(URL(WebUI.WEBUI.versionMappingUrl).readText()) }) return executeWithRetry(
KotlinLogging.logger("$logger fetchServerMappingFile"),
{
json.parseToJsonElement(
network.client.newCall(GET(WebUI.WEBUI.versionMappingUrl)).await().body.string()
).jsonArray
}
)
} }
private fun getLatestCompatibleVersion(): String { private suspend fun getLatestCompatibleVersion(): String {
if (WebUIChannel.doesConfigChannelEqual(WebUIChannel.BUNDLED)) { if (WebUIChannel.doesConfigChannelEqual(WebUIChannel.BUNDLED)) {
logger.debug { "getLatestCompatibleVersion: Channel is \"${WebUIChannel.BUNDLED}\", do not check for update" } logger.debug { "getLatestCompatibleVersion: Channel is \"${WebUIChannel.BUNDLED}\", do not check for update" }
return BuildConfig.WEBUI_TAG return BuildConfig.WEBUI_TAG
@@ -339,13 +418,14 @@ object WebInterfaceManager {
logger.debug { "getLatestCompatibleVersion: webUIChannel= ${serverConfig.webUIChannel}, currentServerVersion= ${BuildConfig.REVISION}, mappingFile= $webUIToServerVersionMappings" } logger.debug { "getLatestCompatibleVersion: webUIChannel= ${serverConfig.webUIChannel}, currentServerVersion= ${BuildConfig.REVISION}, mappingFile= $webUIToServerVersionMappings" }
for (i in 0 until webUIToServerVersionMappings.length()) { for (i in 0 until webUIToServerVersionMappings.size) {
val webUIToServerVersionEntry = webUIToServerVersionMappings.getJSONObject(i) val webUIToServerVersionEntry = webUIToServerVersionMappings[i].jsonObject
var webUIVersion = webUIToServerVersionEntry.getString("uiVersion") var webUIVersion = webUIToServerVersionEntry["uiVersion"].toString()
val minServerVersionString = webUIToServerVersionEntry.getString("serverVersion") val minServerVersionString = webUIToServerVersionEntry["serverVersion"]?.jsonPrimitive?.content ?: throw Exception("Invalid mappingFile")
val minServerVersionNumber = extractVersion(minServerVersionString) val minServerVersionNumber = extractVersion(minServerVersionString)
val ignorePreviewVersion = !WebUIChannel.doesConfigChannelEqual(WebUIChannel.PREVIEW) && webUIVersion == webUIPreviewVersion val ignorePreviewVersion =
!WebUIChannel.doesConfigChannelEqual(WebUIChannel.PREVIEW) && webUIVersion == webUIPreviewVersion
if (ignorePreviewVersion) { if (ignorePreviewVersion) {
continue continue
} else { } else {
@@ -361,24 +441,68 @@ object WebInterfaceManager {
throw Exception("No compatible webUI version found") throw Exception("No compatible webUI version found")
} }
fun downloadVersion(version: String) { private fun emitStatus(version: String, state: UpdateState, progress: Int) {
val webUIZip = "${WebUI.WEBUI.baseFileName}-$version.zip" scope.launch {
val webUIZipPath = "$tmpDir/$webUIZip" notifyFlow.emit(
val webUIZipURL = "${getDownloadUrlFor(version)}/$webUIZip" WebUIUpdateStatus(
info = WebUIUpdateInfo(
val log = KotlinLogging.logger("${logger.name} downloadVersion(version= $version, flavor= ${serverConfig.webUIFlavor})") channel = serverConfig.webUIChannel,
log.info { "Downloading WebUI zip from the Internet..." } tag = version,
updateAvailable = true
executeWithRetry(log, { downloadVersionZipFile(webUIZipURL, webUIZipPath) }) ),
File(applicationDirs.webUIRoot).deleteRecursively() state,
progress
// extract webUI zip )
log.info { "Extracting WebUI zip..." } )
extractDownload(webUIZipPath, applicationDirs.webUIRoot) }
log.info { "Extracting WebUI zip Done." }
} }
private fun downloadVersionZipFile(url: String, filePath: String) { fun startDownloadInScope(version: String) {
scope.launch {
downloadVersion(version)
}
}
suspend fun downloadVersion(version: String) {
emitStatus(version, DOWNLOADING, 0)
try {
val webUIZip = "${WebUI.WEBUI.baseFileName}-$version.zip"
val webUIZipPath = "$tmpDir/$webUIZip"
val webUIZipURL = "${getDownloadUrlFor(version)}/$webUIZip"
val log =
KotlinLogging.logger("${logger.name} downloadVersion(version= $version, flavor= ${serverConfig.webUIFlavor})")
log.info { "Downloading WebUI zip from the Internet..." }
executeWithRetry(log, {
downloadVersionZipFile(webUIZipURL, webUIZipPath) { progress ->
emitStatus(
version,
DOWNLOADING,
progress
)
}
})
File(applicationDirs.webUIRoot).deleteRecursively()
// extract webUI zip
log.info { "Extracting WebUI zip..." }
extractDownload(webUIZipPath, applicationDirs.webUIRoot)
log.info { "Extracting WebUI zip Done." }
emitStatus(version, FINISHED, 100)
} catch (e: Exception) {
emitStatus(version, ERROR, 0)
throw e
}
}
private suspend fun downloadVersionZipFile(
url: String,
filePath: String,
updateProgress: (progress: Int) -> Unit
) {
val zipFile = File(filePath) val zipFile = File(filePath)
zipFile.delete() zipFile.delete()
@@ -402,11 +526,13 @@ object WebInterfaceManager {
} }
totalCount += count totalCount += count
val percentage = val percentage = (totalCount.toFloat() / contentLength * 100).toInt()
(totalCount.toFloat() / contentLength * 100).toInt().toString().padStart(2, '0') val percentageStr = percentage.toString().padStart(2, '0')
print("\b\b$percentage") print("\b\b$percentageStr")
webUIZipFileOut.write(data, 0, count) webUIZipFileOut.write(data, 0, count)
updateProgress(percentage)
} }
println() println()
logger.info { "downloadVersionZipFile: Downloading WebUI Done." } logger.info { "downloadVersionZipFile: Downloading WebUI Done." }
@@ -418,7 +544,7 @@ object WebInterfaceManager {
} }
} }
private fun isDownloadValid(zipFileName: String, zipFilePath: String): Boolean { private suspend fun isDownloadValid(zipFileName: String, zipFilePath: String): Boolean {
val tempUnzippedWebUIFolderPath = zipFileName.replace(".zip", "") val tempUnzippedWebUIFolderPath = zipFileName.replace(".zip", "")
extractDownload(zipFilePath, tempUnzippedWebUIFolderPath) extractDownload(zipFilePath, tempUnzippedWebUIFolderPath)
@@ -435,13 +561,15 @@ object WebInterfaceManager {
ZipFile(zipFilePath).use { it.extractAll(targetPath) } ZipFile(zipFilePath).use { it.extractAll(targetPath) }
} }
fun isUpdateAvailable(currentVersion: String): Boolean { suspend fun isUpdateAvailable(currentVersion: String = getLocalVersion()): Pair<String, Boolean> {
return try { return try {
val latestCompatibleVersion = getLatestCompatibleVersion() val latestCompatibleVersion = getLatestCompatibleVersion()
latestCompatibleVersion != currentVersion val isUpdateAvailable = latestCompatibleVersion != currentVersion
Pair(latestCompatibleVersion, isUpdateAvailable)
} catch (e: Exception) { } catch (e: Exception) {
logger.warn(e) { "isUpdateAvailable: check failed due to" } logger.warn(e) { "isUpdateAvailable: check failed due to" }
false Pair("", false)
} }
} }
} }