mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-20 10:00:58 +00:00
feat(server): keep this machine's own settings when importing a database (#6227)
* feat(server): keep this machine's own settings when importing a database Import replaces the database wholesale, so the uploaded file's listen addresses, ports, base path, certificate paths and node identity land on the destination. Moving a configuration to a new host therefore leaves the panel answering on an address it does not own, presenting certificates it does not have, and claiming the source machine's identity towards its nodes. Capture the host-bound settings before the swap and write them back once the imported database opens. Everything else — inbounds, clients, templates, the rest of the settings — still comes from the file. A checkbox controls it, defaulting to keeping this machine's values; clearing it restores the old behaviour for anyone deliberately cloning a host. * fix(server): drop imported host settings this machine never had, and cover Postgres Two gaps in the previous commit. The snapshot only recorded rows that existed, so a key with no row here — the default for every certificate path, both listen addresses and all the node mTLS material — kept the imported value: exactly the case the change is meant to fix. The snapshot now records which keys were absent and deletes the imported row for them, letting the default apply again. The PostgreSQL path took the flag and ignored it, so a dump restore still adopted the source machine's settings. It now captures and restores the same way the SQLite path does. * chore: drop the accidentally committed dist build stub internal/web/dist/.gitkeep is what make dist-stub creates locally. Committing it changes fresh-clone behaviour for everyone: today a bare go build fails loudly on //go:embed all:dist, which is the documented signal to run the stub target; with the file present the build succeeds and the panel serves an empty dist instead. --------- Co-authored-by: n0ctal <n0ctal@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -30,6 +30,7 @@ import (
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/config"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/sys"
|
||||
@@ -1434,9 +1435,81 @@ func (s *ServerService) GetMigration() ([]byte, string, error) {
|
||||
return data, "x-ui.dump", nil
|
||||
}
|
||||
|
||||
func (s *ServerService) ImportDB(file multipart.File) error {
|
||||
// hostBoundSettingKeys are the settings that describe *this* machine rather
|
||||
// than the configuration being carried: where the panel and the subscription
|
||||
// service listen, the certificates they present, and the identity this panel
|
||||
// uses towards its nodes. An import that overwrites them leaves the
|
||||
// destination unreachable on its own address, or impersonating the source.
|
||||
var hostBoundSettingKeys = []string{
|
||||
"webListen", "webDomain", "webPort", "webCertFile", "webKeyFile", "webBasePath",
|
||||
"subListen", "subDomain", "subPort", "subCertFile", "subKeyFile", "subURI", "subJsonURI",
|
||||
"secret", "panelGuid",
|
||||
"nodeMtlsCaCertPem", "nodeMtlsCaKeyPem", "nodeMtlsClientCertPem",
|
||||
"nodeMtlsClientKeyPem", "nodeMtlsClientCertSha256", "nodeMtlsClientCAPem",
|
||||
}
|
||||
|
||||
// hostBoundSnapshot records this machine's values, and just as importantly
|
||||
// which keys it had no row for: an absent row means the built-in default is in
|
||||
// force, and leaving the imported row in place would silently adopt the source
|
||||
// machine's certificate path or listen address.
|
||||
type hostBoundSnapshot struct {
|
||||
values map[string]string
|
||||
present map[string]struct{}
|
||||
taken bool
|
||||
}
|
||||
|
||||
func captureHostBoundSettings() hostBoundSnapshot {
|
||||
db := database.GetDB()
|
||||
if db == nil {
|
||||
return hostBoundSnapshot{}
|
||||
}
|
||||
var rows []model.Setting
|
||||
if err := db.Model(&model.Setting{}).Where("key IN ?", hostBoundSettingKeys).Find(&rows).Error; err != nil {
|
||||
logger.Warningf("Import: could not read this machine's settings, they will come from the uploaded file: %v", err)
|
||||
return hostBoundSnapshot{}
|
||||
}
|
||||
snap := hostBoundSnapshot{
|
||||
values: make(map[string]string, len(rows)),
|
||||
present: make(map[string]struct{}, len(rows)),
|
||||
taken: true,
|
||||
}
|
||||
for _, row := range rows {
|
||||
snap.values[row.Key] = row.Value
|
||||
snap.present[row.Key] = struct{}{}
|
||||
}
|
||||
return snap
|
||||
}
|
||||
|
||||
func restoreHostBoundSettings(snap hostBoundSnapshot) {
|
||||
if !snap.taken {
|
||||
return
|
||||
}
|
||||
db := database.GetDB()
|
||||
if db == nil {
|
||||
return
|
||||
}
|
||||
for _, key := range hostBoundSettingKeys {
|
||||
if _, had := snap.present[key]; !had {
|
||||
// No row here before the import, so the default applied. Drop the
|
||||
// imported row rather than inherit the source machine's value.
|
||||
if err := db.Where("key = ?", key).Delete(&model.Setting{}).Error; err != nil {
|
||||
logger.Warningf("Import: could not drop imported setting %q: %v", key, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// The imported row may or may not exist; settings are key-value, so an
|
||||
// upsert keyed on the name is the only safe write here.
|
||||
if err := db.Where(model.Setting{Key: key}).
|
||||
Assign(model.Setting{Value: snap.values[key]}).
|
||||
FirstOrCreate(&model.Setting{}).Error; err != nil {
|
||||
logger.Warningf("Import: could not restore setting %q for this machine: %v", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerService) ImportDB(file multipart.File, keepHostSettings bool) error {
|
||||
if database.IsPostgres() {
|
||||
return s.importPostgresDB(file)
|
||||
return s.importPostgresDB(file, keepHostSettings)
|
||||
}
|
||||
kind, err := sniffUploadKind(file)
|
||||
if err != nil {
|
||||
@@ -1488,6 +1561,11 @@ func (s *ServerService) ImportDB(file multipart.File) error {
|
||||
logger.Warningf("Failed to stop Xray before DB import: %v", errStop)
|
||||
}
|
||||
|
||||
var keptSettings hostBoundSnapshot
|
||||
if keepHostSettings {
|
||||
keptSettings = captureHostBoundSettings()
|
||||
}
|
||||
|
||||
if errClose := database.CloseDB(); errClose != nil {
|
||||
logger.Warningf("Failed to close existing DB before replacement: %v", errClose)
|
||||
}
|
||||
@@ -1543,6 +1621,8 @@ func (s *ServerService) ImportDB(file multipart.File) error {
|
||||
}
|
||||
dbReopened = true
|
||||
|
||||
restoreHostBoundSettings(keptSettings)
|
||||
|
||||
s.inboundService.MigrateDB()
|
||||
|
||||
xrayStopped = false
|
||||
@@ -1697,14 +1777,14 @@ func sniffUploadKind(file multipart.File) (int, error) {
|
||||
return sniffImportKind(header[:n]), nil
|
||||
}
|
||||
|
||||
func (s *ServerService) importPostgresDB(file multipart.File) error {
|
||||
func (s *ServerService) importPostgresDB(file multipart.File, keepHostSettings bool) error {
|
||||
kind, err := sniffUploadKind(file)
|
||||
if err != nil {
|
||||
return common.NewErrorf("Error reading uploaded file: %v", err)
|
||||
}
|
||||
switch kind {
|
||||
case importKindPgDump:
|
||||
return s.restorePostgresDump(file)
|
||||
return s.restorePostgresDump(file, keepHostSettings)
|
||||
case importKindSQLiteDB:
|
||||
return s.migrateSQLiteIntoPostgres(file, false)
|
||||
case importKindSQLiteDump:
|
||||
@@ -1714,7 +1794,7 @@ func (s *ServerService) importPostgresDB(file multipart.File) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerService) restorePostgresDump(file multipart.File) error {
|
||||
func (s *ServerService) restorePostgresDump(file multipart.File, keepHostSettings bool) error {
|
||||
bin, err := exec.LookPath("pg_restore")
|
||||
if err != nil {
|
||||
return common.NewError("pg_restore not found on the server; install the postgresql-client package to restore a PostgreSQL database")
|
||||
@@ -1754,6 +1834,11 @@ func (s *ServerService) restorePostgresDump(file multipart.File) error {
|
||||
logger.Warningf("Failed to stop Xray before DB restore: %v", errStop)
|
||||
}
|
||||
|
||||
var keptSettings hostBoundSnapshot
|
||||
if keepHostSettings {
|
||||
keptSettings = captureHostBoundSettings()
|
||||
}
|
||||
|
||||
if errClose := database.CloseDB(); errClose != nil {
|
||||
logger.Warningf("Failed to close existing DB before restore: %v", errClose)
|
||||
}
|
||||
@@ -1770,6 +1855,8 @@ func (s *ServerService) restorePostgresDump(file multipart.File) error {
|
||||
if errInit := database.InitDB(config.GetDBPath()); errInit != nil {
|
||||
return common.NewErrorf("Restore finished but reopening the database failed: %v", errInit)
|
||||
}
|
||||
restoreHostBoundSettings(keptSettings)
|
||||
|
||||
s.inboundService.MigrateDB()
|
||||
|
||||
if runErr != nil {
|
||||
|
||||
Reference in New Issue
Block a user