migration manager

This commit is contained in:
Alireza Ahmadi 2026-05-31 18:15:56 +02:00
parent 150f875b77
commit e09cdc97a9
15 changed files with 673 additions and 492 deletions

View file

@ -15,6 +15,9 @@ var version string
//go:embed name
var name string
//go:embed default_xray.json
var defaultXrayTemplate string
type LogLevel string
const (
@ -86,3 +89,7 @@ func GetDBFolderPath() string {
func GetDBPath() string {
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
}
func GetDefaultXrayTemplate() string {
return defaultXrayTemplate
}

View file

@ -39,4 +39,4 @@
"rules": []
},
"stats": {}
}
}

View file

@ -8,6 +8,7 @@ import (
"path"
"github.com/alireza0/x-ui/config"
"github.com/alireza0/x-ui/database/migrations"
"github.com/alireza0/x-ui/database/model"
"github.com/alireza0/x-ui/util/common"
"github.com/alireza0/x-ui/xray"
@ -77,7 +78,7 @@ func InitDB(dbPath string) error {
return err
}
return nil
return migrations.Run(db)
}
func CloseDB() error {

View file

@ -0,0 +1,5 @@
package migrations
func removeIndex(s []interface{}, index int) []interface{} {
return append(s[:index], s[index+1:]...)
}

View file

@ -0,0 +1,60 @@
package migrations
import (
"gorm.io/gorm"
)
const (
VersionOutbound = 2
VersionRouting = 3
VersionInbound = 4
)
type migration struct {
version int
fn func(*gorm.DB) error
}
var registry = []migration{
{VersionOutbound, migrateV002Outbound},
{VersionRouting, migrateV003Routing},
{VersionInbound, migrateV004Inbound},
}
func Run(db *gorm.DB) error {
if err := ensureSchemaVersionTable(db); err != nil {
return err
}
for _, m := range registry {
applied, err := isVersionApplied(db, m.version)
if err != nil {
return err
}
if applied {
continue
}
if err := m.fn(db); err != nil {
return err
}
if err := recordVersion(db, m.version); err != nil {
return err
}
}
return nil
}
func ensureSchemaVersionTable(db *gorm.DB) error {
return db.Exec(`CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER PRIMARY KEY
)`).Error
}
func isVersionApplied(db *gorm.DB, version int) (bool, error) {
var count int64
err := db.Raw(`SELECT COUNT(*) FROM schema_version WHERE version = ?`, version).Scan(&count).Error
return count > 0, err
}
func recordVersion(db *gorm.DB, version int) error {
return db.Exec(`INSERT INTO schema_version (version) VALUES (?)`, version).Error
}

View file

@ -0,0 +1,38 @@
package migrations
import (
"github.com/alireza0/x-ui/config"
"github.com/alireza0/x-ui/database/model"
"gorm.io/gorm"
)
const xrayTemplateConfigKey = "xrayTemplateConfig"
func getXrayTemplate(db *gorm.DB) (string, error) {
setting := &model.Setting{}
err := db.Model(model.Setting{}).Where("key = ?", xrayTemplateConfigKey).First(setting).Error
if err == gorm.ErrRecordNotFound {
return config.GetDefaultXrayTemplate(), nil
}
if err != nil {
return "", err
}
return setting.Value, nil
}
func saveXrayTemplate(db *gorm.DB, value string) error {
setting := &model.Setting{}
err := db.Model(model.Setting{}).Where("key = ?", xrayTemplateConfigKey).First(setting).Error
if err == gorm.ErrRecordNotFound {
return db.Create(&model.Setting{
Key: xrayTemplateConfigKey,
Value: value,
}).Error
}
if err != nil {
return err
}
setting.Value = value
return db.Save(setting).Error
}

View file

@ -0,0 +1,143 @@
package migrations
import (
"encoding/json"
"github.com/alireza0/x-ui/database/model"
"github.com/alireza0/x-ui/logger"
"gorm.io/gorm"
)
func migrateV002Outbound(db *gorm.DB) error {
var count int64
if err := db.Model(&model.Outbound{}).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
if err := migrateOutboundsFromTemplate(db); err != nil {
return err
}
if err := db.Model(&model.Outbound{}).Count(&count).Error; err != nil {
return err
}
if count == 0 {
initDefaultOutbounds(db)
}
return nil
}
func migrateOutboundsFromTemplate(db *gorm.DB) error {
templateConfig, err := getXrayTemplate(db)
if err != nil {
logger.Warning("outbound migration: get xray template failed:", err)
return nil
}
var cfg map[string]interface{}
if err := json.Unmarshal([]byte(templateConfig), &cfg); err != nil {
logger.Warning("outbound migration: parse template failed:", err)
return nil
}
rawOutbounds, ok := cfg["outbounds"].([]interface{})
if !ok || len(rawOutbounds) == 0 {
return nil
}
migrated := 0
for i, item := range rawOutbounds {
raw, ok := item.(map[string]interface{})
if !ok {
continue
}
outbound := outboundFromMap(raw, i)
if outbound.Tag == "" || outbound.Protocol == "" {
continue
}
if err := db.Create(outbound).Error; err != nil {
logger.Warning("outbound migration: create failed:", err)
continue
}
migrated++
}
if migrated == 0 {
return nil
}
cfg["outbounds"] = []interface{}{}
newTemplate, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
logger.Warning("outbound migration: marshal template failed:", err)
return nil
}
if err := saveXrayTemplate(db, string(newTemplate)); err != nil {
logger.Warning("outbound migration: save template failed:", err)
return nil
}
logger.Info("Migrated", migrated, "outbound(s) from xray settings to database")
return nil
}
func initDefaultOutbounds(db *gorm.DB) {
for _, outbound := range defaultOutbounds() {
if err := db.Create(outbound).Error; err != nil {
logger.Warning("outbound init: create default failed:", err)
}
}
logger.Info("Initialized default outbound(s): direct, blocked")
}
func outboundFromMap(raw map[string]interface{}, sort int) *model.Outbound {
o := &model.Outbound{Sort: sort}
if v, ok := raw["sendThrough"].(string); ok {
o.SendThrough = v
}
if v, ok := raw["protocol"].(string); ok {
o.Protocol = v
}
if v, ok := raw["tag"].(string); ok {
o.Tag = v
}
if v, ok := raw["targetStrategy"].(string); ok {
o.TargetStrategy = v
}
if v, ok := raw["settings"]; ok && v != nil {
b, _ := json.MarshalIndent(v, "", " ")
o.Settings = string(b)
}
if v, ok := raw["streamSettings"]; ok && v != nil {
b, _ := json.MarshalIndent(v, "", " ")
o.StreamSettings = string(b)
}
if v, ok := raw["proxySettings"]; ok && v != nil {
b, _ := json.MarshalIndent(v, "", " ")
o.ProxySettings = string(b)
}
if v, ok := raw["mux"]; ok && v != nil {
b, _ := json.MarshalIndent(v, "", " ")
o.Mux = string(b)
}
return o
}
func defaultOutbounds() []*model.Outbound {
return []*model.Outbound{
{
Sort: 0,
Protocol: "freedom",
Tag: "direct",
Settings: `{"domainStrategy":"UseIP","noises":[],"redirect":""}`,
},
{
Sort: 1,
Protocol: "blackhole",
Tag: "blocked",
Settings: `{}`,
},
}
}

