From ce7ea31cb4b77d10756576d15395c50084004cee Mon Sep 17 00:00:00 2001 From: ChooseC <3464861631@qq.com> Date: Sat, 17 Jan 2026 05:52:02 +0800 Subject: [PATCH 1/4] feat: Optional use of MySQL database --- cli/src/main/kotlin/Main.kt | 45 ++++++++++++++----- common/build.gradle.kts | 2 + .../com/github/zly2006/xbackup/Config.kt | 18 ++++++++ .../com/github/zly2006/xbackup/XBackup.kt | 39 ++++++++++++---- 4 files changed, 83 insertions(+), 21 deletions(-) diff --git a/cli/src/main/kotlin/Main.kt b/cli/src/main/kotlin/Main.kt index efdf7e2..104e879 100644 --- a/cli/src/main/kotlin/Main.kt +++ b/cli/src/main/kotlin/Main.kt @@ -6,6 +6,8 @@ import kotlinx.serialization.json.Json import org.jetbrains.exposed.sql.Database import org.sqlite.SQLiteConfig import org.sqlite.SQLiteDataSource +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource import java.text.SimpleDateFormat import kotlin.io.path.* @@ -19,18 +21,6 @@ suspend fun main() { } val worldRoot = Path("").absolute().normalize() - val database = Database.connect( - SQLiteDataSource( - SQLiteConfig().apply { - enforceForeignKeys(true) - setCacheSize(100_000) - setJournalMode(SQLiteConfig.JournalMode.WAL) - } - ).apply { - url = "jdbc:sqlite:x_backup.db" - } - ) - var gameRoot = worldRoot while (!gameRoot.resolve("config").isDirectory()) { gameRoot = gameRoot.parent @@ -47,6 +37,37 @@ suspend fun main() { println("Warning: Config file not found, using default config") } + val database = when (config.databaseType.lowercase()) { + "mysql" -> { + val hikariConfig = HikariConfig().apply { + jdbcUrl = "jdbc:mysql://${config.mysqlHost}:${config.mysqlPort}/${config.mysqlDatabase}" + username = config.mysqlUsername + password = config.mysqlPassword + driverClassName = "com.mysql.cj.jdbc.Driver" + maximumPoolSize = 10 + minimumIdle = 2 + idleTimeout = 600000 + connectionTimeout = 30000 + maxLifetime = 1800000 + } + Database.connect(HikariDataSource(hikariConfig)) + } + "sqlite", "" -> { + Database.connect( + SQLiteDataSource( + SQLiteConfig().apply { + enforceForeignKeys(true) + setCacheSize(100_000) + setJournalMode(SQLiteConfig.JournalMode.WAL) + } + ).apply { + url = "jdbc:sqlite:x_backup.db" + } + ) + } + else -> throw IllegalArgumentException("Unsupported database type: ${config.databaseType}. Supported types: sqlite, mysql") + } + val blobDir = gameRoot.resolve(config.blobPath).normalize() if (!blobDir.isDirectory()) { println("Fatal: Blob directory not found.") diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 175da5a..b03f5bd 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -28,6 +28,8 @@ dependencies { sharedLib("org.jetbrains.exposed:exposed-jdbc:$exposed_version") sharedLib("org.jetbrains.exposed:exposed-json:$exposed_version") sharedLib("org.xerial:sqlite-jdbc:3.46.0.0") + sharedLib("com.mysql:mysql-connector-j:9.5.0") + sharedLib("com.zaxxer:HikariCP:7.0.2") sharedLib("org.apache.commons:commons-compress:1.26.0") val ktorVersion = property("deps.ktor_version") as String sharedLib("io.ktor:ktor-client-content-negotiation-jvm:$ktorVersion") diff --git a/common/src/main/kotlin/com/github/zly2006/xbackup/Config.kt b/common/src/main/kotlin/com/github/zly2006/xbackup/Config.kt index eb57cc3..d272d81 100644 --- a/common/src/main/kotlin/com/github/zly2006/xbackup/Config.kt +++ b/common/src/main/kotlin/com/github/zly2006/xbackup/Config.kt @@ -84,6 +84,24 @@ class Config { @SerialName("independent_blobs") val independentBlobs = false + @SerialName("database_type") + var databaseType = "sqlite" + + @SerialName("mysql_host") + var mysqlHost = "localhost" + + @SerialName("mysql_port") + var mysqlPort = 3306 + + @SerialName("mysql_database") + var mysqlDatabase = "x_backup" + + @SerialName("mysql_username") + var mysqlUsername = "root" + + @SerialName("mysql_password") + var mysqlPassword = "" + @SerialName("backup_interval") var backupInterval = 10800 diff --git a/src/main/kotlin/com/github/zly2006/xbackup/XBackup.kt b/src/main/kotlin/com/github/zly2006/xbackup/XBackup.kt index 4b88be9..70d4690 100644 --- a/src/main/kotlin/com/github/zly2006/xbackup/XBackup.kt +++ b/src/main/kotlin/com/github/zly2006/xbackup/XBackup.kt @@ -30,6 +30,8 @@ import org.slf4j.LoggerFactory import org.sqlite.SQLiteConfig import org.sqlite.SQLiteConnection import org.sqlite.SQLiteDataSource +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource import java.io.File import java.net.http.HttpClient.Redirect.NORMAL import java.nio.file.Files @@ -259,17 +261,36 @@ object XBackup : ModInitializer { } fun getDatabaseFromWorld(worldPath: Path?): Database { - val database = Database.connect( - SQLiteDataSource( - SQLiteConfig().apply { - enforceForeignKeys(true) - setCacheSize(100_000) - setJournalMode(SQLiteConfig.JournalMode.WAL) + val database = when (config.databaseType.lowercase()) { + "mysql" -> { + val hikariConfig = HikariConfig().apply { + jdbcUrl = "jdbc:mysql://${config.mysqlHost}:${config.mysqlPort}/${config.mysqlDatabase}?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC" + username = config.mysqlUsername + password = config.mysqlPassword + driverClassName = "com.mysql.cj.jdbc.Driver" + maximumPoolSize = 10 + minimumIdle = 2 + idleTimeout = 600000 + connectionTimeout = 30000 + maxLifetime = 1800000 } - ).apply { - url = "jdbc:sqlite:$worldPath/x_backup.db" + Database.connect(HikariDataSource(hikariConfig)) } - ) + "sqlite", "" -> { + Database.connect( + SQLiteDataSource( + SQLiteConfig().apply { + enforceForeignKeys(true) + setCacheSize(100_000) + setJournalMode(SQLiteConfig.JournalMode.WAL) + } + ).apply { + url = "jdbc:sqlite:$worldPath/x_backup.db" + } + ) + } + else -> throw IllegalArgumentException("Unsupported database type: ${config.databaseType}. Supported types: sqlite, mysql") + } TransactionManager.defaultDatabase = database return database } From b63e2f6ad048cce7b76d6a1307ff69037bde80e3 Mon Sep 17 00:00:00 2001 From: ChooseC <3464861631@qq.com> Date: Sat, 17 Jan 2026 07:10:19 +0800 Subject: [PATCH 2/4] fix: resolve SQLite MODIFY COLUMN error when running with BanHammer --- .../zly2006/xbackup/BackupDatabaseService.kt | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/common/src/main/kotlin/com/github/zly2006/xbackup/BackupDatabaseService.kt b/common/src/main/kotlin/com/github/zly2006/xbackup/BackupDatabaseService.kt index b70bb17..be79916 100644 --- a/common/src/main/kotlin/com/github/zly2006/xbackup/BackupDatabaseService.kt +++ b/common/src/main/kotlin/com/github/zly2006/xbackup/BackupDatabaseService.kt @@ -55,12 +55,20 @@ class BackupDatabaseService( } transaction { - SchemaUtils.createMissingTablesAndColumns( - BackupEntryTable, - BackupTable, - BackupEntryBackupTable, - withLogs = false - ) + try { + SchemaUtils.createMissingTablesAndColumns( + BackupEntryTable, + BackupTable, + BackupEntryBackupTable, + withLogs = false + ) + } catch (e: org.jetbrains.exposed.exceptions.ExposedSQLException) { + e.cause?.message?.contains("MODIFY", ignoreCase = true)?.let { + if (!it) { + throw e + } + } + } } } From 3e16aeafe4775c0bc42e41ed649631c77e68de7a Mon Sep 17 00:00:00 2001 From: ChooseC <3464861631@qq.com> Date: Sat, 17 Jan 2026 07:53:37 +0800 Subject: [PATCH 3/4] fix: Skip SQLite database backup when the database type is MySQL. --- .../com/github/zly2006/xbackup/XBackup.kt | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/com/github/zly2006/xbackup/XBackup.kt b/src/main/kotlin/com/github/zly2006/xbackup/XBackup.kt index 70d4690..831218e 100644 --- a/src/main/kotlin/com/github/zly2006/xbackup/XBackup.kt +++ b/src/main/kotlin/com/github/zly2006/xbackup/XBackup.kt @@ -264,7 +264,7 @@ object XBackup : ModInitializer { val database = when (config.databaseType.lowercase()) { "mysql" -> { val hikariConfig = HikariConfig().apply { - jdbcUrl = "jdbc:mysql://${config.mysqlHost}:${config.mysqlPort}/${config.mysqlDatabase}?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC" + jdbcUrl = "jdbc:mysql://${config.mysqlHost}:${config.mysqlPort}/${config.mysqlDatabase}" username = config.mysqlUsername password = config.mysqlPassword driverClassName = "com.mysql.cj.jdbc.Driver" @@ -325,22 +325,24 @@ object XBackup : ModInitializer { put("mod_ver", MOD_VERSION) } ) - val localBackup = File("x_backup.db.back") - localBackup.delete() - try { - (service.database.connector().connection as? SQLiteConnection)?.createStatement() - ?.execute("VACUUM INTO '$localBackup';") - } catch (e: Exception) { - log.error("Error backing up database", e) + if (config.databaseType.lowercase() != "mysql") { + val localBackup = File("x_backup.db.back") + localBackup.delete() + try { + (service.database.connector().connection as? SQLiteConnection)?.createStatement() + ?.execute("VACUUM INTO '$localBackup';") + } catch (e: Exception) { + log.error("Error backing up database", e) + } + Files.move( + localBackup.toPath(), + Path("xb.backups") + .resolve(backId.toString()) + .resolve("x_backup.db") + .createParentDirectories(), + StandardCopyOption.REPLACE_EXISTING + ) } - Files.move( - localBackup.toPath(), - Path("xb.backups") - .resolve(backId.toString()) - .resolve("x_backup.db") - .createParentDirectories(), - StandardCopyOption.REPLACE_EXISTING - ) // delete old backups in ./xb.backups, keep the latest 5 val backups = Path("xb.backups").listDirectoryEntries().filter { it.isDirectory() } backups.sortedByDescending { it.getLastModifiedTime().toMillis() } From 1c365bbb1422443636bc02e6dc685453dd4fe7e1 Mon Sep 17 00:00:00 2001 From: ChooseC <3464861631@qq.com> Date: Sat, 17 Jan 2026 08:29:03 +0800 Subject: [PATCH 4/4] fix: check database type before clearing xb.backups --- .../kotlin/com/github/zly2006/xbackup/XBackup.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/github/zly2006/xbackup/XBackup.kt b/src/main/kotlin/com/github/zly2006/xbackup/XBackup.kt index 831218e..d555370 100644 --- a/src/main/kotlin/com/github/zly2006/xbackup/XBackup.kt +++ b/src/main/kotlin/com/github/zly2006/xbackup/XBackup.kt @@ -344,10 +344,15 @@ object XBackup : ModInitializer { ) } // delete old backups in ./xb.backups, keep the latest 5 - val backups = Path("xb.backups").listDirectoryEntries().filter { it.isDirectory() } - backups.sortedByDescending { it.getLastModifiedTime().toMillis() } - .drop(5) - .forEach { it.toFile().deleteRecursively() } + if (config.databaseType.lowercase() != "mysql") { + val backupsDir = Path("xb.backups") + if (backupsDir.exists()) { + val backups = backupsDir.listDirectoryEntries().filter { it.isDirectory() } + backups.sortedByDescending { it.getLastModifiedTime().toMillis() } + .drop(5) + .forEach { it.toFile().deleteRecursively() } + } + } server.broadcast( Utils.translate( "message.xb.scheduled_backup_finished",