mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
- Close migration DB connections with defer to prevent leaks - Truncate destination tables before migration to avoid duplicates - Wrap migration in transaction for atomicity - Pass DB password via env var instead of CLI args to avoid process list exposure - Improve error messages for MariaDB export/import with alternatives - Update package doc to reflect dual DB support - DRY migration logic with shared migrateAllTables function
175 lines
4.9 KiB
Go
175 lines
4.9 KiB
Go
package database
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
|
|
"github.com/mhsanaei/3x-ui/v2/config"
|
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
|
|
|
"gorm.io/driver/mysql"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
// allModels returns the list of all model structs for migration.
|
|
func allModels() []any {
|
|
return []any{
|
|
&model.User{},
|
|
&model.Inbound{},
|
|
&model.OutboundTraffics{},
|
|
&model.Setting{},
|
|
&model.InboundClientIps{},
|
|
&xray.ClientTraffic{},
|
|
&model.HistoryOfSeeders{},
|
|
}
|
|
}
|
|
|
|
// tableNames returns the GORM table names for all models, in dependency order.
|
|
func tableNames() []string {
|
|
return []string{
|
|
"users",
|
|
"inbounds",
|
|
"outbound_traffics",
|
|
"settings",
|
|
"inbound_client_ips",
|
|
"client_traffics",
|
|
"history_of_seeders",
|
|
}
|
|
}
|
|
|
|
// openSQLite opens a SQLite connection for migration.
|
|
func openSQLite(dbPath string) (*gorm.DB, error) {
|
|
return gorm.Open(sqlite.Open(dbPath), &gorm.Config{Logger: logger.Discard})
|
|
}
|
|
|
|
// openMariaDB opens a MariaDB connection for migration.
|
|
func openMariaDB() (*gorm.DB, error) {
|
|
dbConfig := config.GetDBConfigFromJSON()
|
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
|
dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Name)
|
|
return gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: logger.Discard})
|
|
}
|
|
|
|
// closeDB safely closes the underlying SQL connection.
|
|
func closeDB(gdb *gorm.DB) {
|
|
if gdb == nil {
|
|
return
|
|
}
|
|
sqlDB, err := gdb.DB()
|
|
if err != nil {
|
|
return
|
|
}
|
|
sqlDB.Close()
|
|
}
|
|
|
|
// migrateTable copies all rows from src table to dst table using the given model slice.
|
|
// It returns the number of rows migrated.
|
|
func migrateTable[T any](src, dst *gorm.DB, tableName string) (int64, error) {
|
|
var rows []T
|
|
if err := src.Table(tableName).Find(&rows).Error; err != nil {
|
|
return 0, fmt.Errorf("reading from %s: %w", tableName, err)
|
|
}
|
|
if len(rows) == 0 {
|
|
return 0, nil
|
|
}
|
|
if err := dst.Table(tableName).CreateInBatches(&rows, 100).Error; err != nil {
|
|
return 0, fmt.Errorf("writing to %s: %w", tableName, err)
|
|
}
|
|
return int64(len(rows)), nil
|
|
}
|
|
|
|
// migrateAllTables copies all data between two database connections within a transaction.
|
|
func migrateAllTables(src, dst *gorm.DB) error {
|
|
// Truncate destination tables and migrate within a transaction
|
|
return dst.Transaction(func(tx *gorm.DB) error {
|
|
// Clear destination tables in reverse dependency order
|
|
for i := len(tableNames()) - 1; i >= 0; i-- {
|
|
name := tableNames()[i]
|
|
if err := tx.Exec("DELETE FROM " + name).Error; err != nil {
|
|
return fmt.Errorf("failed to clear %s: %w", name, err)
|
|
}
|
|
}
|
|
|
|
total := int64(0)
|
|
for _, name := range tableNames() {
|
|
var count int64
|
|
var err error
|
|
switch name {
|
|
case "users":
|
|
count, err = migrateTable[model.User](src, tx, name)
|
|
case "inbounds":
|
|
count, err = migrateTable[model.Inbound](src, tx, name)
|
|
case "outbound_traffics":
|
|
count, err = migrateTable[model.OutboundTraffics](src, tx, name)
|
|
case "settings":
|
|
count, err = migrateTable[model.Setting](src, tx, name)
|
|
case "inbound_client_ips":
|
|
count, err = migrateTable[model.InboundClientIps](src, tx, name)
|
|
case "client_traffics":
|
|
count, err = migrateTable[xray.ClientTraffic](src, tx, name)
|
|
case "history_of_seeders":
|
|
count, err = migrateTable[model.HistoryOfSeeders](src, tx, name)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("migration failed for %s: %w", name, err)
|
|
}
|
|
total += count
|
|
log.Printf("Migrated %d rows from %s", count, name)
|
|
}
|
|
log.Printf("Migration complete: %d total rows", total)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// MigrateSQLiteToMariaDB copies all data from the SQLite database to MariaDB.
|
|
// The SQLite file is kept as a backup.
|
|
func MigrateSQLiteToMariaDB() error {
|
|
srcDB, err := openSQLite(config.GetDBPath())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open SQLite source: %w", err)
|
|
}
|
|
defer closeDB(srcDB)
|
|
|
|
dstDB, err := openMariaDB()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open MariaDB destination: %w", err)
|
|
}
|
|
defer closeDB(dstDB)
|
|
|
|
// AutoMigrate all tables on destination
|
|
for _, m := range allModels() {
|
|
if err := dstDB.AutoMigrate(m); err != nil {
|
|
return fmt.Errorf("failed to migrate table on MariaDB: %w", err)
|
|
}
|
|
}
|
|
|
|
return migrateAllTables(srcDB, dstDB)
|
|
}
|
|
|
|
// MigrateMariaDBToSQLite copies all data from MariaDB to the SQLite database.
|
|
// A new SQLite file is created (or overwritten) at the configured path.
|
|
func MigrateMariaDBToSQLite() error {
|
|
srcDB, err := openMariaDB()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open MariaDB source: %w", err)
|
|
}
|
|
defer closeDB(srcDB)
|
|
|
|
dstDB, err := openSQLite(config.GetDBPath())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open SQLite destination: %w", err)
|
|
}
|
|
defer closeDB(dstDB)
|
|
|
|
// AutoMigrate all tables on destination
|
|
for _, m := range allModels() {
|
|
if err := dstDB.AutoMigrate(m); err != nil {
|
|
return fmt.Errorf("failed to migrate table on SQLite: %w", err)
|
|
}
|
|
}
|
|
|
|
return migrateAllTables(srcDB, dstDB)
|
|
}
|