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/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 + } + } + } } } 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..d555370 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}" + 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 } @@ -304,27 +325,34 @@ 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() } - .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",