View file

@ -0,0 +1,224 @@
package migrations
import (
"encoding/json"
"fmt"
"github.com/alireza0/x-ui/config"
"github.com/alireza0/x-ui/database/model"
"github.com/alireza0/x-ui/logger"
"gorm.io/gorm"
)
func migrateV003Routing(db *gorm.DB) error {
templateConfig, err := getXrayTemplate(db)
if err != nil {
logger.Warning("routing rule migration: get xray template failed:", err)
return nil
}
var cfg map[string]interface{}
if err := json.Unmarshal([]byte(templateConfig), &cfg); err != nil {
logger.Warning("routing rule migration: parse template failed:", err)
return nil
}
changed := migrateLegacyApiConfig(cfg)
if changed {
newTemplate, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
logger.Warning("routing rule migration: marshal legacy api config failed:", err)
return nil
}
if err := saveXrayTemplate(db, string(newTemplate)); err != nil {
logger.Warning("routing rule migration: save legacy api config failed:", err)
return nil
}
logger.Info("Migrated legacy api inbound to api.listen in xray settings")
}
var count int64
if err := db.Model(&model.RoutingRule{}).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
routing, ok := cfg["routing"].(map[string]interface{})
if !ok {
return nil
}
rawRules, ok := routing["rules"].([]interface{})
if !ok || len(rawRules) == 0 {
return nil
}
for i, item := range rawRules {
raw, ok := item.(map[string]interface{})
if !ok {
continue
}
rule := routingRuleFromMap(raw, i)
if err := db.Create(rule).Error; err != nil {
logger.Warning("routing rule migration: create failed:", err)
}
}
routing["rules"] = []interface{}{}
cfg["routing"] = routing
newTemplate, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
logger.Warning("routing rule migration: marshal template failed:", err)
return nil
}
if err := saveXrayTemplate(db, string(newTemplate)); err != nil {
logger.Warning("routing rule migration: save template failed:", err)
return nil
}
logger.Info("Migrated", len(rawRules), "routing rule(s) from xray settings to database")
return nil
}
func apiListenMissing(api map[string]interface{}) bool {
_, ok := api["listen"]
return !ok
}
func defaultApiListen() string {
var cfg map[string]interface{}
if err := json.Unmarshal([]byte(config.GetDefaultXrayTemplate()), &cfg); err != nil {
return "127.0.0.1:62789"
}
api, ok := cfg["api"].(map[string]interface{})
if !ok {
return "127.0.0.1:62789"
}
s, _ := api["listen"].(string)
if s == "" {
return "127.0.0.1:62789"
}
return s
}
func inboundListenAddress(inbound map[string]interface{}) string {
port := 0
switch p := inbound["port"].(type) {
case float64:
port = int(p)
case int:
port = p
case int64:
port = int(p)
}
if port == 0 {
return ""
}
host := "127.0.0.1"
if listen, ok := inbound["listen"]; ok {
switch v := listen.(type) {
case string:
if v != "" {
host = v
}
case []interface{}:
if len(v) > 0 {
if s, ok := v[0].(string); ok && s != "" {
host = s
}
}
}
}
return fmt.Sprintf("%s:%d", host, port)
}
func isLegacyApiRoutingRule(rule map[string]interface{}) bool {
outboundTag, _ := rule["outboundTag"].(string)
if outboundTag != "api" {
return false
}
val, ok := rule["inboundTag"]
if !ok {
return false
}
switch v := val.(type) {
case []interface{}:
return len(v) == 1 && v[0] == "api"
case []string:
return len(v) == 1 && v[0] == "api"
default:
return false
}
}
func migrateLegacyApiConfig(cfg map[string]interface{}) bool {
api, ok := cfg["api"].(map[string]interface{})
if !ok || !apiListenMissing(api) {
return false
}
var listenAddr string
inbounds, _ := cfg["inbounds"].([]interface{})
apiInboundIndex := -1
var apiInbound map[string]interface{}
for i, item := range inbounds {
inbound, ok := item.(map[string]interface{})
if !ok {
continue
}
tag, _ := inbound["tag"].(string)
protocol, _ := inbound["protocol"].(string)
if tag == "api" && protocol == "dokodemo-door" {
apiInboundIndex = i
apiInbound = inbound
break
}
}
if apiInboundIndex >= 0 {
listenAddr = inboundListenAddress(apiInbound)
if listenAddr == "" {
listenAddr = defaultApiListen()
}
cfg["inbounds"] = removeIndex(inbounds, apiInboundIndex)
if routing, ok := cfg["routing"].(map[string]interface{}); ok {
if rawRules, ok := routing["rules"].([]interface{}); ok {
for i, item := range rawRules {
rule, ok := item.(map[string]interface{})
if !ok {
continue
}
if isLegacyApiRoutingRule(rule) {
routing["rules"] = removeIndex(rawRules, i)
cfg["routing"] = routing
break
}
}
}
}
} else {
listenAddr = defaultApiListen()
}
api["listen"] = listenAddr
cfg["api"] = api
return true
}
func routingRuleFromMap(raw map[string]interface{}, index int) *model.RoutingRule {
tag, _ := raw["ruleTag"].(string)
delete(raw, "ruleTag")
if tag == "" {
tag = fmt.Sprintf("migrated-rule-%d", index)
}
b, _ := json.Marshal(raw)
return &model.RoutingRule{
Tag: tag,
Sort: index,
RawJson: string(b),
}
}

