From 82a1b85d45fc5a35c63f618c8c9e68ba84a8ab4c Mon Sep 17 00:00:00 2001 From: Sora39831 <540587985@qq.com> Date: Fri, 3 Apr 2026 09:14:20 +0800 Subject: [PATCH] docs: add MariaDB support design spec Design for adding MariaDB as alternative database backend with data migration, x-ui.sh switching UI, and driver-agnostic InitDB. --- .../2026-04-03-mariadb-support-design.md | 332 ++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-03-mariadb-support-design.md diff --git a/docs/superpowers/specs/2026-04-03-mariadb-support-design.md b/docs/superpowers/specs/2026-04-03-mariadb-support-design.md new file mode 100644 index 00000000..ee9138da --- /dev/null +++ b/docs/superpowers/specs/2026-04-03-mariadb-support-design.md @@ -0,0 +1,332 @@ +# 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