Design for adding MariaDB as alternative database backend with data migration, x-ui.sh switching UI, and driver-agnostic InitDB.
11 KiB
MariaDB Support for 3x-ui
Summary
Add MariaDB as an alternative database backend to SQLite. Users switch between SQLite and MariaDB via the x-ui.sh management script (option 27). Data is automatically migrated during the switch. MariaDB connection credentials are stored in /etc/x-ui/x-ui.json.
Requirements
- Support both SQLite and MariaDB as database backends
- Switch via
x-ui.shwith interactive prompts for MariaDB credentials (IP, port, username, password, database name) - Auto-migrate data when switching between SQLite and MariaDB
- Keep old database as backup after migration
- MariaDB has core feature parity (CRUD, migrations, seeders) but skips SQLite-specific features (WAL checkpoint, file export/import)
- Credentials stored in
/etc/x-ui/x-ui.json
Architecture: Approach A — Driver-agnostic InitDB
Refactor database.InitDB() to read config from the JSON settings file, determine the driver type, and open the appropriate GORM connection. The package-level var db *gorm.DB singleton stays unchanged — all callers continue using database.GetDB().
Section 1: Configuration
New settings in web/service/setting.go
Add to defaultValueMap:
| Key | Default | Description |
|---|---|---|
dbType |
"sqlite" |
"sqlite" or "mariadb" |
dbHost |
"127.0.0.1" |
MariaDB host |
dbPort |
"3306" |
MariaDB port |
dbUser |
"" |
MariaDB username |
dbPassword |
"" |
MariaDB password |
dbName |
"3xui" |
MariaDB database name |
Add getter/setter methods: GetDBType(), SetDBType(), GetDBHost(), SetDBHost(), GetDBPort(), SetDBPort(), GetDBUser(), SetDBUser(), GetDBPassword(), SetDBPassword(), GetDBName(), SetDBName().
Config reading before DB init
Problem: settings are stored IN the database, but we need dbType BEFORE opening the DB.
Solution: config/config.go gets a GetDBTypeFromJSON() function that reads /etc/x-ui/x-ui.json directly (falls back to "sqlite" if file doesn't exist or key is missing). This is called before database.InitDB().
New CLI flags in main.go
Add -dbType, -dbHost, -dbPort, -dbUser, -dbPassword, -dbName flags to the setting subcommand. These write directly to the JSON config file (not via the DB) using config.WriteSettingToJSON(key, value).
New config/config.go helper: WriteSettingToJSON(key, value string) — reads the JSON file, updates the key, writes back.
Section 2: Database Layer (database/db.go)
Refactored InitDB()
func InitDB() error {
dbType := config.GetDBTypeFromJSON()
switch dbType {
case "mariadb":
return initMariaDB()
default: // "sqlite"
return initSQLite(config.GetDBPath())
}
}
initSQLite(path string) error
Existing logic unchanged — opens SQLite with gorm.io/driver/sqlite, runs initModels(), initUser(), runSeeders().
initMariaDB() error
- Read host, port, user, password, dbName from JSON config.
- Build DSN:
user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local - Open with
gorm.io/driver/mysql. - Run
initModels(),initUser(),runSeeders()(same as SQLite).
Adapted functions
Checkpoint()— if MariaDB, returnnil. If SQLite, existing WAL logic.IsSQLiteDB()— unchanged, only called for SQLite.ValidateSQLiteDB()— unchanged, only called for SQLite.
New dependency
gorm.io/driver/mysql added to go.mod.
Section 3: Data Migration (database/migrate.go)
New file with two functions:
MigrateSQLiteToMariaDB() error
- Open SQLite connection from
config.GetDBPath(). - Open MariaDB connection from JSON settings.
- For each table (users, inbounds, outbound_traffics, settings, inbound_client_ips, client_traffics, history_of_seeders):
- AutoMigrate the model on MariaDB.
SELECT *from SQLite →INSERTinto MariaDB using GORM raw SQL.
- On success: close connections (SQLite file kept as backup).
- On failure: return error with context.
MigrateMariaDBToSQLite() error
Reverse of above:
- Open MariaDB connection from JSON settings.
- Open/create SQLite connection at
config.GetDBPath(). - For each table: read from MariaDB, write to SQLite.
- On success: close connections.
- On failure: return error.
Row transfer approach: Use the existing model structs explicitly. For each table, query all rows from source into a []Model slice, then batch-insert into destination. This avoids raw SQL differences between SQLite and MySQL. Example for users:
var users []model.User
srcDB.Find(&users)
dstDB.CreateInBatches(&users, 100)
This pattern repeats for each of the 7 tables.
Section 4: main.go Changes
Updated callers
All database.InitDB(config.GetDBPath()) calls change to database.InitDB():
runWebServer()(line 49)resetSetting()(line 134)updateTgbotSetting()(line 221)updateSetting()(line 259)updateCert()(line 318)migrateDb()(line 395)
New migrate-db subcommand
case "migrate-db":
migrateDbBetweenDrivers()
migrateDbBetweenDrivers():
- Read
dbTypefrom JSON config. - If
dbType == "mariadb": calldatabase.MigrateSQLiteToMariaDB(). - If
dbType == "sqlite": calldatabase.MigrateMariaDBToSQLite(). - Print success/failure message.
New CLI flags
Add to setting subcommand:
-dbType string— set database type-dbHost string— set MariaDB host-dbPort string— set MariaDB port-dbUser string— set MariaDB username-dbPassword string— set MariaDB password-dbName string— set MariaDB database name
These call config.WriteSettingToJSON() to write directly to the JSON file. Only the 6 DB-related settings use WriteSettingToJSON() — all other settings (port, username, etc.) continue to use the existing SettingService methods that write through the database.
Section 5: web/service/server.go Changes
GetDb()
Add check at the top:
dbType, _ := s.GetDBType()
if dbType == "mariadb" {
return nil, common.NewError("Database export is not supported for MariaDB")
}
Existing SQLite logic unchanged.
ImportDB()
Add check at the top:
dbType, _ := s.GetDBType()
if dbType == "mariadb" {
return common.NewError("Database import is not supported for MariaDB")
}
Existing SQLite logic unchanged.
Section 6: x-ui.sh Changes
New menu option 27
Add to show_menu:
│────────────────────────────────────────────────│
│ ${green}27.${plain} 数据库管理 │
Add to the case statement:
27)
check_install && db_menu
;;
Update prompt: 请输入选择 [0-27]
db_menu() function
db_menu() {
# Read current dbType from JSON
local current_type=$(read_json_dbtype)
echo -e "
╔────────────────────────────────────────────────╗
│ ${green}数据库管理${plain} │
│ ${green}0.${plain} 返回主菜单 │
│ ${green}1.${plain} 查看当前数据库类型(当前: ${current_type}) │
│ ${green}2.${plain} 切换到 MariaDB │
│ ${green}3.${plain} 切换到 SQLite │
╚────────────────────────────────────────────────╝
"
read -rp "请输入选择 [0-3]:" num
case "${num}" in
0) show_menu ;;
1) db_show_status && db_menu ;;
2) db_switch_to_mariadb ;;
3) db_switch_to_sqlite ;;
*) echo "无效选项" && db_menu ;;
esac
}
db_switch_to_mariadb()
db_switch_to_mariadb() {
echo "请输入 MariaDB 连接信息(直接回车使用默认值):"
read -rp "MariaDB IP(默认 127.0.0.1): " db_host
db_host=${db_host:-127.0.0.1}
read -rp "MariaDB 端口(默认 3306): " db_port
db_port=${db_port:-3306}
read -rp "MariaDB 用户名: " db_user
if [ -z "$db_user" ]; then
echo -e "${red}用户名不能为空${plain}"
db_menu
return
fi
read -rsp "MariaDB 密码: " db_pass
echo
if [ -z "$db_pass" ]; then
echo -e "${red}密码不能为空${plain}"
db_menu
return
fi
read -rp "数据库名(默认 3xui): " db_name
db_name=${db_name:-3xui}
# Write settings to JSON config
/usr/local/x-ui/x-ui setting -dbType mariadb -dbHost "$db_host" -dbPort "$db_port" -dbUser "$db_user" -dbPassword "$db_pass" -dbName "$db_name"
# Migrate data
echo "正在迁移数据从 SQLite 到 MariaDB..."
/usr/local/x-ui/x-ui migrate-db
if [ $? -eq 0 ]; then
echo -e "${green}数据库切换成功,正在重启面板...${plain}"
restart
else
echo -e "${red}数据迁移失败,正在回滚到 SQLite...${plain}"
/usr/local/x-ui/x-ui setting -dbType sqlite
restart
fi
}
db_switch_to_sqlite()
db_switch_to_sqlite() {
/usr/local/x-ui/x-ui setting -dbType sqlite
echo "正在迁移数据从 MariaDB 到 SQLite..."
/usr/local/x-ui/x-ui migrate-db
if [ $? -eq 0 ]; then
echo -e "${green}数据库切换成功,正在重启面板...${plain}"
restart
else
echo -e "${red}数据迁移失败${plain}"
fi
}
Helper functions in x-ui.sh
read_json_dbtype()— readsdbTypefrom/etc/x-ui/x-ui.jsonusinggrep/sedor Python if available.db_show_status()— displays current DB type and connection info.
Files Changed
| File | Changes |
|---|---|
go.mod |
Add gorm.io/driver/mysql |
config/config.go |
Add GetDBTypeFromJSON(), WriteSettingToJSON() |
database/db.go |
Refactor InitDB() to be driver-agnostic, add initMariaDB(), adapt Checkpoint() |
database/migrate.go |
New file — MigrateSQLiteToMariaDB(), MigrateMariaDBToSQLite() |
main.go |
Update all InitDB calls, add migrate-db subcommand, add setting CLI flags |
web/service/setting.go |
Add 6 new settings + getter/setter methods |
web/service/server.go |
Guard GetDb()/ImportDB() for MariaDB |
x-ui.sh |
Add option 27, db_menu(), db_switch_to_mariadb(), db_switch_to_sqlite(), helpers |
Testing
- Fresh install with SQLite (default) — verify panel works as before
- Switch to MariaDB via x-ui.sh — verify data migrates and panel starts
- Switch back to SQLite — verify data migrates back
- Verify MariaDB CRUD operations (create inbound, modify settings, etc.)
- Verify GetDb/ImportDB return appropriate errors when using MariaDB
- Verify invalid MariaDB credentials show error and rollback to SQLite