View file

@ -0,0 +1,190 @@
package migrations
import (
"encoding/json"
"github.com/alireza0/x-ui/database/model"
"github.com/alireza0/x-ui/logger"
"github.com/alireza0/x-ui/xray"
"gorm.io/gorm"
)
func migrateV004Inbound(db *gorm.DB) error {
if err := migrationInboundRequirements(db); err != nil {
return err
}
migrationRemoveOrphanedTraffics(db)
return nil
}
func migrationInboundRequirements(db *gorm.DB) error {
tx := db.Begin()
var err error
defer func() {
if err == nil {
tx.Commit()
if dbErr := db.Exec(`VACUUM "main"`).Error; dbErr != nil {
logger.Warningf("VACUUM failed: %v", dbErr)
}
} else {
tx.Rollback()
}
}()
var inbounds []*model.Inbound
err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
return err
}
for inboundIndex := range inbounds {
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbounds[inboundIndex].Settings), &settings)
clients, ok := settings["clients"].([]interface{})
if ok {
var newClients []interface{}
for clientIndex := range clients {
c := clients[clientIndex].(map[string]interface{})
if _, ok := c["email"]; !ok {
c["email"] = ""
}
if _, ok := c["flow"]; ok {
if c["flow"] == "xtls-rprx-direct" {
c["flow"] = ""
}
}
newClients = append(newClients, interface{}(c))
}
settings["clients"] = newClients
modifiedSettings, marshalErr := json.MarshalIndent(settings, "", " ")
if marshalErr != nil {
err = marshalErr
return err
}
inbounds[inboundIndex].Settings = string(modifiedSettings)
}
modelClients := parseInboundClients(inbounds[inboundIndex].Settings)
for _, modelClient := range modelClients {
if len(modelClient.Email) > 0 {
var count int64
tx.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count)
if count == 0 {
if addErr := addClientStat(tx, inbounds[inboundIndex].Id, &modelClient); addErr != nil {
err = addErr
return err
}
}
}
}
}
if saveErr := tx.Save(inbounds).Error; saveErr != nil {
err = saveErr
return err
}
tx.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
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 nil
}
for _, ep := range externalProxy {
var reverses interface{}
var stream map[string]interface{}
json.Unmarshal(ep.StreamSettings, &stream)
if tlsSettings, ok := stream["tlsSettings"].(map[string]interface{}); ok {
if settings, ok := tlsSettings["settings"].(map[string]interface{}); ok {
if domains, ok := settings["domains"].([]interface{}); ok {
for _, domain := range domains {
if domainMap, ok := domain.(map[string]interface{}); ok {
domainMap["forceTls"] = "same"
domainMap["port"] = ep.Port
domainMap["dest"] = domainMap["domain"].(string)
delete(domainMap, "domain")
}
}
}
reverses = settings["domains"]
delete(settings, "domains")
}
}
stream["externalProxy"] = reverses
newStream, _ := json.MarshalIndent(stream, " ", " ")
tx.Model(model.Inbound{}).Where("id = ?", ep.Id).Update("stream_settings", newStream)
}
return nil
}
func migrationRemoveOrphanedTraffics(db *gorm.DB) {
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
)
`)
}
func parseInboundClients(settingsJSON string) []model.Client {
settings := map[string]interface{}{}
if err := json.Unmarshal([]byte(settingsJSON), &settings); err != nil {
return nil
}
raw, ok := settings["clients"]
if !ok || raw == nil {
return nil
}
switch v := raw.(type) {
case []interface{}:
b, err := json.Marshal(v)
if err != nil {
return nil
}
var clients []model.Client
if json.Unmarshal(b, &clients) != nil {
return nil
}
return clients
case string:
if v == "" {
return nil
}
var clients []model.Client
if json.Unmarshal([]byte(v), &clients) != nil {
return nil
}
return clients
default:
return nil
}
}
func addClientStat(tx *gorm.DB, inboundId int, client *model.Client) error {
clientTraffic := xray.ClientTraffic{
InboundId: inboundId,
Email: client.Email,
Total: client.TotalGB,
ExpiryTime: client.ExpiryTime,
Enable: client.Enable,
Up: 0,
Down: 0,
Reset: client.Reset,
}
return tx.Create(&clientTraffic).Error
}

11
main.go
View file

@ -47,11 +47,6 @@ func runWebServer() {
log.Fatal(err)
}
outboundService := service.OutboundService{}
outboundService.MigrateDB()
routingRuleService := service.RoutingRuleService{}
routingRuleService.MigrateDB()
var server *web.Server
server = web.NewServer()
@ -368,16 +363,10 @@ func getPanelURI() {
}
func migrateDb() {
inboundService := service.InboundService{}
outboundService := service.OutboundService{}
err := database.InitDB(config.GetDBPath())
if err != nil {
log.Fatal(err)
}
fmt.Println("Start migrating database...")
inboundService.MigrateDB()
outboundService.MigrateDB()
fmt.Println("Migration done!")
}

View file

@ -1143,18 +1143,6 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
return needRestart, count, err
}
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
)
`)
}
func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model.Client) error {
clientTraffic := xray.ClientTraffic{}
clientTraffic.InboundId = inboundId
@ -1488,125 +1476,6 @@ func (s *InboundService) GetClientReverseTags() (string, error) {
return string(result), nil
}
func (s *InboundService) MigrationRequirements() {
db := database.GetDB()
tx := db.Begin()
var err error
defer func() {
if err == nil {
tx.Commit()
if dbErr := db.Exec(`VACUUM "main"`).Error; dbErr != nil {
logger.Warningf("VACUUM failed: %v", dbErr)
}
} else {
tx.Rollback()
}
}()
// Fix inbounds based problems
var inbounds []*model.Inbound
err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
return
}
for inbound_index := range inbounds {
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, ok := settings["clients"].([]interface{})
if ok {
// Fix Client configuration problems
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
// Add email='' if it is not exists
if _, ok := c["email"]; !ok {
c["email"] = ""
}
// Remove "flow": "xtls-rprx-direct"
if _, ok := c["flow"]; ok {
if c["flow"] == "xtls-rprx-direct" {
c["flow"] = ""
}
}
newClients = append(newClients, interface{}(c))
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return
}
inbounds[inbound_index].Settings = string(modifiedSettings)
}
// Add client traffic row for all clients which has email
modelClients, err := s.GetClients(inbounds[inbound_index])
if err != nil {
return
}
for _, modelClient := range modelClients {
if len(modelClient.Email) > 0 {
var count int64
tx.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count)
if count == 0 {
s.AddClientStat(tx, inbounds[inbound_index].Id, &modelClient)
}
}
}
}
tx.Save(inbounds)
// Remove orphaned traffics
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 {
var reverses interface{}
var stream map[string]interface{}
json.Unmarshal(ep.StreamSettings, &stream)
if tlsSettings, ok := stream["tlsSettings"].(map[string]interface{}); ok {
if settings, ok := tlsSettings["settings"].(map[string]interface{}); ok {
if domains, ok := settings["domains"].([]interface{}); ok {
for _, domain := range domains {
if domainMap, ok := domain.(map[string]interface{}); ok {
domainMap["forceTls"] = "same"
domainMap["port"] = ep.Port
domainMap["dest"] = domainMap["domain"].(string)
delete(domainMap, "domain")
}
}
}
reverses = settings["domains"]
delete(settings, "domains")
}
}
stream["externalProxy"] = reverses
newStream, _ := json.MarshalIndent(stream, " ", " ")
tx.Model(model.Inbound{}).Where("id = ?", ep.Id).Update("stream_settings", newStream)
}
}
func (s *InboundService) MigrateDB() {
s.MigrationRequirements()
s.MigrationRemoveOrphanedTraffics()
}
func (s *InboundService) GetOnlineClients() []string {
return p.GetOnlineClients()
}

