Compare commits

..

5 Commits

Author SHA1 Message Date
Syer10
3bc75d5b00 Review comments 2026-05-12 09:32:09 -04:00
Syer10
fe6dd05411 Update H2 2026-05-11 19:20:06 -04:00
Syer10
57c0a85a35 Add Kotlinx.DateTime extensions 2026-05-11 00:31:23 -04:00
Syer10
c2c927ae97 Update Exposed 2026-05-11 00:25:44 -04:00
renovate[bot]
2c70700bb7 Update exposed to v1 2026-05-11 04:16:03 +00:00
20 changed files with 87 additions and 205 deletions

View File

@@ -67,7 +67,7 @@ jobs:
export LD_PRELOAD="$(pwd)/scripts/resources/catch_abort.so"
JAR=$(ls ./server/build/*.jar| head -1)
set +e
timeout 30s java \
timeout 30s java -DcrashOnFailedMigration=true \
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
-Dsuwayomi.tachidesk.config.server.databaseType=POSTGRESQL \
@@ -83,7 +83,7 @@ jobs:
exit "$ecode"
fi
timeout 30s java \
timeout 30s java -DcrashOnFailedMigration=true \
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
-jar "$JAR"

View File

@@ -10,15 +10,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- .
### Changed
- (Database/H2) Use the latest H2 database engine
- (Startup) Crash on startup if an unrecoverable error happens
- .
### Fixed
- (CloudFlareInterceptor) Don't send the `cf_clearance` cookie back to Flaresolverr
- (WebUI) Handle serving non-default webui with "bundled"
- (WebUI) Wait until WebUI is ready to open in browser
- (Downloads) Truncate filenames by byte length to prevent "File name too long" IO errors
- (Extension) Do not indicate an update is available when the extension is not installed
## [v2.2.2100] + [WebUI: v20260508.01] - 2026-05-08

View File

@@ -4,7 +4,7 @@ coroutines = "1.11.0"
serialization = "1.11.0"
jvmTarget = "21"
okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
javalin = "7.2.2"
javalin = "7.2.0"
jte = "3.2.4"
jackson = "3.1.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
exposed = "1.2.0"
@@ -37,7 +37,7 @@ serialization-xml-core = { module = "io.github.pdvrieze.xmlutil:core", version.r
serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", version.ref = "xmlserialization" }
# Logging
slf4japi = "org.slf4j:slf4j-api:2.0.18"
slf4japi = "org.slf4j:slf4j-api:2.0.17"
logback = "ch.qos.logback:logback-classic:1.5.32"
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.02"
@@ -116,7 +116,7 @@ appdirs = "ca.gosyer:kotlin-multiplatform-appdirs:2.0.0"
cache4k = "io.github.reactivecircus.cache4k:cache4k:0.14.0"
zip4j = "net.lingala.zip4j:zip4j:2.11.6"
commonscompress = "org.apache.commons:commons-compress:1.28.0"
junrar = "com.github.junrar:junrar:7.6.0"
junrar = "com.github.junrar:junrar:7.5.10"
# AES/CBC/PKCS7Padding Cypher provider
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500

View File

@@ -13,6 +13,6 @@ import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
* Metadata storage for clients, server/global level.
*/
object GlobalMetaTable : IntIdTable() {
val key = varchar("meta_key", 256)
val key = varchar("key", 256)
val value = varchar("value", 4096)
}

View File

@@ -358,7 +358,6 @@ object Extension {
} else {
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
it[isInstalled] = false
it[hasUpdate] = false
}
}

View File

@@ -15,7 +15,7 @@ import suwayomi.tachidesk.manga.model.table.CategoryMetaTable.ref
* Metadata storage for clients, about Category with id == [ref].
*/
object CategoryMetaTable : IntIdTable() {
val key = varchar("meta_key", 256)
val key = varchar("key", 256)
val value = varchar("value", 4096)
val ref = reference("category_ref", CategoryTable, ReferenceOption.CASCADE)
}

View File

@@ -26,7 +26,7 @@ import suwayomi.tachidesk.manga.model.table.ChapterMetaTable.ref
* }
*/
object ChapterMetaTable : IntIdTable() {
val key = varchar("meta_key", 256)
val key = varchar("key", 256)
val value = varchar("value", 4096)
val ref = reference("chapter_ref", ChapterTable, ReferenceOption.CASCADE)
}

View File

