mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-10 23:02:06 +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
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user