View file

@ -294,131 +294,3 @@ func (s *OutboundService) GetOnlineOutbounds() []string {
}
return p.GetOnlineOutbounds()
}
func outboundFromMap(raw map[string]interface{}, sort int) *model.Outbound {
o := &model.Outbound{Sort: sort}
if v, ok := raw["sendThrough"].(string); ok {
o.SendThrough = v
}
if v, ok := raw["protocol"].(string); ok {
o.Protocol = v
}
if v, ok := raw["tag"].(string); ok {
o.Tag = v
}
if v, ok := raw["targetStrategy"].(string); ok {
o.TargetStrategy = v
}
if v, ok := raw["settings"]; ok && v != nil {
b, _ := json.Marshal(v)
o.Settings = string(b)
}
if v, ok := raw["streamSettings"]; ok && v != nil {
b, _ := json.Marshal(v)
o.StreamSettings = string(b)
}
if v, ok := raw["proxySettings"]; ok && v != nil {
b, _ := json.Marshal(v)
o.ProxySettings = string(b)
}
if v, ok := raw["mux"]; ok && v != nil {
b, _ := json.Marshal(v)
o.Mux = string(b)
}
return o
}
func defaultOutbounds() []*model.Outbound {
return []*model.Outbound{
{
Sort: 0,
Protocol: "freedom",
Tag: "direct",
Settings: `{"domainStrategy":"UseIP","noises":[],"redirect":""}`,
},
{
Sort: 1,
Protocol: "blackhole",
Tag: "blocked",
Settings: `{}`,
},
}
}
func (s *OutboundService) initDefaultOutbounds() {
db := database.GetDB()
for _, outbound := range defaultOutbounds() {
if err := db.Create(outbound).Error; err != nil {
logger.Warning("outbound init: create default failed:", err)
}
}
logger.Info("Initialized default outbound(s): direct, blocked")
}
func (s *OutboundService) migrateOutboundsFromTemplate() {
db := database.GetDB()
templateConfig, err := s.settingService.GetXrayConfigTemplate()
if err != nil {
logger.Warning("outbound migration: get xray template failed:", err)
return
}
var cfg map[string]interface{}
if err := json.Unmarshal([]byte(templateConfig), &cfg); err != nil {
logger.Warning("outbound migration: parse template failed:", err)
return
}
rawOutbounds, ok := cfg["outbounds"].([]interface{})
if !ok || len(rawOutbounds) == 0 {
return
}
migrated := 0
for i, item := range rawOutbounds {
raw, ok := item.(map[string]interface{})
if !ok {
continue
}
outbound := outboundFromMap(raw, i)
if outbound.Tag == "" || outbound.Protocol == "" {
continue
}
if err := db.Create(outbound).Error; err != nil {
logger.Warning("outbound migration: create failed:", err)
continue
}
migrated++
}
if migrated == 0 {
return
}
cfg["outbounds"] = []interface{}{}
newTemplate, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
logger.Warning("outbound migration: marshal template failed:", err)
return
}
if err := s.settingService.saveSetting("xrayTemplateConfig", string(newTemplate)); err != nil {
logger.Warning("outbound migration: save template failed:", err)
return
}
logger.Info("Migrated", migrated, "outbound(s) from xray settings to database")
}
func (s *OutboundService) MigrateDB() {
db := database.GetDB()
var count int64
db.Model(&model.Outbound{}).Count(&count)
if count > 0 {
return
}
s.migrateOutboundsFromTemplate()
db.Model(&model.Outbound{}).Count(&count)
if count == 0 {
s.initDefaultOutbounds()
}
}

