mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-04 03:14:40 -05:00
Fix/webui setup failure in case bundled webui is missing (#625)
* Rename functions * Require version to be passed to "downloadVersion" Makes it possible to download different versions than the latest compatible one with retry functionality * Fallback to downloading bundled webUI in case it's missing In case no download was possible and the fallback to the bundled version also failed due to it not existing, try to download the version of the bundled version as a last resort. * Handle exception of "getLatestCompatibleVersion" * Move validation of download to actual download function * Extract retry logic into function * Retry every fetch up to 3 times * Log full exception and change log level
This commit is contained in:
@@ -11,6 +11,7 @@ import kotlinx.serialization.decodeFromString
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
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.json.JSONArray
|
||||||
@@ -36,6 +37,8 @@ private val tmpDir = System.getProperty("java.io.tmpdir")
|
|||||||
|
|
||||||
private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
|
private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
|
||||||
|
|
||||||
|
class BundledWebUIMissing : Exception("No bundled webUI version found")
|
||||||
|
|
||||||
enum class WebUIChannel {
|
enum class WebUIChannel {
|
||||||
BUNDLED, // the default webUI version bundled with the server release
|
BUNDLED, // the default webUI version bundled with the server release
|
||||||
STABLE,
|
STABLE,
|
||||||
@@ -147,16 +150,17 @@ 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 = {
|
val doDownload: (getVersion: () -> String) -> Boolean = { getVersion ->
|
||||||
try {
|
try {
|
||||||
downloadLatestCompatibleVersion()
|
downloadVersion(getVersion())
|
||||||
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
false
|
false
|
||||||
} || isLocalWebUIValid
|
} || isLocalWebUIValid
|
||||||
}
|
}
|
||||||
|
|
||||||
// download the latest compatible version for the current selected webUI
|
// download the latest compatible version for the current selected webUI
|
||||||
val fallbackToDefaultWebUI = !doDownload()
|
val fallbackToDefaultWebUI = !doDownload() { getLatestCompatibleVersion() }
|
||||||
if (!fallbackToDefaultWebUI) {
|
if (!fallbackToDefaultWebUI) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -166,7 +170,7 @@ object WebInterfaceManager {
|
|||||||
|
|
||||||
serverConfig.webUIFlavor = DEFAULT_WEB_UI
|
serverConfig.webUIFlavor = DEFAULT_WEB_UI
|
||||||
|
|
||||||
val fallbackToBundledVersion = !doDownload()
|
val fallbackToBundledVersion = !doDownload() { getLatestCompatibleVersion() }
|
||||||
if (!fallbackToBundledVersion) {
|
if (!fallbackToBundledVersion) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -174,11 +178,21 @@ object WebInterfaceManager {
|
|||||||
|
|
||||||
logger.warn { "doInitialSetup: fallback to bundled default webUI \"$DEFAULT_WEB_UI\"" }
|
logger.warn { "doInitialSetup: fallback to bundled default webUI \"$DEFAULT_WEB_UI\"" }
|
||||||
|
|
||||||
extractBundledWebUI()
|
try {
|
||||||
|
extractBundledWebUI()
|
||||||
|
return
|
||||||
|
} catch (e: BundledWebUIMissing) {
|
||||||
|
logger.warn(e) { "doInitialSetup: fallback to downloading the version of the bundled webUI" }
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadFailed = !doDownload() { BuildConfig.WEBUI_TAG }
|
||||||
|
if (downloadFailed) {
|
||||||
|
throw Exception("Unable to setup a webUI")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractBundledWebUI() {
|
private fun extractBundledWebUI() {
|
||||||
val resourceWebUI: InputStream = BuildConfig::class.java.getResourceAsStream("/WebUI.zip") ?: throw Error("extractBundledWebUI: No bundled webUI version found")
|
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..." }
|
||||||
|
|
||||||
@@ -205,7 +219,11 @@ object WebInterfaceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info { "checkForUpdate(${serverConfig.webUIFlavor}, $localVersion): An update is available, starting download..." }
|
logger.info { "checkForUpdate(${serverConfig.webUIFlavor}, $localVersion): An update is available, starting download..." }
|
||||||
downloadLatestCompatibleVersion()
|
try {
|
||||||
|
downloadVersion(getLatestCompatibleVersion())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn(e) { "checkForUpdate: failed due to" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDownloadUrlFor(version: String): String {
|
private fun getDownloadUrlFor(version: String): String {
|
||||||
@@ -259,10 +277,26 @@ object WebInterfaceManager {
|
|||||||
return digest.toHex()
|
return digest.toHex()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun <T> executeWithRetry(log: KLogger, execute: () -> T, maxRetries: Int = 3, retryCount: Int = 0): T {
|
||||||
|
try {
|
||||||
|
return execute()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.warn(e) { "(retry $retryCount/$maxRetries) failed due to" }
|
||||||
|
|
||||||
|
if (retryCount < maxRetries) {
|
||||||
|
return executeWithRetry(log, execute, maxRetries, retryCount + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun fetchMD5SumFor(version: String): String {
|
private fun fetchMD5SumFor(version: String): String {
|
||||||
return try {
|
return try {
|
||||||
val url = "${getDownloadUrlFor(version)}/md5sum"
|
executeWithRetry(KotlinLogging.logger("${logger.name} fetchMD5SumFor($version)"), {
|
||||||
URL(url).readText().trim()
|
val url = "${getDownloadUrlFor(version)}/md5sum"
|
||||||
|
URL(url).readText().trim()
|
||||||
|
})
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
@@ -274,8 +308,14 @@ object WebInterfaceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchPreviewVersion(): String {
|
private fun fetchPreviewVersion(): String {
|
||||||
val releaseInfoJson = URL(WebUI.WEBUI.latestReleaseInfoUrl).readText()
|
return executeWithRetry(KotlinLogging.logger("${logger.name} fetchPreviewVersion"), {
|
||||||
return Json.decodeFromString<JsonObject>(releaseInfoJson)["tag_name"]?.jsonPrimitive?.content ?: throw Exception("Failed to get the preview version tag")
|
val releaseInfoJson = URL(WebUI.WEBUI.latestReleaseInfoUrl).readText()
|
||||||
|
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 fun getLatestCompatibleVersion(): String {
|
private fun getLatestCompatibleVersion(): String {
|
||||||
@@ -285,7 +325,7 @@ object WebInterfaceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val currentServerVersionNumber = extractVersion(BuildConfig.REVISION)
|
val currentServerVersionNumber = extractVersion(BuildConfig.REVISION)
|
||||||
val webUIToServerVersionMappings = JSONArray(URL(WebUI.WEBUI.versionMappingUrl).readText())
|
val webUIToServerVersionMappings = fetchServerMappingFile()
|
||||||
|
|
||||||
logger.debug { "getLatestCompatibleVersion: webUIChannel= ${serverConfig.webUIChannel}, currentServerVersion= ${BuildConfig.REVISION}, mappingFile= $webUIToServerVersionMappings" }
|
logger.debug { "getLatestCompatibleVersion: webUIChannel= ${serverConfig.webUIChannel}, currentServerVersion= ${BuildConfig.REVISION}, mappingFile= $webUIToServerVersionMappings" }
|
||||||
|
|
||||||
@@ -311,45 +351,27 @@ object WebInterfaceManager {
|
|||||||
throw Exception("No compatible webUI version found")
|
throw Exception("No compatible webUI version found")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadLatestCompatibleVersion(retryCount: Int = 0): Boolean {
|
fun downloadVersion(version: String) {
|
||||||
val latestCompatibleVersion = getLatestCompatibleVersion()
|
val webUIZip = "${WebUI.WEBUI.baseFileName}-$version.zip"
|
||||||
|
|
||||||
val webUIZip = "${WebUI.WEBUI.baseFileName}-$latestCompatibleVersion.zip"
|
|
||||||
val webUIZipPath = "$tmpDir/$webUIZip"
|
val webUIZipPath = "$tmpDir/$webUIZip"
|
||||||
val webUIZipFile = File(webUIZipPath)
|
val webUIZipURL = "${getDownloadUrlFor(version)}/$webUIZip"
|
||||||
|
|
||||||
logger.info { "downloadLatestCompatibleVersion: Downloading WebUI (flavor= ${serverConfig.webUIFlavor}, version \"$latestCompatibleVersion\") zip from the Internet..." }
|
val log = KotlinLogging.logger("${logger.name} downloadVersion(version= $version, flavor= ${serverConfig.webUIFlavor})")
|
||||||
|
log.info { "Downloading WebUI zip from the Internet..." }
|
||||||
try {
|
|
||||||
val webUIZipURL = "${getDownloadUrlFor(latestCompatibleVersion)}/$webUIZip"
|
|
||||||
downloadVersion(webUIZipURL, webUIZipFile)
|
|
||||||
|
|
||||||
if (!isDownloadValid(webUIZip, webUIZipPath)) {
|
|
||||||
throw Exception("Download is invalid")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
val retry = retryCount < 3
|
|
||||||
logger.error { "downloadLatestCompatibleVersion: Download failed${if (retry) ", retrying ${retryCount + 1}/3" else ""} - error: $e" }
|
|
||||||
|
|
||||||
if (retry) {
|
|
||||||
return downloadLatestCompatibleVersion(retryCount + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
executeWithRetry(log, { downloadVersionZipFile(webUIZipURL, webUIZipPath) })
|
||||||
File(applicationDirs.webUIRoot).deleteRecursively()
|
File(applicationDirs.webUIRoot).deleteRecursively()
|
||||||
|
|
||||||
// extract webUI zip
|
// extract webUI zip
|
||||||
logger.info { "downloadLatestCompatibleVersion: Extracting WebUI zip..." }
|
log.info { "Extracting WebUI zip..." }
|
||||||
extractDownload(webUIZipPath, applicationDirs.webUIRoot)
|
extractDownload(webUIZipPath, applicationDirs.webUIRoot)
|
||||||
logger.info { "downloadLatestCompatibleVersion: Extracting WebUI zip Done." }
|
log.info { "Extracting WebUI zip Done." }
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadVersion(url: String, zipFile: File) {
|
private fun downloadVersionZipFile(url: String, filePath: String) {
|
||||||
|
val zipFile = File(filePath)
|
||||||
zipFile.delete()
|
zipFile.delete()
|
||||||
|
|
||||||
val data = ByteArray(1024)
|
val data = ByteArray(1024)
|
||||||
|
|
||||||
zipFile.outputStream().use { webUIZipFileOut ->
|
zipFile.outputStream().use { webUIZipFileOut ->
|
||||||
@@ -361,7 +383,7 @@ object WebInterfaceManager {
|
|||||||
connection.inputStream.buffered().use { inp ->
|
connection.inputStream.buffered().use { inp ->
|
||||||
var totalCount = 0
|
var totalCount = 0
|
||||||
|
|
||||||
print("downloadVersion: Download progress: % 00")
|
print("downloadVersionZipFile: Download progress: % 00")
|
||||||
while (true) {
|
while (true) {
|
||||||
val count = inp.read(data, 0, 1024)
|
val count = inp.read(data, 0, 1024)
|
||||||
|
|
||||||
@@ -377,9 +399,13 @@ object WebInterfaceManager {
|
|||||||
webUIZipFileOut.write(data, 0, count)
|
webUIZipFileOut.write(data, 0, count)
|
||||||
}
|
}
|
||||||
println()
|
println()
|
||||||
logger.info { "downloadVersion: Downloading WebUI Done." }
|
logger.info { "downloadVersionZipFile: Downloading WebUI Done." }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isDownloadValid(zipFile.name, filePath)) {
|
||||||
|
throw Exception("Download is invalid")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isDownloadValid(zipFileName: String, zipFilePath: String): Boolean {
|
private fun isDownloadValid(zipFileName: String, zipFilePath: String): Boolean {
|
||||||
@@ -404,7 +430,7 @@ object WebInterfaceManager {
|
|||||||
val latestCompatibleVersion = getLatestCompatibleVersion()
|
val latestCompatibleVersion = getLatestCompatibleVersion()
|
||||||
latestCompatibleVersion != currentVersion
|
latestCompatibleVersion != currentVersion
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.debug { "isUpdateAvailable: check failed due to $e" }
|
logger.warn(e) { "isUpdateAvailable: check failed due to" }
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user