feat(db): split inbound tables to optional mysql with sqlite fallback

This commit is contained in:
Yasin Abbasi 2026-02-16 01:26:54 +03:30
parent 5b796672e9
commit f57364e78b
6 changed files with 306 additions and 100 deletions

View file

@ -1,4 +1,15 @@
XUI_DEBUG=true
XUI_DB_FOLDER=x-ui
XUI_LOG_FOLDER=x-ui
XUI_BIN_FOLDER=x-ui
XUI_BIN_FOLDER=x-ui
# Optional: use MySQL only for inbounds and client_traffics tables.
# If XUI_MYSQL_DSN is set, it has priority over host/user/password fields.
# Example DSN: user:pass@tcp(127.0.0.1:3306)/xui_shared?charset=utf8mb4&parseTime=True&loc=Local
XUI_MYSQL_DSN=
XUI_MYSQL_HOST=
XUI_MYSQL_PORT=3306
XUI_MYSQL_USER=
XUI_MYSQL_PASSWORD=
XUI_MYSQL_DB=
XUI_MYSQL_PARAMS=charset=utf8mb4&parseTime=True&loc=Local

View file

@ -1,5 +1,5 @@
// Package database provides database initialization, migration, and management utilities
// for the 3x-ui panel using GORM with SQLite.
// for the 3x-ui panel using GORM with SQLite and optional MySQL split storage.
package database
import (
@ -11,34 +11,38 @@ import (
"os"
"path"
"slices"
"strings"
"github.com/mhsanaei/3x-ui/v2/config"
"github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/util/crypto"
"github.com/mhsanaei/3x-ui/v2/xray"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var db *gorm.DB
var inboundDB *gorm.DB
const (
defaultUsername = "admin"
defaultPassword = "admin"
)
func initModels() error {
func initSQLiteModels(includeInboundModels bool) error {
models := []any{
&model.User{},
&model.Inbound{},
&model.OutboundTraffics{},
&model.Setting{},
&model.InboundClientIps{},
&xray.ClientTraffic{},
&model.HistoryOfSeeders{},
}
if includeInboundModels {
models = append(models, &model.Inbound{}, &xray.ClientTraffic{})
}
for _, model := range models {
if err := db.AutoMigrate(model); err != nil {
log.Printf("Error auto migrating model: %v", err)
@ -48,6 +52,87 @@ func initModels() error {
return nil
}
func initInboundModels() error {
if inboundDB == nil {
return errors.New("inbound database is nil")
}
models := []any{
&model.Inbound{},
&xray.ClientTraffic{},
}
for _, model := range models {
if err := inboundDB.AutoMigrate(model); err != nil {
log.Printf("Error auto migrating inbound model: %v", err)
return err
}
}
return nil
}
func migrateInboundDataIfNeeded() error {
if inboundDB == nil || db == nil || inboundDB == db {
return nil
}
var mysqlInboundCount int64
if err := inboundDB.Model(&model.Inbound{}).Count(&mysqlInboundCount).Error; err != nil {
return err
}
if mysqlInboundCount == 0 {
var sqliteInbounds []model.Inbound
if err := db.Model(&model.Inbound{}).Find(&sqliteInbounds).Error; err != nil {
return err
}
if len(sqliteInbounds) > 0 {
if err := inboundDB.CreateInBatches(&sqliteInbounds, 200).Error; err != nil {
return err
}
}
}
var mysqlClientTrafficCount int64
if err := inboundDB.Model(&xray.ClientTraffic{}).Count(&mysqlClientTrafficCount).Error; err != nil {
return err
}
if mysqlClientTrafficCount == 0 {
var sqliteClientTraffics []xray.ClientTraffic
if err := db.Model(&xray.ClientTraffic{}).Find(&sqliteClientTraffics).Error; err != nil {
return err
}
if len(sqliteClientTraffics) > 0 {
if err := inboundDB.CreateInBatches(&sqliteClientTraffics, 500).Error; err != nil {
return err
}
}
}
return nil
}
func getMySQLDSN() string {
if dsn := strings.TrimSpace(os.Getenv("XUI_MYSQL_DSN")); dsn != "" {
return dsn
}
host := strings.TrimSpace(os.Getenv("XUI_MYSQL_HOST"))
port := strings.TrimSpace(os.Getenv("XUI_MYSQL_PORT"))
user := strings.TrimSpace(os.Getenv("XUI_MYSQL_USER"))
pass := os.Getenv("XUI_MYSQL_PASSWORD")
dbName := strings.TrimSpace(os.Getenv("XUI_MYSQL_DB"))
params := strings.TrimSpace(os.Getenv("XUI_MYSQL_PARAMS"))
if host == "" || user == "" || dbName == "" {
return ""
}
if port == "" {
port = "3306"
}
if params == "" {
params = "charset=utf8mb4&parseTime=True&loc=Local"
}
return user + ":" + pass + "@tcp(" + host + ":" + port + ")/" + dbName + "?" + params
}
// initUser creates a default admin user if the users table is empty.
func initUser() error {
empty, err := isTableEmpty("users")
@ -142,10 +227,29 @@ func InitDB(dbPath string) error {
if err != nil {
return err
}
inboundDB = db
if err := initModels(); err != nil {
mysqlDSN := getMySQLDSN()
useDedicatedInboundDB := mysqlDSN != ""
if useDedicatedInboundDB {
mysqlInboundDB, mysqlErr := gorm.Open(mysql.Open(mysqlDSN), c)
if mysqlErr != nil {
return mysqlErr
}
inboundDB = mysqlInboundDB
}
if err := initSQLiteModels(!useDedicatedInboundDB); err != nil {
return err
}
if useDedicatedInboundDB {
if err := initInboundModels(); err != nil {
return err
}
if err := migrateInboundDataIfNeeded(); err != nil {
return err
}
}
isUsersEmpty, err := isTableEmpty("users")
if err != nil {
@ -160,14 +264,31 @@ func InitDB(dbPath string) error {
// CloseDB closes the database connection if it exists.
func CloseDB() error {
var closeErr error
if inboundDB != nil && db != nil && inboundDB != db {
sqlInboundDB, err := inboundDB.DB()
if err != nil {
closeErr = err
} else {
closeErr = sqlInboundDB.Close()
}
}
if db != nil {
sqlDB, err := db.DB()
if err != nil {
if closeErr != nil {
return closeErr
}
return err
}
if err = sqlDB.Close(); err != nil {
if closeErr != nil {
return closeErr
}
return err
}
return sqlDB.Close()
}
return nil
return closeErr
}
// GetDB returns the global GORM database instance.
@ -175,6 +296,14 @@ func GetDB() *gorm.DB {
return db
}
// GetInboundDB returns the DB used for inbounds and client traffics.
func GetInboundDB() *gorm.DB {
if inboundDB != nil {
return inboundDB
}
return db
}
// IsNotFound checks if the given error is a GORM record not found error.
func IsNotFound(err error) bool {
return err == gorm.ErrRecordNotFound

1
go.mod
View file

@ -26,6 +26,7 @@ require (
golang.org/x/sys v0.41.0
golang.org/x/text v0.34.0
google.golang.org/grpc v1.78.0
gorm.io/driver/mysql v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
)

View file

@ -113,20 +113,30 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C
}
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
db := database.GetDB()
db := database.GetInboundDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where(`id in (
SELECT DISTINCT inbounds.id
FROM inbounds,
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
WHERE
protocol in ('vmess','vless','trojan','shadowsocks')
AND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ?
)`, subId, true).Find(&inbounds).Error
err := db.Model(model.Inbound{}).
Preload("ClientStats").
Where("enable = ?", true).
Where("protocol IN ?", []model.Protocol{model.VMESS, model.VLESS, model.Trojan, model.Shadowsocks}).
Find(&inbounds).Error
if err != nil {
return nil, err
}
return inbounds, nil
filtered := make([]*model.Inbound, 0, len(inbounds))
for _, inbound := range inbounds {
clients, cErr := s.inboundService.GetClients(inbound)
if cErr != nil {
continue
}
for _, client := range clients {
if client.Enable && client.SubID == subId {
filtered = append(filtered, inbound)
break
}
}
}
return filtered, nil
}
func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email string) xray.ClientTraffic {
@ -139,16 +149,42 @@ func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email stri
}
func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) {
db := database.GetDB()
var inbound *model.Inbound
err := db.Model(model.Inbound{}).
Where("JSON_TYPE(settings, '$.fallbacks') = 'array'").
Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
Find(&inbound).Error
if err != nil {
db := database.GetInboundDB()
var inbounds []*model.Inbound
if err := db.Model(model.Inbound{}).Find(&inbounds).Error; err != nil {
return "", 0, "", err
}
var inbound *model.Inbound
for _, candidate := range inbounds {
var settings map[string]any
if err := json.Unmarshal([]byte(candidate.Settings), &settings); err != nil {
continue
}
fallbacks, ok := settings["fallbacks"].([]any)
if !ok {
continue
}
match := false
for _, item := range fallbacks {
fallback, ok := item.(map[string]any)
if !ok {
continue
}
if fallbackDest, ok := fallback["dest"].(string); ok && fallbackDest == dest {
match = true
break
}
}
if match {
inbound = candidate
break
}
}
if inbound == nil {
return "", 0, "", fmt.Errorf("fallback master not found for dest %s", dest)
}
var stream map[string]any
json.Unmarshal([]byte(streamSettings), &stream)
var masterStream map[string]any

View file

@ -94,7 +94,7 @@ func (j *CheckClientIpJob) clearAccessLog() {
}
func (j *CheckClientIpJob) hasLimitIp() bool {
db := database.GetDB()
db := database.GetInboundDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Find(&inbounds).Error
@ -440,7 +440,7 @@ func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, c
}
func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) {
db := database.GetDB()
db := database.GetInboundDB()
inbound := &model.Inbound{}
err := db.Model(&model.Inbound{}).Where("settings LIKE ?", "%"+clientEmail+"%").First(inbound).Error

View file

@ -29,7 +29,7 @@ type InboundService struct {
// GetInbounds retrieves all inbounds for a specific user.
// Returns a slice of inbound models with their associated client statistics.
func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
db := database.GetDB()
db := database.GetInboundDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("user_id = ?", userId).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
@ -60,7 +60,7 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
// GetAllInbounds retrieves all inbounds from the database.
// Returns a slice of all inbound models with their associated client statistics.
func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
db := database.GetDB()
db := database.GetInboundDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
@ -88,7 +88,7 @@ func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
}
func (s *InboundService) GetInboundsByTrafficReset(period string) ([]*model.Inbound, error) {
db := database.GetDB()
db := database.GetInboundDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Where("traffic_reset = ?", period).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
@ -98,7 +98,7 @@ func (s *InboundService) GetInboundsByTrafficReset(period string) ([]*model.Inbo
}
func (s *InboundService) checkPortExist(listen string, port int, ignoreId int) (bool, error) {
db := database.GetDB()
db := database.GetInboundDB()
if listen == "" || listen == "0.0.0.0" || listen == "::" || listen == "::0" {
db = db.Model(model.Inbound{}).Where("port = ?", port)
} else {
@ -142,16 +142,29 @@ func (s *InboundService) GetClients(inbound *model.Inbound) ([]model.Client, err
}
func (s *InboundService) getAllEmails() ([]string, error) {
db := database.GetDB()
var emails []string
err := db.Raw(`
SELECT JSON_EXTRACT(client.value, '$.email')
FROM inbounds,
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
`).Scan(&emails).Error
db := database.GetInboundDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Select("settings").Find(&inbounds).Error
if err != nil {
return nil, err
}
emailSet := make(map[string]struct{})
for _, inbound := range inbounds {
clients, cErr := s.GetClients(inbound)
if cErr != nil {
continue
}
for _, client := range clients {
if client.Email == "" {
continue
}
emailSet[client.Email] = struct{}{}
}
}
emails := make([]string, 0, len(emailSet))
for email := range emailSet {
emails = append(emails, email)
}
return emails, nil
}
@ -277,7 +290,7 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
}
}
db := database.GetDB()
db := database.GetInboundDB()
tx := db.Begin()
defer func() {
if err == nil {
@ -323,7 +336,7 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
// It removes the inbound from the database and the running Xray instance if active.
// Returns whether Xray needs restart and any error.
func (s *InboundService) DelInbound(id int) (bool, error) {
db := database.GetDB()
db := database.GetInboundDB()
var tag string
needRestart := false
@ -366,7 +379,7 @@ func (s *InboundService) DelInbound(id int) (bool, error) {
}
func (s *InboundService) GetInbound(id int) (*model.Inbound, error) {
db := database.GetDB()
db := database.GetInboundDB()
inbound := &model.Inbound{}
err := db.Model(model.Inbound{}).First(inbound, id).Error
if err != nil {
@ -394,7 +407,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
tag := oldInbound.Tag
db := database.GetDB()
db := database.GetInboundDB()
tx := db.Begin()
defer func() {
@ -631,7 +644,7 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
oldInbound.Settings = string(newSettings)
db := database.GetDB()
db := database.GetInboundDB()
tx := db.Begin()
defer func() {
@ -723,7 +736,7 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
oldInbound.Settings = string(newSettings)
db := database.GetDB()
db := database.GetInboundDB()
err = s.DelClientIPs(db, email)
if err != nil {
@ -861,7 +874,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
}
oldInbound.Settings = string(newSettings)
db := database.GetDB()
db := database.GetInboundDB()
tx := db.Begin()
defer func() {
@ -941,7 +954,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
var err error
db := database.GetDB()
db := database.GetInboundDB()
tx := db.Begin()
defer func() {
@ -1300,7 +1313,7 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
}
func (s *InboundService) GetInboundTags() (string, error) {
db := database.GetDB()
db := database.GetInboundDB()
var inboundTags []string
err := db.Model(model.Inbound{}).Select("tag").Find(&inboundTags).Error
if err != nil && err != gorm.ErrRecordNotFound {
@ -1311,15 +1324,21 @@ func (s *InboundService) GetInboundTags() (string, error) {
}
func (s *InboundService) MigrationRemoveOrphanedTraffics() {
db := database.GetDB()
db.Exec(`
DELETE FROM client_traffics
WHERE email NOT IN (
SELECT JSON_EXTRACT(client.value, '$.email')
FROM inbounds,
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
)
`)
db := database.GetInboundDB()
emails, err := s.getAllEmails()
if err != nil {
logger.Warningf("MigrationRemoveOrphanedTraffics failed to load emails: %v", err)
return
}
if len(emails) == 0 {
if err := db.Where("1 = 1").Delete(xray.ClientTraffic{}).Error; err != nil {
logger.Warningf("MigrationRemoveOrphanedTraffics delete-all failed: %v", err)
}
return
}
if err := db.Where("email NOT IN ?", emails).Delete(xray.ClientTraffic{}).Error; err != nil {
logger.Warningf("MigrationRemoveOrphanedTraffics delete-orphans failed: %v", err)
}
}
func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model.Client) error {
@ -1352,7 +1371,7 @@ func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *mod
}
func (s *InboundService) UpdateClientIPs(tx *gorm.DB, oldEmail string, newEmail string) error {
return tx.Model(model.InboundClientIps{}).Where("client_email = ?", oldEmail).Update("client_email", newEmail).Error
return database.GetDB().Model(model.InboundClientIps{}).Where("client_email = ?", oldEmail).Update("client_email", newEmail).Error
}
func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error {
@ -1360,11 +1379,11 @@ func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error {
}
func (s *InboundService) DelClientIPs(tx *gorm.DB, email string) error {
return tx.Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error
return database.GetDB().Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error
}
func (s *InboundService) GetClientInboundByTrafficID(trafficId int) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {
db := database.GetDB()
db := database.GetInboundDB()
var traffics []*xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).Where("id = ?", trafficId).Find(&traffics).Error
if err != nil {
@ -1379,7 +1398,7 @@ func (s *InboundService) GetClientInboundByTrafficID(trafficId int) (traffic *xr
}
func (s *InboundService) GetClientInboundByEmail(email string) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {
db := database.GetDB()
db := database.GetInboundDB()
var traffics []*xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error
if err != nil {
@ -1766,7 +1785,7 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
}
func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error {
db := database.GetDB()
db := database.GetInboundDB()
// Reset traffic stats in ClientTraffic table
result := db.Model(xray.ClientTraffic{}).
@ -1834,7 +1853,7 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (bool, e
traffic.Down = 0
traffic.Enable = true
db := database.GetDB()
db := database.GetInboundDB()
err = db.Save(traffic).Error
if err != nil {
return false, err
@ -1844,7 +1863,7 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (bool, e
}
func (s *InboundService) ResetAllClientTraffics(id int) error {
db := database.GetDB()
db := database.GetInboundDB()
now := time.Now().Unix() * 1000
return db.Transaction(func(tx *gorm.DB) error {
@ -1881,7 +1900,7 @@ func (s *InboundService) ResetAllClientTraffics(id int) error {
}
func (s *InboundService) ResetAllTraffics() error {
db := database.GetDB()
db := database.GetInboundDB()
result := db.Model(model.Inbound{}).
Where("user_id > ?", 0).
@ -1892,7 +1911,7 @@ func (s *InboundService) ResetAllTraffics() error {
}
func (s *InboundService) DelDepletedClients(id int) (err error) {
db := database.GetDB()
db := database.GetInboundDB()
tx := db.Begin()
defer func() {
if err == nil {
@ -1977,7 +1996,7 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
}
func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffic, error) {
db := database.GetDB()
db := database.GetInboundDB()
var inbounds []*model.Inbound
// Retrieve inbounds where settings contain the given tgId
@ -2041,7 +2060,7 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl
}
func (s *InboundService) UpdateClientTrafficByEmail(email string, upload int64, download int64) error {
db := database.GetDB()
db := database.GetInboundDB()
result := db.Model(xray.ClientTraffic{}).
Where("email = ?", email).
@ -2056,16 +2075,30 @@ func (s *InboundService) UpdateClientTrafficByEmail(email string, upload int64,
}
func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic, error) {
db := database.GetDB()
db := database.GetInboundDB()
var traffics []xray.ClientTraffic
err := db.Model(xray.ClientTraffic{}).Where(`email IN(
SELECT JSON_EXTRACT(client.value, '$.email') as email
FROM inbounds,
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
WHERE
JSON_EXTRACT(client.value, '$.id') in (?)
)`, id).Find(&traffics).Error
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Find(&inbounds).Error
if err != nil {
logger.Debug(err)
return nil, err
}
emails := make([]string, 0)
for _, inbound := range inbounds {
clients, cErr := s.GetClients(inbound)
if cErr != nil {
continue
}
for _, client := range clients {
if client.ID == id && client.Email != "" {
emails = append(emails, client.Email)
}
}
}
if len(emails) == 0 {
return traffics, nil
}
err = db.Model(xray.ClientTraffic{}).Where("email IN ?", emails).Find(&traffics).Error
if err != nil {
logger.Debug(err)
@ -2083,7 +2116,7 @@ func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic,
}
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
db := database.GetDB()
db := database.GetInboundDB()
inbound := &model.Inbound{}
traffic = &xray.ClientTraffic{}
@ -2195,7 +2228,7 @@ func (s *InboundService) ClearClientIps(clientEmail string) error {
}
func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error) {
db := database.GetDB()
db := database.GetInboundDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("remark like ?", "%"+query+"%").Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
@ -2205,13 +2238,14 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error)
}
func (s *InboundService) MigrationRequirements() {
db := database.GetDB()
db := database.GetInboundDB()
sqliteDB := database.GetDB()
tx := db.Begin()
var err error
defer func() {
if err == nil {
tx.Commit()
if dbErr := db.Exec(`VACUUM "main"`).Error; dbErr != nil {
if dbErr := sqliteDB.Exec(`VACUUM "main"`).Error; dbErr != nil {
logger.Warningf("VACUUM failed: %v", dbErr)
}
} else {
@ -2313,24 +2347,17 @@ func (s *InboundService) MigrationRequirements() {
tx.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
// Migrate old MultiDomain to External Proxy
var externalProxy []struct {
Id int
Port int
StreamSettings []byte
}
err = tx.Raw(`select id, port, stream_settings
from inbounds
WHERE protocol in ('vmess','vless','trojan')
AND json_extract(stream_settings, '$.security') = 'tls'
AND json_extract(stream_settings, '$.tlsSettings.settings.domains') IS NOT NULL`).Scan(&externalProxy).Error
if err != nil || len(externalProxy) == 0 {
return
}
for _, ep := range externalProxy {
for _, ep := range inbounds {
var reverses any
var stream map[string]any
json.Unmarshal(ep.StreamSettings, &stream)
if err := json.Unmarshal([]byte(ep.StreamSettings), &stream); err != nil {
continue
}
security, _ := stream["security"].(string)
if security != "tls" {
continue
}
if tlsSettings, ok := stream["tlsSettings"].(map[string]any); ok {
if settings, ok := tlsSettings["settings"].(map[string]any); ok {
if domains, ok := settings["domains"].([]any); ok {
@ -2338,7 +2365,9 @@ func (s *InboundService) MigrationRequirements() {
if domainMap, ok := domain.(map[string]any); ok {
domainMap["forceTls"] = "same"
domainMap["port"] = ep.Port
domainMap["dest"] = domainMap["domain"].(string)
if domainVal, ok2 := domainMap["domain"].(string); ok2 {
domainMap["dest"] = domainVal
}
delete(domainMap, "domain")
}
}
@ -2370,7 +2399,7 @@ func (s *InboundService) GetOnlineClients() []string {
}
func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) {
db := database.GetDB()
db := database.GetInboundDB()
var rows []xray.ClientTraffic
err := db.Model(&xray.ClientTraffic{}).Select("email, last_online").Find(&rows).Error
if err != nil && err != gorm.ErrRecordNotFound {
@ -2384,7 +2413,7 @@ func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) {
}
func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) {
db := database.GetDB()
db := database.GetInboundDB()
// Step 1: Get ClientTraffic records for emails in the input list
var clients []xray.ClientTraffic
@ -2466,7 +2495,7 @@ func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (b
oldInbound.Settings = string(newSettings)
db := database.GetDB()
db := database.GetInboundDB()
// remove IP bindings
if err := s.DelClientIPs(db, email); err != nil {