mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-07 21:54:10 +00:00
- Replace plain textarea with CodeMirror editor (YAML syntax highlighting, line numbers, auto-indent) for Clash subscription template - Fix confAlerts crash when subClashURI/subURI/subJsonURI is null/undefined (prevented save button from enabling) - Add yaml.js CodeMirror mode asset - Include docs and .gitignore cleanup
332 lines
11 KiB
Markdown
332 lines
11 KiB
Markdown
# 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.sh` with 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()`
|
||
|
||
```go
|
||
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`
|
||
|
||
1. Read host, port, user, password, dbName from JSON config.
|
||
2. Build DSN: `user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local`
|
||
3. Open with `gorm.io/driver/mysql`.
|
||
4. Run `initModels()`, `initUser()`, `runSeeders()` (same as SQLite).
|
||
|
||
### Adapted functions
|
||
|
||
- `Checkpoint()` — if MariaDB, return `nil`. 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`
|
||
|
||
1. Open SQLite connection from `config.GetDBPath()`.
|
||
2. Open MariaDB connection from JSON settings.
|
||
3. For each table (users, inbounds, outbound_traffics, settings, inbound_client_ips, client_traffics, history_of_seeders):
|
||
- AutoMigrate the model on MariaDB.
|
||
- `SELECT *` from SQLite → `INSERT` into MariaDB using GORM raw SQL.
|
||
4. On success: close connections (SQLite file kept as backup).
|
||
5. On failure: return error with context.
|
||
|
||
### `MigrateMariaDBToSQLite() error`
|
||
|
||
Reverse of above:
|
||
1. Open MariaDB connection from JSON settings.
|
||
2. Open/create SQLite connection at `config.GetDBPath()`.
|
||
3. For each table: read from MariaDB, write to SQLite.
|
||
4. On success: close connections.
|
||
5. 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:
|
||
|
||
```go
|
||
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
|
||
|
||
```go
|
||
case "migrate-db":
|
||
migrateDbBetweenDrivers()
|
||
```
|
||
|
||
`migrateDbBetweenDrivers()`:
|
||
1. Read `dbType` from JSON config.
|
||
2. If `dbType == "mariadb"`: call `database.MigrateSQLiteToMariaDB()`.
|
||
3. If `dbType == "sqlite"`: call `database.MigrateMariaDBToSQLite()`.
|
||
4. 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:
|
||
```go
|
||
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:
|
||
```go
|
||
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:
|
||
```bash
|
||
27)
|
||
check_install && db_menu
|
||
;;
|
||
```
|
||
|
||
Update prompt: `请输入选择 [0-27]`
|
||
|
||
### `db_menu()` function
|
||
|
||
```bash
|
||
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()`
|
||
|
||
```bash
|
||
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()`
|
||
|
||
```bash
|
||
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()` — reads `dbType` from `/etc/x-ui/x-ui.json` using `grep`/`sed` or 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
|
||
|
||
1. Fresh install with SQLite (default) — verify panel works as before
|
||
2. Switch to MariaDB via x-ui.sh — verify data migrates and panel starts
|
||
3. Switch back to SQLite — verify data migrates back
|
||
4. Verify MariaDB CRUD operations (create inbound, modify settings, etc.)
|
||
5. Verify GetDb/ImportDB return appropriate errors when using MariaDB
|
||
6. Verify invalid MariaDB credentials show error and rollback to SQLite
|