View file

@ -353,216 +353,6 @@ func (s *RoutingRuleService) ReplaceBalancerTag(oldTag, newTag string) error {
return nil
}
func apiListenMissing(api map[string]interface{}) bool {
_, ok := api["listen"]
return !ok
}
func defaultApiListen() string {
var cfg map[string]interface{}
if err := json.Unmarshal([]byte(xrayTemplateConfig), &cfg); err != nil {
return "127.0.0.1:62789"
}
api, ok := cfg["api"].(map[string]interface{})
if !ok {
return "127.0.0.1:62789"
}
s, _ := api["listen"].(string)
if s == "" {
return "127.0.0.1:62789"
}
return s
}
func inboundListenAddress(inbound map[string]interface{}) string {
port := 0
switch p := inbound["port"].(type) {
case float64:
port = int(p)
case int:
port = p
case int64:
port = int(p)
}
if port == 0 {
return ""
}
host := "127.0.0.1"
if listen, ok := inbound["listen"]; ok {
switch v := listen.(type) {
case string:
if v != "" {
host = v
}
case []interface{}:
if len(v) > 0 {
if s, ok := v[0].(string); ok && s != "" {
host = s
}
}
}
}
return fmt.Sprintf("%s:%d", host, port)
}
func isLegacyApiRoutingRule(rule map[string]interface{}) bool {
outboundTag, _ := rule["outboundTag"].(string)
if outboundTag != "api" {
return false
}
val, ok := rule["inboundTag"]
if !ok {
return false
}
switch v := val.(type) {
case []interface{}:
return len(v) == 1 && v[0] == "api"
case []string:
return len(v) == 1 && v[0] == "api"
default:
return false
}
}
func migrateLegacyApiConfig(cfg map[string]interface{}) bool {
api, ok := cfg["api"].(map[string]interface{})
if !ok || !apiListenMissing(api) {
return false
}
var listenAddr string
inbounds, _ := cfg["inbounds"].([]interface{})
apiInboundIndex := -1
var apiInbound map[string]interface{}
for i, item := range inbounds {
inbound, ok := item.(map[string]interface{})
if !ok {
continue
}
tag, _ := inbound["tag"].(string)
protocol, _ := inbound["protocol"].(string)
if tag == "api" && protocol == "dokodemo-door" {
apiInboundIndex = i
apiInbound = inbound
break
}
}
if apiInboundIndex >= 0 {
listenAddr = inboundListenAddress(apiInbound)
if listenAddr == "" {
listenAddr = defaultApiListen()
}
cfg["inbounds"] = RemoveIndex(inbounds, apiInboundIndex)
if routing, ok := cfg["routing"].(map[string]interface{}); ok {
if rawRules, ok := routing["rules"].([]interface{}); ok {
for i, item := range rawRules {
rule, ok := item.(map[string]interface{})
if !ok {
continue
}
if isLegacyApiRoutingRule(rule) {
routing["rules"] = RemoveIndex(rawRules, i)
cfg["routing"] = routing
break
}
}
}
}
} else {
listenAddr = defaultApiListen()
}
api["listen"] = listenAddr
cfg["api"] = api
return true
}
func routingRuleFromMap(raw map[string]interface{}, index int) *model.RoutingRule {
tag, _ := raw["ruleTag"].(string)
delete(raw, "ruleTag")
if tag == "" {
tag = fmt.Sprintf("migrated-rule-%d", index)
}
b, _ := json.Marshal(raw)
return &model.RoutingRule{
Tag: tag,
Sort: index,
RawJson: string(b),
}
}
func (s *RoutingRuleService) MigrateDB() {
db := database.GetDB()
var count int64
db.Model(&model.RoutingRule{}).Count(&count)
templateConfig, err := s.settingService.GetXrayConfigTemplate()
if err != nil {
logger.Warning("routing rule migration: get xray template failed:", err)
return
}
var cfg map[string]interface{}
if err := json.Unmarshal([]byte(templateConfig), &cfg); err != nil {
logger.Warning("routing rule migration: parse template failed:", err)
return
}
if migrateLegacyApiConfig(cfg) {
newTemplate, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
logger.Warning("routing rule migration: marshal legacy api config failed:", err)
return
}
if err := s.settingService.saveSetting("xrayTemplateConfig", string(newTemplate)); err != nil {
logger.Warning("routing rule migration: save legacy api config failed:", err)
return
}
logger.Info("Migrated legacy api inbound to api.listen in xray settings")
}
if count > 0 {
return
}
routing, ok := cfg["routing"].(map[string]interface{})
if !ok {
return
}
rawRules, ok := routing["rules"].([]interface{})
if !ok || len(rawRules) == 0 {
return
}
for i, item := range rawRules {
raw, ok := item.(map[string]interface{})
if !ok {
continue
}
rule := routingRuleFromMap(raw, i)
if err := db.Create(rule).Error; err != nil {
logger.Warning("routing rule migration: create failed:", err)
}
}
routing["rules"] = []interface{}{}
cfg["routing"] = routing
newTemplate, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
logger.Warning("routing rule migration: marshal template failed:", err)
return
}
if err := s.settingService.saveSetting("xrayTemplateConfig", string(newTemplate)); err != nil {
logger.Warning("routing rule migration: save template failed:", err)
return
}
logger.Info("Migrated", len(rawRules), "routing rule(s) from xray settings to database")
}
func (s *RoutingRuleService) BuildRulesArray() ([]interface{}, error) {
rules, err := s.GetAllRules()
if err != nil {

View file

@ -581,10 +581,6 @@ func (s *ServerService) ImportDB(file multipart.File) error {
}
return common.NewErrorf("Error migrating db: %v", err)
}
s.inboundService.MigrateDB()
s.outboundService.MigrateDB()
s.routingRuleService.MigrateDB()
// Start Xray
err = s.RestartXrayService()
if err != nil {

View file

@ -1,7 +1,6 @@
package service
import (
_ "embed"
"encoding/json"
"errors"
"fmt"
@ -10,6 +9,7 @@ import (
"strings"
"time"
"github.com/alireza0/x-ui/config"
"github.com/alireza0/x-ui/database"
"github.com/alireza0/x-ui/database/model"
"github.com/alireza0/x-ui/logger"
@ -19,11 +19,8 @@ import (
"github.com/alireza0/x-ui/web/entity"
)
//go:embed config.json
var xrayTemplateConfig string
var defaultValueMap = map[string]string{
"xrayTemplateConfig": xrayTemplateConfig,
"xrayTemplateConfig": config.GetDefaultXrayTemplate(),
"webListen": "",
"webDomain": "",
"webPort": "54321",
@ -472,7 +469,7 @@ func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
func (s *SettingService) GetDefaultXrayConfig() (interface{}, error) {
var jsonData interface{}
err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData)
err := json.Unmarshal([]byte(config.GetDefaultXrayTemplate()), &jsonData)
if err != nil {
return nil, err
}