Skip to content

Commit 52ec1d4

Browse files
feat(panel): add geo files updater with status/version and localized index UI
1 parent 35b3a50 commit 52ec1d4

16 files changed

+335
-0
lines changed

internal/web/controller/server.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
5151
g.POST("/restartXrayService", a.restartXrayService)
5252
g.POST("/installPanel", a.installPanel)
5353
g.POST("/installXray/:version", a.installXray)
54+
g.POST("/updateGeoFiles", a.updateGeoFiles)
5455
g.POST("/logs/:count", a.getLogs)
5556
g.POST("/xraylogs/:count", a.getXrayLogs)
5657
g.POST("/importDB", a.importDB)
@@ -114,6 +115,11 @@ func (a *ServerController) installXray(c *gin.Context) {
114115
jsonMsg(c, I18nWeb(c, "install")+" xray", err)
115116
}
116117

118+
func (a *ServerController) updateGeoFiles(c *gin.Context) {
119+
err := a.serverService.UpdateGeoFiles()
120+
jsonMsg(c, "update geo files", err)
121+
}
122+
117123
func (a *ServerController) stopXrayService(c *gin.Context) {
118124
a.lastGetStatusTime = time.Now()
119125
err := a.serverService.StopXrayService()

internal/web/html/xui/index.html

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@
172172
<a-tag @click="openBackup" color="purple" style="cursor: pointer;">{{ i18n
173173
"pages.index.backup" }}
174174
</a-tag>
175+
<a-tag @click="restartPanel" color="purple" style="cursor: pointer;">{{ i18n
176+
"pages.settings.restartPanel" }}
177+
</a-tag>
175178
</a-card>
176179
</a-col>
177180
<a-col :lg="12" :sm="24">
@@ -316,6 +319,28 @@
316319
</a-row>
317320
</a-card>
318321
</a-col>
322+
<a-col :lg="12" :sm="24">
323+
<a-card hoverable>
324+
<b>{{ i18n "pages.index.geoData" }}:</b>
325+
<a-tag @click="updateGeoFiles" color="purple" style="cursor: pointer;">{{ i18n "pages.index.geoUpdate" }}</a-tag>
326+
<a-tag color="green">
327+
<a-tooltip>
328+
geoip.dat [[ geoVersion(status.geoFiles.geoip) ]]
329+
<template slot="title">
330+
[[ geoFileMeta(status.geoFiles.geoip) ]]
331+
</template>
332+
</a-tooltip>
333+
</a-tag>
334+
<a-tag color="green">
335+
<a-tooltip>
336+
geosite.dat [[ geoVersion(status.geoFiles.geosite) ]]
337+
<template slot="title">
338+
[[ geoFileMeta(status.geoFiles.geosite) ]]
339+
</template>
340+
</a-tooltip>
341+
</a-tag>
342+
</a-card>
343+
</a-col>
319344
</a-row>
320345
</transition>
321346
</a-spin>
@@ -491,6 +516,10 @@
491516
this.netIO = {up: 0, down: 0};
492517
this.netTraffic = {sent: 0, recv: 0};
493518
this.publicIP = {ipv4: 0, ipv6: 0};
519+
this.geoFiles = {
520+
geoip: {exists: false, size: 0, updatedAt: 0, version: ''},
521+
geosite: {exists: false, size: 0, updatedAt: 0, version: ''},
522+
};
494523
this.hostname = 0;
495524
this.swap = new CurTotal(0, 0);
496525
this.tcpCount = 0;
@@ -513,6 +542,7 @@
513542
this.netIO = data.netIO;
514543
this.netTraffic = data.netTraffic;
515544
this.publicIP = data.publicIP;
545+
this.geoFiles = data.geoFiles || this.geoFiles;
516546
this.hostName = data.hostname;
517547
this.swap = new CurTotal(data.swap.current, data.swap.total);
518548
this.tcpCount = data.tcpCount;
@@ -720,6 +750,20 @@
720750
onResize() {
721751
this.isMobile = window.innerWidth <= 768;
722752
},
753+
geoFileMeta(file) {
754+
if (!file || !file.exists) {
755+
return '{{ i18n "pages.index.geoMissing" }}';
756+
}
757+
const updatedAt = file.updatedAt ? new Date(file.updatedAt * 1000).toLocaleString() : '{{ i18n "pages.index.geoUnknown" }}';
758+
const version = file.version ? file.version : '{{ i18n "pages.index.geoUnknown" }}';
759+
return `{{ i18n "pages.index.geoVersion" }}: ${version} | {{ i18n "pages.index.geoSize" }}: ${sizeFormat(file.size)} | {{ i18n "pages.index.geoUpdated" }}: ${updatedAt}`;
760+
},
761+
geoVersion(file) {
762+
if (!file || !file.version) {
763+
return `({{ i18n "pages.index.geoUnknown" }})`;
764+
}
765+
return `(${file.version})`;
766+
},
723767
async getStatus() {
724768
try {
725769
const msg = await HttpUtil.get('/panel/api/server/status');
@@ -784,6 +828,34 @@
784828
location.reload();
785829
}
786830
},
831+
async updateGeoFiles() {
832+
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
833+
const msg = await HttpUtil.post('/panel/api/server/updateGeoFiles');
834+
this.loading(false);
835+
if (!msg.success) {
836+
return;
837+
}
838+
await this.getStatus();
839+
},
840+
restartPanel() {
841+
this.$confirm({
842+
title: '{{ i18n "pages.settings.restartPanel" }}',
843+
content: '{{ i18n "pages.settings.restartPanelDesc" }}',
844+
okText: '{{ i18n "confirm"}}',
845+
class: themeSwitcher.currentTheme,
846+
cancelText: '{{ i18n "cancel"}}',
847+
onOk: async () => {
848+
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
849+
const restartMsg = await HttpUtil.post("/panel/setting/restartPanel");
850+
this.loading(false);
851+
if (restartMsg.success) {
852+
this.loading(true);
853+
await PromiseUtil.sleep(5000);
854+
location.reload();
855+
}
856+
},
857+
});
858+
},
787859
async openLogs() {
788860
logModal.loading = true;
789861
const msg = await HttpUtil.post('/panel/api/server/logs/' + logModal.rows, {

internal/web/service/server.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,20 @@ type Status struct {
9292
IPv4 string `json:"ipv4"`
9393
IPv6 string `json:"ipv6"`
9494
} `json:"publicIP"`
95+
GeoFiles struct {
96+
GeoIP struct {
97+
Exists bool `json:"exists"`
98+
Size uint64 `json:"size"`
99+
UpdatedAt int64 `json:"updatedAt"`
100+
Version string `json:"version"`
101+
} `json:"geoip"`
102+
GeoSite struct {
103+
Exists bool `json:"exists"`
104+
Size uint64 `json:"size"`
105+
UpdatedAt int64 `json:"updatedAt"`
106+
Version string `json:"version"`
107+
} `json:"geosite"`
108+
} `json:"geoFiles"`
95109
AppStats struct {
96110
Threads uint32 `json:"threads"`
97111
Mem uint64 `json:"mem"`
@@ -234,6 +248,27 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
234248

235249
status.PublicIP.IPv4 = getPublicIP("8.8.8.8:80")
236250
status.PublicIP.IPv6 = getPublicIP("[2001:4860:4860::8888]:80")
251+
fillGeoFileStatus := func(path string, dest *struct {
252+
Exists bool `json:"exists"`
253+
Size uint64 `json:"size"`
254+
UpdatedAt int64 `json:"updatedAt"`
255+
Version string `json:"version"`
256+
}) {
257+
info, err := os.Stat(path)
258+
if err != nil {
259+
dest.Exists = false
260+
dest.Size = 0
261+
dest.UpdatedAt = 0
262+
dest.Version = ""
263+
return
264+
}
265+
dest.Exists = true
266+
dest.Size = uint64(info.Size())
267+
dest.UpdatedAt = info.ModTime().Unix()
268+
dest.Version = readGeoFileVersion(path)
269+
}
270+
fillGeoFileStatus(xray.GetGeoipPath(), &status.GeoFiles.GeoIP)
271+
fillGeoFileStatus(xray.GetGeositePath(), &status.GeoFiles.GeoSite)
237272

238273
status.HostName, _ = os.Hostname()
239274

@@ -468,6 +503,137 @@ func (s *ServerService) UpdateXray(version string) error {
468503
return nil
469504
}
470505

506+
func (s *ServerService) UpdateGeoFiles() error {
507+
files := []struct {
508+
url string
509+
path string
510+
name string
511+
}{
512+
{
513+
url: "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat",
514+
path: xray.GetGeoipPath(),
515+
name: "geoip.dat",
516+
},
517+
{
518+
url: "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat",
519+
path: xray.GetGeositePath(),
520+
name: "geosite.dat",
521+
},
522+
}
523+
524+
client := &http.Client{Timeout: 2 * time.Minute}
525+
for _, file := range files {
526+
version, err := getLatestReleaseTag(file.url, file.name)
527+
if err != nil {
528+
logger.Warningf("failed to detect %s version: %v", file.name, err)
529+
version = ""
530+
}
531+
532+
req, err := http.NewRequest(http.MethodGet, file.url, nil)
533+
if err != nil {
534+
return fmt.Errorf("create request for %s: %w", file.name, err)
535+
}
536+
req.Header.Set("User-Agent", "tx-ui")
537+
538+
resp, err := client.Do(req)
539+
if err != nil {
540+
return fmt.Errorf("download %s: %w", file.name, err)
541+
}
542+
if resp.StatusCode != http.StatusOK {
543+
resp.Body.Close()
544+
return fmt.Errorf("download %s: unexpected status %s", file.name, resp.Status)
545+
}
546+
547+
if err := os.MkdirAll(filepath.Dir(file.path), 0o755); err != nil {
548+
resp.Body.Close()
549+
return fmt.Errorf("prepare directory for %s: %w", file.name, err)
550+
}
551+
552+
tmpPath := file.path + ".tmp"
553+
tmpFile, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o644)
554+
if err != nil {
555+
resp.Body.Close()
556+
return fmt.Errorf("create temp file for %s: %w", file.name, err)
557+
}
558+
559+
_, copyErr := io.Copy(tmpFile, resp.Body)
560+
closeErr := tmpFile.Close()
561+
resp.Body.Close()
562+
if copyErr != nil {
563+
_ = os.Remove(tmpPath)
564+
return fmt.Errorf("write %s: %w", file.name, copyErr)
565+
}
566+
if closeErr != nil {
567+
_ = os.Remove(tmpPath)
568+
return fmt.Errorf("close temp file for %s: %w", file.name, closeErr)
569+
}
570+
if err := os.Rename(tmpPath, file.path); err != nil {
571+
_ = os.Remove(tmpPath)
572+
return fmt.Errorf("replace %s: %w", file.name, err)
573+
}
574+
575+
if version != "" {
576+
if err := os.WriteFile(file.path+".version", []byte(version), 0o644); err != nil {
577+
logger.Warningf("failed to write %s version file: %v", file.name, err)
578+
}
579+
}
580+
}
581+
582+
if err := s.xrayService.RestartXray(true); err != nil {
583+
logger.Error("restart xray after geo files update failed:", err)
584+
return err
585+
}
586+
587+
return nil
588+
}
589+
590+
func extractReleaseTag(path string, fileName string) string {
591+
parts := strings.Split(path, "/")
592+
for i := 0; i < len(parts)-2; i++ {
593+
if parts[i] == "download" && parts[i+2] == fileName {
594+
return strings.TrimSpace(parts[i+1])
595+
}
596+
}
597+
return ""
598+
}
599+
600+
func getLatestReleaseTag(assetURL string, assetName string) (string, error) {
601+
noRedirectClient := &http.Client{
602+
Timeout: 20 * time.Second,
603+
CheckRedirect: func(req *http.Request, via []*http.Request) error {
604+
return http.ErrUseLastResponse
605+
},
606+
}
607+
608+
req, err := http.NewRequest(http.MethodGet, assetURL, nil)
609+
if err != nil {
610+
return "", err
611+
}
612+
req.Header.Set("User-Agent", "tx-ui")
613+
614+
resp, err := noRedirectClient.Do(req)
615+
if err != nil {
616+
return "", err
617+
}
618+
defer resp.Body.Close()
619+
620+
location := resp.Header.Get("Location")
621+
if location == "" {
622+
// Some proxies may still follow redirects. Try current request URL path as fallback.
623+
return extractReleaseTag(resp.Request.URL.Path, assetName), nil
624+
}
625+
626+
return extractReleaseTag(location, assetName), nil
627+
}
628+
629+
func readGeoFileVersion(path string) string {
630+
data, err := os.ReadFile(path + ".version")
631+
if err != nil {
632+
return ""
633+
}
634+
return strings.TrimSpace(string(data))
635+
}
636+
471637
func (s *ServerService) UpdatePanel(version string) {
472638
fmt.Println("Starting x-ui installation...")
473639

internal/web/translation/translate.ar_EG.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,13 @@
128128
"upgradeAvail" = "تحديث لوحة جديد متاح"
129129
"installingPanel" = "بدأ تثبيت اللوحة"
130130
"systemHost" = "اسم المضيف"
131+
"geoData" = "بيانات جيو"
132+
"geoUpdate" = "تحديث"
133+
"geoMissing" = "مفقود"
134+
"geoUnknown" = "غير معروف"
135+
"geoVersion" = "الإصدار"
136+
"geoSize" = "الحجم"
137+
"geoUpdated" = "آخر تحديث"
131138

132139
[pages.inbounds]
133140
"title" = "الإدخالات"

internal/web/translation/translate.en_US.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@
121121
"upgradeAvail" = "New panel update is availabe"
122122
"installingPanel" = "Panel started to install"
123123
"systemHost" = "Hostname"
124+
"geoData" = "Geo Data"
125+
"geoUpdate" = "Update"
126+
"geoMissing" = "Missing"
127+
"geoUnknown" = "unknown"
128+
"geoVersion" = "Version"
129+
"geoSize" = "Size"
130+
"geoUpdated" = "Updated"
124131

125132
[pages.inbounds]
126133
"title" = "Inbounds"

internal/web/translation/translate.es_ES.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@
120120
"upgradeAvail" = "Hay una nueva actualización del panel disponible"
121121
"installingPanel" = "El panel comenzó a instalarse"
122122
"systemHost" = "Hostname"
123+
"geoData" = "Datos Geo"
124+
"geoUpdate" = "Actualizar"
125+
"geoMissing" = "Falta"
126+
"geoUnknown" = "desconocido"
127+
"geoVersion" = "Versión"
128+
"geoSize" = "Tamaño"
129+
"geoUpdated" = "Actualizado"
123130

124131
[pages.inbounds]
125132
"title" = "Entradas"

internal/web/translation/translate.fa_IR.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@
121121
"upgradeAvail" = "نسخه جدیدی از پنل موجود است"
122122
"installingPanel" = "نصب پنل شروع شد"
123123
"systemHost" = "میزبان"
124+
"geoData" = "داده‌های جئو"
125+
"geoUpdate" = "به‌روزرسانی"
126+
"geoMissing" = "موجود نیست"
127+
"geoUnknown" = "نامشخص"
128+
"geoVersion" = "نسخه"
129+
"geoSize" = "حجم"
130+
"geoUpdated" = "آخرین به‌روزرسانی"
124131

125132
[pages.inbounds]
126133
"title" = "کاربران"

internal/web/translation/translate.id_ID.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@
120120
"upgradeAvail" = "Pembaruan panel baru tersedia"
121121
"installingPanel" = "Panel mulai diinstal"
122122
"systemHost" = "Hostname"
123+
"geoData" = "Data Geo"
124+
"geoUpdate" = "Perbarui"
125+
"geoMissing" = "Tidak ada"
126+
"geoUnknown" = "tidak diketahui"
127+
"geoVersion" = "Versi"
128+
"geoSize" = "Ukuran"
129+
"geoUpdated" = "Diperbarui"
123130

124131
[pages.inbounds]
125132
"title" = "Masuk"

0 commit comments

Comments
 (0)