@@ -26,7 +26,7 @@ import suwayomi.tachidesk.manga.model.table.MangaMetaTable.ref
* }
*/
object MangaMetaTable : IntIdTable() {
val key = varchar("meta_key", 256)
val key = varchar("key", 256)
val value = varchar("value", 4096)
val ref = reference("manga_ref", MangaTable, ReferenceOption.CASCADE)
}

View File

@@ -14,7 +14,7 @@ import suwayomi.tachidesk.manga.model.table.SourceMetaTable.ref
* Metadata storage for clients, about Source with id == [ref].
*/
object SourceMetaTable : IntIdTable() {
val key = varchar("meta_key", 256)
val key = varchar("key", 256)
val value = varchar("value", 4096)
val ref = long("source_ref")
}

View File

@@ -5,8 +5,6 @@ import android.content.Context
import io.github.oshai.kotlinlogging.KotlinLogging
import suwayomi.tachidesk.manga.impl.update.IUpdater
import suwayomi.tachidesk.server.database.H2Migration
import suwayomi.tachidesk.server.util.ExitCode
import suwayomi.tachidesk.server.util.shutdownApp
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
@@ -101,35 +99,31 @@ private val MIGRATIONS =
fun runMigrations(applicationDirs: ApplicationDirs) {
val logger = KotlinLogging.logger("Migration")
try {
val migrationPreferences =
Injekt
.get<Application>()
.getSharedPreferences(
"migrations",
Context.MODE_PRIVATE,
)
val version = migrationPreferences.getInt("version", 0)
logger.info { "Running migrations, previous version $version, target version ${MIGRATIONS.size}" }
val migrationPreferences =
Injekt
.get<Application>()
.getSharedPreferences(
"migrations",
Context.MODE_PRIVATE,
)
val version = migrationPreferences.getInt("version", 0)
MIGRATIONS.forEachIndexed { index, (migrationName, migrationFunction) ->
val migrationVersion = index + 1
logger.info { "Running migrations, previous version $version, target version ${MIGRATIONS.size}" }
val isMigrationRequired = version < migrationVersion
if (!isMigrationRequired) {
logger.info { "Skipping migration version $migrationVersion: $migrationName" }
return@forEachIndexed
}
MIGRATIONS.forEachIndexed { index, (migrationName, migrationFunction) ->
val migrationVersion = index + 1
logger.info { "Running migration version $migrationVersion: $migrationName" }
migrationFunction(applicationDirs)
migrationPreferences.edit().putInt("version", migrationVersion).apply()
val isMigrationRequired = version < migrationVersion
if (!isMigrationRequired) {
logger.info { "Skipping migration version $migrationVersion: $migrationName" }
return@forEachIndexed
}
} catch (e: Exception) {
logger.error(e) { "Failed to run migrations" }
shutdownApp(ExitCode.MigrationsRunFailure)
logger.info { "Running migration version $migrationVersion: $migrationName" }
migrationFunction(applicationDirs)
migrationPreferences.edit().putInt("version", migrationVersion).apply()
}
}

View File

@@ -366,7 +366,6 @@ fun applicationSetup() {
}
} catch (e: Exception) {
logger.error(e) { "Exception while creating initial server.conf" }
shutdownApp(ExitCode.SetupConfFileFailed)
}
// copy local source icon
@@ -379,7 +378,6 @@ fun applicationSetup() {
}
} catch (e: Exception) {
logger.error(e) { "Exception while copying Local source's icon" }
shutdownApp(ExitCode.LocalSourceIconCopyFailure)
}
// fixes #119 , ref:
@@ -397,12 +395,7 @@ fun applicationSetup() {
databaseUp()
try {
LocalSource.register()
} catch (e: Exception) {
logger.error(e) { "Failed to setup LocalSource" }
shutdownApp(ExitCode.LocalSourceSetupFailure)
}
LocalSource.register()
serverConfig.subscribeTo(
combine<Any, DatabaseSettings>(
@@ -532,7 +525,7 @@ fun applicationSetup() {
}
}
}
download { github { release("jbr-release-21.0.10b1163.108") } }
download { github() }
settings {
windowlessRenderingEnabled = true
cachePath = (Path(applicationDirs.dataRoot) / "cache/kcef").toString()

View File

@@ -27,6 +27,7 @@ import suwayomi.tachidesk.server.util.ExitCode
import suwayomi.tachidesk.server.util.shutdownApp
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.sql.SQLException
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
@@ -183,8 +184,10 @@ fun databaseUp() {
}
val migrations = loadMigrationsFrom("suwayomi.tachidesk.server.database.migration", ServerConfig::class.java)
runMigrations(migrations)
} catch (e: Exception) {
} catch (e: SQLException) {
logger.error(e) { "Error up-to-database migration" }
shutdownApp(ExitCode.DbMigrationFailure)
if (System.getProperty("crashOnFailedMigration").toBoolean()) {
shutdownApp(ExitCode.DbMigrationFailure)
}
}
}

