mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-10 06:42:07 +01:00
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:
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
package suwayomi.tachidesk.graphql.queries
|
||||
|
||||
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.JavalinSetup.future
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import suwayomi.tachidesk.server.util.WebInterfaceManager
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class InfoQuery {
|
||||
@@ -28,17 +32,17 @@ class InfoQuery {
|
||||
)
|
||||
}
|
||||
|
||||
data class CheckForUpdatesPayload(
|
||||
data class CheckForServerUpdatesPayload(
|
||||
/** [channel] mirrors [suwayomi.tachidesk.server.BuildConfig.BUILD_TYPE] */
|
||||
val channel: String,
|
||||
val tag: String,
|
||||
val url: String
|
||||
)
|
||||
|
||||
fun checkForUpdates(): CompletableFuture<List<CheckForUpdatesPayload>> {
|
||||
fun checkForServerUpdates(): CompletableFuture<List<CheckForServerUpdatesPayload>> {
|
||||
return future {
|
||||
AppUpdate.checkUpdate().map {
|
||||
CheckForUpdatesPayload(
|
||||
CheckForServerUpdatesPayload(
|
||||
channel = it.channel,
|
||||
tag = it.tag,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import suwayomi.tachidesk.graphql.mutations.CategoryMutation
|
||||
import suwayomi.tachidesk.graphql.mutations.ChapterMutation
|
||||
import suwayomi.tachidesk.graphql.mutations.DownloadMutation
|
||||
import suwayomi.tachidesk.graphql.mutations.ExtensionMutation
|
||||
import suwayomi.tachidesk.graphql.mutations.InfoMutation
|
||||
import suwayomi.tachidesk.graphql.mutations.MangaMutation
|
||||
import suwayomi.tachidesk.graphql.mutations.MetaMutation
|
||||
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.GraphQLUpload
|
||||
import suwayomi.tachidesk.graphql.subscriptions.DownloadSubscription
|
||||
import suwayomi.tachidesk.graphql.subscriptions.InfoSubscription
|
||||
import suwayomi.tachidesk.graphql.subscriptions.UpdateSubscription
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KType
|
||||
@@ -74,6 +76,7 @@ val schema = toSchema(
|
||||
TopLevelObject(ChapterMutation()),
|
||||
TopLevelObject(DownloadMutation()),
|
||||
TopLevelObject(ExtensionMutation()),
|
||||
TopLevelObject(InfoMutation()),
|
||||
TopLevelObject(MangaMutation()),
|
||||
TopLevelObject(MetaMutation()),
|
||||
TopLevelObject(SourceMutation()),
|
||||
@@ -81,6 +84,7 @@ val schema = toSchema(
|
||||
),
|
||||
subscriptions = listOf(
|
||||
TopLevelObject(DownloadSubscription()),
|
||||
TopLevelObject(InfoSubscription()),
|
||||
TopLevelObject(UpdateSubscription())
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -19,6 +19,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.future.future
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mu.KotlinLogging
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
@@ -47,7 +48,9 @@ object JavalinSetup {
|
||||
fun javalinSetup() {
|
||||
val app = Javalin.create { config ->
|
||||
if (serverConfig.webUIEnabled) {
|
||||
WebInterfaceManager.setupWebUI()
|
||||
runBlocking {
|
||||
WebInterfaceManager.setupWebUI()
|
||||
}
|
||||
|
||||
logger.info { "Serving web static files for ${serverConfig.webUIFlavor}" }
|
||||
config.addStaticFiles(applicationDirs.webUIRoot, Location.EXTERNAL)
|
||||
|
||||
@@ -7,21 +7,44 @@ package suwayomi.tachidesk.server.util
|
||||
* 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 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.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import mu.KLogger
|
||||
import mu.KotlinLogging
|
||||
import net.lingala.zip4j.ZipFile
|
||||
import org.json.JSONArray
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
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.BuildConfig
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import suwayomi.tachidesk.util.HAScheduler
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.net.HttpURLConnection
|
||||
@@ -31,6 +54,7 @@ import java.security.MessageDigest
|
||||
import java.util.Date
|
||||
import java.util.prefs.Preferences
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||
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(
|
||||
"https://github.com/Suwayomi/Tachidesk-WebUI-preview",
|
||||
"https://raw.githubusercontent.com/Suwayomi/Tachidesk-WebUI/master/versionToServerVersionMapping.json",
|
||||
@@ -64,12 +93,34 @@ const val DEFAULT_WEB_UI = "WebUI"
|
||||
|
||||
object WebInterfaceManager {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
private const val webUIPreviewVersion = "PREVIEW"
|
||||
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 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 {
|
||||
scheduleWebUIUpdateCheck()
|
||||
}
|
||||
@@ -90,25 +141,36 @@ object WebInterfaceManager {
|
||||
val lastAutomatedUpdate = preferences.getLong(lastWebUIUpdateCheckKey, System.currentTimeMillis())
|
||||
|
||||
val task = {
|
||||
logger.debug { "Checking for webUI update (channel= ${serverConfig.webUIChannel}, interval= ${serverConfig.webUIUpdateCheckInterval}h, lastAutomatedUpdate= ${Date(lastAutomatedUpdate)})" }
|
||||
checkForUpdate()
|
||||
logger.debug {
|
||||
"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) {
|
||||
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") {
|
||||
return
|
||||
}
|
||||
|
||||
if (doesLocalWebUIExist(applicationDirs.webUIRoot)) {
|
||||
val currentVersion = getLocalVersion(applicationDirs.webUIRoot)
|
||||
val currentVersion = getLocalVersion()
|
||||
|
||||
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
|
||||
// this could be the case in case no compatible webUI version is available and a newer server version was installed
|
||||
val shouldUpdateToBundledVersion =
|
||||
serverConfig.webUIFlavor == DEFAULT_WEB_UI && extractVersion(getLocalVersion(applicationDirs.webUIRoot)) < extractVersion(
|
||||
serverConfig.webUIFlavor == DEFAULT_WEB_UI && extractVersion(getLocalVersion()) < extractVersion(
|
||||
BuildConfig.WEBUI_TAG
|
||||
)
|
||||
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.
|
||||
*/
|
||||
private fun doInitialSetup() {
|
||||
private suspend fun doInitialSetup() {
|
||||
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
|
||||
*/
|
||||
val doDownload: (getVersion: () -> String) -> Boolean = { getVersion ->
|
||||
val doDownload: suspend (getVersion: suspend () -> String) -> Boolean = { getVersion ->
|
||||
try {
|
||||
downloadVersion(getVersion())
|
||||
true
|
||||
@@ -202,7 +264,8 @@ object WebInterfaceManager {
|
||||
}
|
||||
|
||||
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..." }
|
||||
|
||||
@@ -219,11 +282,11 @@ object WebInterfaceManager {
|
||||
extractDownload(webUIZipPath, applicationDirs.webUIRoot)
|
||||
}
|
||||
|
||||
private fun checkForUpdate() {
|
||||
private suspend fun checkForUpdate() {
|
||||
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" }
|
||||
return
|
||||
}
|
||||
@@ -243,8 +306,12 @@ object WebInterfaceManager {
|
||||
return "$downloadSpecificVersionBaseUrl/$version"
|
||||
}
|
||||
|
||||
private fun getLocalVersion(path: String): String {
|
||||
return File("$path/revision").readText().trim()
|
||||
private fun getLocalVersion(path: String = applicationDirs.webUIRoot): String {
|
||||
return try {
|
||||
File("$path/revision").readText().trim()
|
||||
} catch (e: Exception) {
|
||||
"r-1"
|
||||
}
|
||||
}
|
||||
|
||||
private fun doesLocalWebUIExist(path: String): Boolean {
|
||||
@@ -253,7 +320,7 @@ object WebInterfaceManager {
|
||||
return webUIRevisionFile.exists()
|
||||
}
|
||||
|
||||
private fun isLocalWebUIValid(path: String): Boolean {
|
||||
private suspend fun isLocalWebUIValid(path: String): Boolean {
|
||||
if (!doesLocalWebUIExist(path)) {
|
||||
return false
|
||||
}
|
||||
@@ -287,7 +354,12 @@ object WebInterfaceManager {
|
||||
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 {
|
||||
return execute()
|
||||
} catch (e: Exception) {
|
||||
@@ -301,11 +373,10 @@ object WebInterfaceManager {
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchMD5SumFor(version: String): String {
|
||||
private suspend fun fetchMD5SumFor(version: String): String {
|
||||
return try {
|
||||
executeWithRetry(KotlinLogging.logger("${logger.name} fetchMD5SumFor($version)"), {
|
||||
val url = "${getDownloadUrlFor(version)}/md5sum"
|
||||
URL(url).readText().trim()
|
||||
network.client.newCall(GET("${getDownloadUrlFor(version)}/md5sum")).await().body.string().trim()
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
@@ -317,18 +388,26 @@ object WebInterfaceManager {
|
||||
return versionString.substring(1).toInt()
|
||||
}
|
||||
|
||||
private fun fetchPreviewVersion(): String {
|
||||
private suspend fun fetchPreviewVersion(): String {
|
||||
return executeWithRetry(KotlinLogging.logger("${logger.name} fetchPreviewVersion"), {
|
||||
val releaseInfoJson = URL(WebUI.WEBUI.latestReleaseInfoUrl).readText()
|
||||
Json.decodeFromString<JsonObject>(releaseInfoJson)["tag_name"]?.jsonPrimitive?.content ?: throw Exception("Failed to get the preview version tag")
|
||||
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")
|
||||
})
|
||||
}
|
||||
|
||||
private fun fetchServerMappingFile(): JSONArray {
|
||||
return executeWithRetry(KotlinLogging.logger("$logger fetchServerMappingFile"), { JSONArray(URL(WebUI.WEBUI.versionMappingUrl).readText()) })
|
||||
private suspend fun fetchServerMappingFile(): JsonArray {
|
||||
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)) {
|
||||
logger.debug { "getLatestCompatibleVersion: Channel is \"${WebUIChannel.BUNDLED}\", do not check for update" }
|
||||
return BuildConfig.WEBUI_TAG
|
||||
@@ -339,13 +418,14 @@ object WebInterfaceManager {
|
||||
|
||||
logger.debug { "getLatestCompatibleVersion: webUIChannel= ${serverConfig.webUIChannel}, currentServerVersion= ${BuildConfig.REVISION}, mappingFile= $webUIToServerVersionMappings" }
|
||||
|
||||
for (i in 0 until webUIToServerVersionMappings.length()) {
|
||||
val webUIToServerVersionEntry = webUIToServerVersionMappings.getJSONObject(i)
|
||||
var webUIVersion = webUIToServerVersionEntry.getString("uiVersion")
|
||||
val minServerVersionString = webUIToServerVersionEntry.getString("serverVersion")
|
||||
for (i in 0 until webUIToServerVersionMappings.size) {
|
||||
val webUIToServerVersionEntry = webUIToServerVersionMappings[i].jsonObject
|
||||
var webUIVersion = webUIToServerVersionEntry["uiVersion"].toString()
|
||||
val minServerVersionString = webUIToServerVersionEntry["serverVersion"]?.jsonPrimitive?.content ?: throw Exception("Invalid mappingFile")
|
||||
val minServerVersionNumber = extractVersion(minServerVersionString)
|
||||
|
||||
val ignorePreviewVersion = !WebUIChannel.doesConfigChannelEqual(WebUIChannel.PREVIEW) && webUIVersion == webUIPreviewVersion
|
||||
val ignorePreviewVersion =
|
||||
!WebUIChannel.doesConfigChannelEqual(WebUIChannel.PREVIEW) && webUIVersion == webUIPreviewVersion
|
||||
if (ignorePreviewVersion) {
|
||||
continue
|
||||
} else {
|
||||
@@ -361,24 +441,68 @@ object WebInterfaceManager {
|
||||
throw Exception("No compatible webUI version found")
|
||||
}
|
||||
|
||||
fun downloadVersion(version: String) {
|
||||
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) })
|
||||
File(applicationDirs.webUIRoot).deleteRecursively()
|
||||
|
||||
// extract webUI zip
|
||||
log.info { "Extracting WebUI zip..." }
|
||||
extractDownload(webUIZipPath, applicationDirs.webUIRoot)
|
||||
log.info { "Extracting WebUI zip Done." }
|
||||
private fun emitStatus(version: String, state: UpdateState, progress: Int) {
|
||||
scope.launch {
|
||||
notifyFlow.emit(
|
||||
WebUIUpdateStatus(
|
||||
info = WebUIUpdateInfo(
|
||||
channel = serverConfig.webUIChannel,
|
||||
tag = version,
|
||||
updateAvailable = true
|
||||
),
|
||||
state,
|
||||
progress
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
zipFile.delete()
|
||||
|
||||
@@ -402,11 +526,13 @@ object WebInterfaceManager {
|
||||
}
|
||||
|
||||
totalCount += count
|
||||
val percentage =
|
||||
(totalCount.toFloat() / contentLength * 100).toInt().toString().padStart(2, '0')
|
||||
print("\b\b$percentage")
|
||||
val percentage = (totalCount.toFloat() / contentLength * 100).toInt()
|
||||
val percentageStr = percentage.toString().padStart(2, '0')
|
||||
print("\b\b$percentageStr")
|
||||
|
||||
webUIZipFileOut.write(data, 0, count)
|
||||
|
||||
updateProgress(percentage)
|
||||
}
|
||||
println()
|
||||
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", "")
|
||||
|
||||
extractDownload(zipFilePath, tempUnzippedWebUIFolderPath)
|
||||
@@ -435,13 +561,15 @@ object WebInterfaceManager {
|
||||
ZipFile(zipFilePath).use { it.extractAll(targetPath) }
|
||||
}
|
||||
|
||||
fun isUpdateAvailable(currentVersion: String): Boolean {
|
||||
suspend fun isUpdateAvailable(currentVersion: String = getLocalVersion()): Pair<String, Boolean> {
|
||||
return try {
|
||||
val latestCompatibleVersion = getLatestCompatibleVersion()
|
||||
latestCompatibleVersion != currentVersion
|
||||
val isUpdateAvailable = latestCompatibleVersion != currentVersion
|
||||
|
||||
Pair(latestCompatibleVersion, isUpdateAvailable)
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "isUpdateAvailable: check failed due to" }
|
||||
false
|
||||
Pair("", false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user