Streamline deprecated settings config value migration logic (#1633)

* Streamline deprecated settings config value migration logic

* Add "autoDownloadAheadLimit" config migration

For consistency

* Replace "exitCode" with "shutdownApp"

* Enhance shutdown logging to include exit reason
This commit is contained in:
schroda
2025-09-13 18:22:09 +02:00
committed by GitHub
parent 904d3980d6
commit 904157a91a
5 changed files with 81 additions and 56 deletions
@@ -8,6 +8,7 @@ package suwayomi.tachidesk.server
* 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 com.typesafe.config.Config import com.typesafe.config.Config
import io.github.config4k.toConfig
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -202,6 +203,7 @@ class ServerConfig(
SettingsRegistry.SettingDeprecated( SettingsRegistry.SettingDeprecated(
replaceWith = "autoDownloadNewChaptersLimit", replaceWith = "autoDownloadNewChaptersLimit",
message = "Replaced with autoDownloadNewChaptersLimit", message = "Replaced with autoDownloadNewChaptersLimit",
migrateConfigValue = { it.unwrapped() as? Int }
), ),
readMigrated = { autoDownloadNewChaptersLimit.value }, readMigrated = { autoDownloadNewChaptersLimit.value },
setMigrated = { autoDownloadNewChaptersLimit.value = it }, setMigrated = { autoDownloadNewChaptersLimit.value = it },
@@ -299,6 +301,13 @@ class ServerConfig(
SettingsRegistry.SettingDeprecated( SettingsRegistry.SettingDeprecated(
replaceWith = "authMode", replaceWith = "authMode",
message = "Removed - prefer authMode", message = "Removed - prefer authMode",
migrateConfigValue = {
if (it.unwrapped() as? Boolean == true) {
AuthMode.BASIC_AUTH.name
} else {
null
}
}
), ),
readMigrated = { authMode.value == AuthMode.BASIC_AUTH }, readMigrated = { authMode.value == AuthMode.BASIC_AUTH },
setMigrated = { authMode.value = if (it) AuthMode.BASIC_AUTH else AuthMode.NONE }, setMigrated = { authMode.value = if (it) AuthMode.BASIC_AUTH else AuthMode.NONE },
@@ -613,6 +622,28 @@ class ServerConfig(
SettingsRegistry.SettingDeprecated( SettingsRegistry.SettingDeprecated(
replaceWith = "koreaderSyncStrategyForward, koreaderSyncStrategyBackward", replaceWith = "koreaderSyncStrategyForward, koreaderSyncStrategyBackward",
message = "Replaced with koreaderSyncStrategyForward and koreaderSyncStrategyBackward", message = "Replaced with koreaderSyncStrategyForward and koreaderSyncStrategyBackward",
migrateConfig = { value, config ->
val oldStrategy = (value.unwrapped() as? String)?.uppercase()
val (forward, backward) =
when (oldStrategy) {
"PROMPT" -> "PROMPT" to "PROMPT"
"SILENT" -> "KEEP_REMOTE" to "KEEP_LOCAL"
"SEND" -> "KEEP_LOCAL" to "KEEP_LOCAL"
"RECEIVE" -> "KEEP_REMOTE" to "KEEP_REMOTE"
"DISABLED" -> "DISABLED" to "DISABLED"
else -> null to null
}
if (forward != null && backward != null) {
config
.withValue("server.koreaderSyncStrategyForward", forward.toConfig("internal").getValue("internal"))
.withValue("server.koreaderSyncStrategyBackward", backward.toConfig("internal").getValue("internal"))
.withoutPath("server.koreaderSyncStrategy")
} else {
config
}
}
), ),
readMigrated = { readMigrated = {
// This is a best-effort reverse mapping. It's not perfect but covers common cases. // This is a best-effort reverse mapping. It's not perfect but covers common cases.
@@ -742,6 +773,7 @@ class ServerConfig(
SettingsRegistry.SettingDeprecated( SettingsRegistry.SettingDeprecated(
replaceWith = "authUsername", replaceWith = "authUsername",
message = "Removed - prefer authUsername", message = "Removed - prefer authUsername",
migrateConfigValue = { it.unwrapped() as? String },
), ),
readMigrated = { authUsername.value }, readMigrated = { authUsername.value },
setMigrated = { authUsername.value = it }, setMigrated = { authUsername.value = it },
@@ -755,6 +787,7 @@ class ServerConfig(
SettingsRegistry.SettingDeprecated( SettingsRegistry.SettingDeprecated(
replaceWith = "authPassword", replaceWith = "authPassword",
message = "Removed - prefer authPassword", message = "Removed - prefer authPassword",
migrateConfigValue = { it.unwrapped() as? String },
), ),
readMigrated = { authPassword.value }, readMigrated = { authPassword.value },
setMigrated = { authPassword.value = it }, setMigrated = { authPassword.value = it },
@@ -1,14 +1,28 @@
package suwayomi.tachidesk.server.settings package suwayomi.tachidesk.server.settings
import com.typesafe.config.ConfigValue
import com.typesafe.config.parser.ConfigDocument
import kotlin.reflect.KClass import kotlin.reflect.KClass
/** /**
* Registry to track all settings for automatic updating and validation * Registry to track all settings for automatic updating and validation
*/ */
object SettingsRegistry { object SettingsRegistry {
/**
* Requires either [migrateConfigValue] or [migrateConfig] to be set.
* If neither is specified, the server will exit on startup due to being misconfigured.
*/
data class SettingDeprecated( data class SettingDeprecated(
val replaceWith: String? = null, val replaceWith: String? = null,
val message: String, val message: String,
/**
* For cases which do not require custom config miration logic.
*/
val migrateConfigValue: ((value: ConfigValue) -> Any?)? = null,
/**
* For cases which require complete control over the config migration.
*/
val migrateConfig: ((value: ConfigValue, config: ConfigDocument) -> ConfigDocument)? = null
) )
interface ITypeInfo { interface ITypeInfo {
@@ -37,7 +37,6 @@ import org.koin.core.context.startKoin
import org.koin.core.module.Module import org.koin.core.module.Module
import org.koin.dsl.module import org.koin.dsl.module
import suwayomi.tachidesk.global.impl.KcefWebView.Companion.toCefCookie import suwayomi.tachidesk.global.impl.KcefWebView.Companion.toCefCookie
import suwayomi.tachidesk.graphql.types.AuthMode
import suwayomi.tachidesk.graphql.types.DatabaseType import suwayomi.tachidesk.graphql.types.DatabaseType
import suwayomi.tachidesk.i18n.LocalizationHelper import suwayomi.tachidesk.i18n.LocalizationHelper
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
@@ -47,9 +46,12 @@ import suwayomi.tachidesk.manga.impl.update.Updater
import suwayomi.tachidesk.manga.impl.util.lang.renameTo import suwayomi.tachidesk.manga.impl.util.lang.renameTo
import suwayomi.tachidesk.server.database.databaseUp import suwayomi.tachidesk.server.database.databaseUp
import suwayomi.tachidesk.server.generated.BuildConfig import suwayomi.tachidesk.server.generated.BuildConfig
import suwayomi.tachidesk.server.settings.SettingsRegistry
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
import suwayomi.tachidesk.server.util.ConfigTypeRegistration import suwayomi.tachidesk.server.util.ConfigTypeRegistration
import suwayomi.tachidesk.server.util.ExitCode
import suwayomi.tachidesk.server.util.SystemTray import suwayomi.tachidesk.server.util.SystemTray
import suwayomi.tachidesk.server.util.shutdownApp
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import xyz.nulldev.androidcompat.AndroidCompat import xyz.nulldev.androidcompat.AndroidCompat
@@ -142,17 +144,18 @@ fun setupLogLevelUpdating(
) )
} }
fun <T : Any> migrateConfig( fun migrateConfig(
configDocument: ConfigDocument, configDocument: ConfigDocument,
config: Config, config: Config,
configKey: String, configKey: String,
toConfigKey: String, toConfigKey: String,
toType: (ConfigValue) -> T?, toType: (ConfigValue) -> Any?,
): ConfigDocument { ): ConfigDocument {
try { try {
val configValue = config.getValue(configKey) val configValue = config.getValue(configKey)
val typedValue = toType(configValue) val typedValue = toType(configValue)
if (typedValue != null) { if (typedValue != null) {
logger.debug { "Migrating config value: $configKey -> $toConfigKey" }
return configDocument.withValue( return configDocument.withValue(
toConfigKey, toConfigKey,
typedValue.toConfig("internal").getValue("internal"), typedValue.toConfig("internal").getValue("internal"),
@@ -309,59 +312,31 @@ fun applicationSetup() {
// make sure the user config file is up-to-date // make sure the user config file is up-to-date
GlobalConfigManager.updateUserConfig { config -> GlobalConfigManager.updateUserConfig { config ->
var updatedConfig = this var updatedConfig = this
updatedConfig =
migrateConfig(
updatedConfig,
config,
"server.basicAuthEnabled",
"server.authMode",
toType = {
if (it.unwrapped() as? Boolean == true) {
AuthMode.BASIC_AUTH.name
} else {
null
}
},
)
updatedConfig =
migrateConfig(
updatedConfig,
config,
"server.basicAuthUsername",
"server.authUsername",
toType = { it.unwrapped() as? String },
)
updatedConfig =
migrateConfig(
updatedConfig,
config,
"server.basicAuthPassword",
"server.authPassword",
toType = { it.unwrapped() as? String },
)
// Migrate KOReader sync strategy from single to forward/backward strategies val settingsRequiringMigration = SettingsRegistry.getAll().filterValues { it.deprecated?.replaceWith != null }
try { settingsRequiringMigration.forEach { (name, data) ->
val oldStrategy = config.getString("server.koreaderSyncStrategy") val configKey = "server.$name"
val (forward, backward) = val toConfigKey = "server.${data.deprecated!!.replaceWith}"
when (oldStrategy.uppercase()) {
"PROMPT" -> "PROMPT" to "PROMPT" if (data.deprecated!!.migrateConfig != null) {
"SILENT" -> "KEEP_REMOTE" to "KEEP_LOCAL" logger.debug { "Migrating config value: $configKey -> $toConfigKey" }
"SEND" -> "KEEP_LOCAL" to "KEEP_LOCAL" updatedConfig = data.deprecated!!.migrateConfig!!(config.getValue(configKey), updatedConfig)
"RECEIVE" -> "KEEP_REMOTE" to "KEEP_REMOTE" return@forEach
"DISABLED" -> "DISABLED" to "DISABLED"
else -> null to null
} }
if (forward != null && backward != null) { if (data.deprecated!!.migrateConfigValue != null) {
updatedConfig = updatedConfig =
updatedConfig migrateConfig(
.withValue("server.koreaderSyncStrategyForward", forward.toConfig("internal").getValue("internal")) updatedConfig,
.withValue("server.koreaderSyncStrategyBackward", backward.toConfig("internal").getValue("internal")) config,
.withoutPath("server.koreaderSyncStrategy") configKey,
toConfigKey,
data.deprecated!!.migrateConfigValue!!,
)
return@forEach
} }
} catch (_: ConfigException.Missing) {
// Key doesn't exist, no migration needed shutdownApp(ExitCode.ConfigMigrationMisconfiguredFailure)
} }
updatedConfig updatedConfig
@@ -21,10 +21,11 @@ import suwayomi.tachidesk.graphql.types.DatabaseType
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.ServerConfig import suwayomi.tachidesk.server.ServerConfig
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.server.util.ExitCode
import suwayomi.tachidesk.server.util.shutdownApp
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.sql.SQLException import java.sql.SQLException
import kotlin.system.exitProcess
object DBManager { object DBManager {
var db: Database? = null var db: Database? = null
@@ -90,7 +91,7 @@ fun databaseUp() {
} catch (e: SQLException) { } catch (e: SQLException) {
logger.error(e) { "Error up-to-database migration" } logger.error(e) { "Error up-to-database migration" }
if (System.getProperty("crashOnFailedMigration").toBoolean()) { if (System.getProperty("crashOnFailedMigration").toBoolean()) {
exitProcess(101) shutdownApp(ExitCode.DbMigrationFailure)
} }
} }
} }
@@ -19,10 +19,12 @@ enum class ExitCode(
MutexCheckFailedTachideskRunning(1), MutexCheckFailedTachideskRunning(1),
MutexCheckFailedAnotherAppRunning(2), MutexCheckFailedAnotherAppRunning(2),
WebUISetupFailure(3), WebUISetupFailure(3),
ConfigMigrationMisconfiguredFailure(4),
DbMigrationFailure(5),
} }
fun shutdownApp(exitCode: ExitCode) { fun shutdownApp(exitCode: ExitCode) {
logger.info { "Shutting Down Suwayomi-Server. Goodbye!" } logger.info { "Shutting Down Suwayomi-Server. Goodbye! (reason= ${exitCode.code} (${exitCode.name}))" }
exitProcess(exitCode.code) exitProcess(exitCode.code)
} }