feat(postgres): in-panel backup/restore and consistent CLI backend

Two PostgreSQL gaps on the panel:

1. x-ui setting and other CLI subcommands read XUI_DB_TYPE/XUI_DB_DSN from
   the process environment, which systemd injects via EnvironmentFile but a
   plain shell invocation does not. On a PostgreSQL install the CLI silently
   fell back to SQLite, so changes made from the management menu never
   reached the panel's database. Load the systemd EnvironmentFile
   (/etc/default/x-ui and distro equivalents) at startup; godotenv.Load does
   not override existing vars, so it stays a no-op for the managed service.

2. DB backup/restore (panel endpoints and the Telegram bot) only handled the
   SQLite file, so on PostgreSQL Back Up returned a stale/absent x-ui.db and
   Restore silently did nothing. Add pg_dump/pg_restore based backup/restore:
   - GetDb/ImportDB run pg_dump (custom format) / pg_restore, passing
     credentials via the PG* environment instead of argv.
   - getDb downloads x-ui.dump on Postgres, x-ui.db on SQLite.
   - Telegram backup sends the matching file via GetDb.
   - BackupModal shows a Postgres note and accepts .dump; the dist page
     injects window.X_UI_DB_TYPE; new strings translated for all locales.
   - install.sh installs postgresql-client for the external-DSN path and
     points the user to in-panel Backup & Restore.

Closes #4658
This commit is contained in:
MHSanaei
2026-05-31 17:53:34 +02:00
parent a2f20f85f3
commit cc34dc381c
24 changed files with 311 additions and 29 deletions
+10 -13
View File
@@ -3533,35 +3533,32 @@ func (t *Tgbot) sendBackup(chatId int64) {
output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
t.SendMsgToTgbot(chatId, output)
// Update by manually trigger a checkpoint operation
err := database.Checkpoint()
if err != nil {
logger.Error("Error in trigger a checkpoint operation: ", err)
}
// Send database backup
file, err := os.Open(config.GetDBPath())
// Send database backup (SQLite file, or a pg_dump archive on PostgreSQL)
dbData, err := t.serverService.GetDb()
if err == nil {
defer file.Close()
dbFilename := "x-ui.db"
if database.IsPostgres() {
dbFilename = "x-ui.dump"
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
document := tu.Document(
tu.ID(chatId),
tu.File(file),
tu.FileFromBytes(dbData, dbFilename),
)
_, err = bot.SendDocument(ctx, document)
cancel()
if err != nil {
logger.Error("Error in uploading backup: ", err)
}
} else {
logger.Error("Error in opening db file for backup: ", err)
logger.Error("Error in getting db backup: ", err)
}
// Small delay between file sends
time.Sleep(500 * time.Millisecond)
// Send config.json backup
file, err = os.Open(xray.GetConfigPath())
file, err := os.Open(xray.GetConfigPath())
if err == nil {
defer file.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)