View File

@@ -9,12 +9,9 @@ import java.net.URLClassLoader
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.bufferedReader
import kotlin.io.path.bufferedWriter
import kotlin.io.path.copyTo
import kotlin.io.path.createDirectories
import kotlin.io.path.deleteExisting
import kotlin.io.path.deleteIfExists
import kotlin.io.path.div
import kotlin.io.path.exists
import kotlin.io.path.name
@@ -49,10 +46,6 @@ object H2Migration {
}
val script = Path("$dbBase.${h2Old.substringAfterLast('.')}.sql")
script.deleteIfExists()
val modifiedScript = Path("$dbBase.${h2Old.substringAfterLast('.')}.modified.sql")
modifiedScript.deleteIfExists()
// Backup original database.
val backup = Path("$dbBase.mv.db.${h2Old.substringAfterLast('.')}.backup")
@@ -79,32 +72,19 @@ object H2Migration {
libsDir.resolve("h2-$h2New.bin"),
)
// Delete attempted migration if failed previously
val newDatabase = Path(rootDir, "database.${h2New.substringAfterLast('.')}.mv.db")
newDatabase.deleteIfExists()
val modifiedNewDatabase = Path(rootDir, "database.${h2Old.substringAfterLast('.')}.modified.${h2New.substringAfterLast('.')}.mv.db")
modifiedNewDatabase.deleteIfExists()
runMigrationTool(
migrationJar = migrationJar,
libsDir = libsDir,
mvStore = mvStore,
script = script,
modifiedScript = modifiedScript,
h2Old = h2Old,
h2New = h2New,
)
// Move database to proper path
if (modifiedNewDatabase.exists()) {
modifiedNewDatabase.copyTo(mvStore, overwrite = true)
modifiedNewDatabase.deleteExisting()
newDatabase.deleteIfExists()
} else {
newDatabase.copyTo(mvStore, overwrite = true)
newDatabase.deleteExisting()
}
val newDatabase = Path(rootDir, "database.${h2New.substringAfterLast('.')}.mv.db")
newDatabase.copyTo(mvStore, overwrite = true)
newDatabase.deleteExisting()
logger.info { "H2 migration completed successfully." }
}
@@ -143,7 +123,6 @@ object H2Migration {
libsDir: Path,
mvStore: Path,
script: Path,
modifiedScript: Path,
h2Old: String,
h2New: String,
) {
@@ -157,77 +136,32 @@ object H2Migration {
val main =
clazz.getMethod("main", Array<String>::class.java)
try {
main.invoke(
null,
arrayOf(
// h2 driver dir
"-l",
libsDir.absolutePathString(),
// from version
"-f",
h2Old,
// to version
"-t",
h2New,
// user
"-u",
"",
// password
"-p",
"",
// database.mv.db
"-d",
mvStore.absolutePathString(),
// database backup in SQL
"-s",
script.absolutePathString(),
),
)
} catch (e: Exception) {
// Modify raw .sql file as needed for compatibility
if (e.stackTraceToString().contains("Unknown data type: \"DATETIME\"; SQL statement:") && script.exists()) {
script.bufferedReader().use { reader ->
modifiedScript.bufferedWriter().use { writer ->
reader.forEachLine { line ->
writer.write(
line.replace(
" \"EXECUTED_AT\" DATETIME(9) NOT NULL",
" \"EXECUTED_AT\" TIMESTAMP(9) NOT NULL",
),
)
writer.newLine()
}
}
}
main.invoke(
null,
arrayOf(
// h2 driver dir
"-l",
libsDir.absolutePathString(),
// from version
"-f",
h2Old,
// to version
"-t",
h2New,
// user
"-u",
"",
// password
"-p",
"",
// database.mv.db
"-d",
modifiedScript.absolutePathString(),
),
)
} else {
throw e
}
}
main.invoke(
null,
arrayOf(
// h2 driver dir
"-l",
libsDir.absolutePathString(),
// from version
"-f",
h2Old,
// to version
"-t",
h2New,
// user
"-u",
"",
// password
"-p",
"",
// database.mv.db
"-d",
mvStore.absolutePathString(),
// database backup in SQL
"-s",
script.absolutePathString(),
),
)
}
}
}

