diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9e369e9aa..9ba2c0fc1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,11 +70,11 @@ exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "e exposed-javatime = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" } exposed-kotlintime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" } postgres = "org.postgresql:postgresql:42.7.11" -h2 = "com.h2database:h2:1.4.200" # current database driver, can't update to h2 v2 without sql migration +h2 = "com.h2database:h2:2.4.240" hikaricp = "com.zaxxer:HikariCP:7.0.2" # Exposed Migrations -exposed-migrations = "com.github.Suwayomi:exposed-migrations:3.10.0" +exposed-migrations = "com.github.Suwayomi:exposed-migrations:3.10.1" # Dependency Injection koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/Migration.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/Migration.kt index 219a16885..ba67b3828 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/Migration.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/Migration.kt @@ -4,6 +4,7 @@ import android.app.Application import android.content.Context import io.github.oshai.kotlinlogging.KotlinLogging import suwayomi.tachidesk.manga.impl.update.IUpdater +import suwayomi.tachidesk.server.database.H2Migration import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File @@ -74,6 +75,14 @@ private fun migrateMangaDownloadDir(applicationDirs: ApplicationDirs) { } } +fun migrateDatabaseToV24240(applicationDirs: ApplicationDirs) { + H2Migration.migrate( + applicationDirs.dataRoot, + "1.4.200", + "2.4.240", + ) +} + private val MIGRATIONS = listOf Unit>>( "InitialMigration" to { applicationDirs -> @@ -83,6 +92,9 @@ private val MIGRATIONS = "FixGlobalUpdateScheduling" to { Injekt.get().deleteLastAutomatedUpdateTimestamp() }, + "MigrateDatabaseToV2.4.240" to { applicationDirs -> + migrateDatabaseToV24240(applicationDirs) + }, ) fun runMigrations(applicationDirs: ApplicationDirs) { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt index 7e1f59122..c293c558b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt @@ -391,6 +391,8 @@ fun applicationSetup() { "Localization service initialized. Supported languages: ${LocalizationHelper.getSupportedLocales()}" } + runMigrations(applicationDirs) + databaseUp() LocalSource.register() @@ -440,8 +442,6 @@ fun applicationSetup() { ignoreInitialValue = false, ) - runMigrations(applicationDirs) - setLogLevelFor("org.eclipse.jetty", Level.OFF) setLogLevelFor("com.zaxxer.hikari", Level.WARN) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/database/H2Migration.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/database/H2Migration.kt new file mode 100644 index 000000000..6a4008dcb --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/database/H2Migration.kt @@ -0,0 +1,163 @@ +package suwayomi.tachidesk.server.database + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.NetworkHelper +import io.github.oshai.kotlinlogging.KotlinLogging +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.net.URLClassLoader +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.copyTo +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteExisting +import kotlin.io.path.div +import kotlin.io.path.exists +import kotlin.io.path.name +import kotlin.io.path.notExists +import kotlin.io.path.outputStream + +object H2Migration { + private val logger = KotlinLogging.logger {} + + private val client by lazy { + Injekt.get().client + } + + private const val TOOL_VERSION = "1.8" + + private const val TOOL_URL = + "https://manticore-projects.com/download/H2MigrationTool-$TOOL_VERSION/H2MigrationTool-$TOOL_VERSION-all.jar" + + private const val MAVEN_BASE = + "https://repo1.maven.org/maven2/com/h2database/h2" + + fun migrate( + rootDir: String, + h2Old: String, + h2New: String, + ) { + val dbBase = "$rootDir/database" + val mvStore = Path("$dbBase.mv.db") + if (mvStore.notExists()) { + logger.info { "No H2 database found. Skipping migration." } + return + } + + val script = Path("$dbBase.${h2Old.substringAfterLast('.')}.sql") + + // Backup original database. + val backup = Path("$dbBase.mv.db.${h2Old.substringAfterLast('.')}.backup") + mvStore.copyTo(backup, overwrite = true) + logger.info { "Created backup: ${backup.absolutePathString()}" } + + val toolsDir = Path(rootDir) / "bin" / "h2-migration-tools" + val libsDir = toolsDir / "h2libs" + libsDir.createDirectories() + + // Download migration tool + val migrationJar = + toolsDir.resolve("H2MigrationTool-$TOOL_VERSION-all.jar") + downloadIfNeeded( + TOOL_URL, + migrationJar, + ) + downloadIfNeeded( + "$MAVEN_BASE/$h2Old/h2-$h2Old.jar", + libsDir.resolve("h2-$h2Old.bin"), + ) + downloadIfNeeded( + "$MAVEN_BASE/$h2New/h2-$h2New.jar", + libsDir.resolve("h2-$h2New.bin"), + ) + + runMigrationTool( + migrationJar = migrationJar, + libsDir = libsDir, + mvStore = mvStore, + script = script, + ) + + // Move database to proper path + val newDatabase = Path(rootDir, "database.${h2New.substringAfterLast('.')}.mv.db") + newDatabase.copyTo(mvStore, overwrite = true) + newDatabase.deleteExisting() + + logger.info { "H2 migration completed successfully." } + } + + private fun downloadIfNeeded( + url: String, + output: Path, + ) { + if (output.exists()) { + logger.debug { "Already downloaded: ${output.name}" } + return + } + + client + .newCall(GET(url)) + .execute() + .use { response -> + + if (!response.isSuccessful) { + throw RuntimeException( + "Failed to download $url " + + "(HTTP ${response.code})", + ) + } + + output.outputStream().use { out -> + response.body.byteStream().copyTo(out) + } + } + + logger.info { "Saved: ${output.absolutePathString()}" } + } + + private fun runMigrationTool( + migrationJar: Path, + libsDir: Path, + mvStore: Path, + script: Path, + ) { + URLClassLoader( + arrayOf(migrationJar.toUri().toURL()), + javaClass.classLoader, + ).use { classLoader -> + val clazz = + classLoader.loadClass("com.manticore.h2.H2MigrationTool") + + val main = + clazz.getMethod("main", Array::class.java) + + main.invoke( + null, + arrayOf( + // h2 driver dir + "-l", + libsDir.absolutePathString(), + // from version + "-f", + "1.4.200", + // to version + "-t", + "2.4.240", + // user + "-u", + "", + // password + "-p", + "", + // database.mv.db + "-d", + mvStore.absolutePathString(), + // database backup in SQL + "-s", + script.absolutePathString(), + ), + ) + } + } +}