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:
@@ -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 },
|
||||||
|
|||||||
+14
@@ -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"
|
|
||||||
"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) {
|
if (data.deprecated!!.migrateConfig != null) {
|
||||||
updatedConfig =
|
logger.debug { "Migrating config value: $configKey -> $toConfigKey" }
|
||||||
updatedConfig
|
updatedConfig = data.deprecated!!.migrateConfig!!(config.getValue(configKey), updatedConfig)
|
||||||
.withValue("server.koreaderSyncStrategyForward", forward.toConfig("internal").getValue("internal"))
|
return@forEach
|
||||||
.withValue("server.koreaderSyncStrategyBackward", backward.toConfig("internal").getValue("internal"))
|
|
||||||
.withoutPath("server.koreaderSyncStrategy")
|
|
||||||
}
|
}
|
||||||
} catch (_: ConfigException.Missing) {
|
|
||||||
// Key doesn't exist, no migration needed
|
if (data.deprecated!!.migrateConfigValue != null) {
|
||||||
|
updatedConfig =
|
||||||
|
migrateConfig(
|
||||||
|
updatedConfig,
|
||||||
|
config,
|
||||||
|
configKey,
|
||||||
|
toConfigKey,
|
||||||
|
data.deprecated!!.migrateConfigValue!!,
|
||||||
|
)
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user