View File

@@ -10,10 +10,17 @@ package suwayomi.tachidesk.server.database.migration
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import de.neonew.exposed.migrations.helpers.SQLMigration
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
@Suppress("ClassName", "unused")
class M0023_CategoryMetaRefFix : SQLMigration() {
fun String.toSqlName(): String =
TransactionManager.defaultDatabase!!.identifierManager.let {
it.quoteIfNecessary(
it.inProperCase(this),
)
}
private val CategoryMetaTable by lazy { "CategoryMeta".toSqlName() }
private val CategoryRefColumn by lazy { "category_ref".toSqlName() }
private val CategoryTable by lazy { "Category".toSqlName() }

View File

@@ -8,7 +8,6 @@ package suwayomi.tachidesk.server.database.migration
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import de.neonew.exposed.migrations.helpers.SQLMigration
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
@Suppress("ClassName", "unused")
class M0049_FixDuplicatedMetas : SQLMigration() {
@@ -16,7 +15,7 @@ class M0049_FixDuplicatedMetas : SQLMigration() {
table: String,
refColumn: String? = null,
): String {
val groupBy = listOfNotNull(refColumn, "KEY".toSqlName()).joinToString(", ")
val groupBy = listOfNotNull(refColumn, "KEY").joinToString(", ")
return """
DELETE FROM $table
@@ -31,11 +30,10 @@ class M0049_FixDuplicatedMetas : SQLMigration() {
""".trimIndent()
}
override val sql: String by lazy {
override val sql: String =
createMigrationForTable("CATEGORYMETA", "CATEGORY_REF") +
createMigrationForTable("CHAPTERMETA", "CHAPTER_REF") +
createMigrationForTable("GLOBALMETA") +
createMigrationForTable("MANGAMETA", "MANGA_REF") +
createMigrationForTable("SOURCEMETA", "SOURCE_REF")
}
}

View File

@@ -1,38 +0,0 @@
package suwayomi.tachidesk.server.database.migration
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* 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 de.neonew.exposed.migrations.helpers.SQLMigration
import suwayomi.tachidesk.graphql.types.DatabaseType
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
import suwayomi.tachidesk.server.serverConfig
@Suppress("ClassName", "unused")
class M0055_RenameMetaKeys : SQLMigration() {
fun postgresRename(table: String): String =
"ALTER TABLE $table " +
"RENAME COLUMN " + "KEY".toSqlName() + " TO META_KEY;"
fun h2Rename(table: String): String =
"ALTER TABLE $table " +
"ALTER COLUMN " + "KEY".toSqlName() + " RENAME TO META_KEY;"
fun createRenameMigration(table: String): String =
when (serverConfig.databaseType.value) {
DatabaseType.H2 -> h2Rename(table.toSqlName())
DatabaseType.POSTGRESQL -> postgresRename(table.toSqlName())
}
override val sql: String by lazy {
createRenameMigration("CATEGORYMETA") +
createRenameMigration("CHAPTERMETA") +
createRenameMigration("GLOBALMETA") +
createRenameMigration("MANGAMETA") +
createRenameMigration("SOURCEMETA")
}
}

View File

@@ -1,9 +1,17 @@
package suwayomi.tachidesk.server.database.migration.helpers
import de.neonew.exposed.migrations.helpers.SQLMigration
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
import suwayomi.tachidesk.graphql.types.DatabaseType
import suwayomi.tachidesk.server.serverConfig
fun String.toSqlName(): String =
TransactionManager.current().db.identifierManager.let {
it.quoteIfNecessary(
it.inProperCase(this),
)
}
abstract class RenameFieldMigration(
tableName: String,
originalName: String,

View File

@@ -1,10 +0,0 @@
package suwayomi.tachidesk.server.database.migration.helpers
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
fun String.toSqlName(): String =
TransactionManager.current().db.identifierManager.let {
it.quoteIfNecessary(
it.inProperCase(this),
)
}

View File

@@ -21,10 +21,6 @@ enum class ExitCode(
WebUISetupFailure(3),
ConfigMigrationMisconfiguredFailure(4),
DbMigrationFailure(5),
SetupConfFileFailed(6),
LocalSourceIconCopyFailure(7),
LocalSourceSetupFailure(8),
MigrationsRunFailure(9),
}
fun shutdownApp(exitCode: ExitCode) {