diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt new file mode 100644 index 00000000..76e57ba9 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt @@ -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 { + 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 } + ) + } + } + } +} 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 ef51a08b..c00d68fc 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt @@ -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> { + fun checkForServerUpdates(): CompletableFuture> { 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 { + return future { + val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable() + WebUIUpdateInfo( + channel = serverConfig.webUIChannel, + tag = version, + updateAvailable + ) + } + } + + fun getWebUIUpdateStatus(): WebUIUpdateStatus { + return WebInterfaceManager.status.value + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt index 7f6998a8..1bde0389 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -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()) ) ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/InfoSubscription.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/InfoSubscription.kt new file mode 100644 index 00000000..54afc145 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/InfoSubscription.kt @@ -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 { + return WebInterfaceManager.status + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/WebUIUpdateType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/WebUIUpdateType.kt new file mode 100644 index 00000000..28e1081e --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/WebUIUpdateType.kt @@ -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 +) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt index 28ce98d0..be26cb0a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt @@ -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) 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 2c6f9856..4d75761d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt @@ -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() 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(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 executeWithRetry(log: KLogger, execute: () -> T, maxRetries: Int = 3, retryCount: Int = 0): T { + private suspend fun 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(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(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 { 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) } } }