mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
feat: track and display geofile version from GitHub release tag to v1.8.1.1
This commit is contained in:
parent
6e04e6d247
commit
b4c42bb89f
5 changed files with 157 additions and 13 deletions
|
|
@ -1 +1 @@
|
||||||
v1.8.1.0
|
v1.8.1.1
|
||||||
|
|
|
||||||
39
docs/Tasktracking/2026-04-27-add-geofile-version-tracking.md
Normal file
39
docs/Tasktracking/2026-04-27-add-geofile-version-tracking.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Task Record
|
||||||
|
|
||||||
|
Date: 2026-04-27
|
||||||
|
Related Module: web/service/server, web/controller/server, web/html/index
|
||||||
|
Change Type: Add
|
||||||
|
|
||||||
|
## Background
|
||||||
|
When downloading geoip.dat and geosite.dat from GitHub releases, the version information (GitHub release tag like `202604262232`) was not captured or displayed. The user wanted to track and show the version of geofiles in the UI.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
- `web/service/server.go`:
|
||||||
|
- Changed `downloadFile` closure to return the captured version string alongside the error
|
||||||
|
- Modified `http.Client` to use `CheckRedirect` callback that extracts the release tag from the 302 redirect URL path (format: `/releases/download/{version}/{filename}`)
|
||||||
|
- Added `GeofileVersion` struct and `GeofileVersions` map type for version metadata storage
|
||||||
|
- Added `loadGeofileVersions()` and `saveGeofileVersions()` for reading/writing `geofile_versions.json` in the bin folder
|
||||||
|
- Added `GetGeofileVersions()` public method for API access
|
||||||
|
- `web/controller/server.go`:
|
||||||
|
- Added `GET /getGeofileVersions` endpoint returning version metadata
|
||||||
|
- `web/html/index.html`:
|
||||||
|
- Added `geofileVersions` to Vue data
|
||||||
|
- Added `loadGeofileVersions()` method, called when the Xray version modal opens
|
||||||
|
- Geofiles panel now displays version string (e.g. `202604262232`) next to each file name
|
||||||
|
- Added CSS classes for version text in light/dark themes
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
- New file: `bin/geofile_versions.json` stores version metadata per geofile
|
||||||
|
- New API: `GET /panel/api/server/getGeofileVersions`
|
||||||
|
- No database schema changes
|
||||||
|
- Xray binary filename expectations unchanged (files still saved as `geoip.dat`/`geosite.dat`)
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- `gofmt -l -w .` passed
|
||||||
|
- `go vet ./...` passed
|
||||||
|
- Tested redirect URL parsing logic: path `/releases/download/202604262232/geoip.dat` correctly extracts `202604262232`
|
||||||
|
- Confirmed `http.Client` with `CheckRedirect` does not interfere with `If-Modified-Since`/`Last-Modified` caching
|
||||||
|
|
||||||
|
## Risks And Follow-Up
|
||||||
|
- Version extraction depends on GitHub's redirect URL format; if GitHub changes the URL structure, version will be empty (graceful degradation — shows `-` in UI)
|
||||||
|
- Worker nodes: version metadata is written locally on each node after their own download via `syncGeoIfNeeded()`, so each worker has its own `geofile_versions.json`
|
||||||
|
|
@ -52,6 +52,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||||
g.GET("/getNewmldsa65", a.getNewmldsa65)
|
g.GET("/getNewmldsa65", a.getNewmldsa65)
|
||||||
g.GET("/getNewmlkem768", a.getNewmlkem768)
|
g.GET("/getNewmlkem768", a.getNewmlkem768)
|
||||||
g.GET("/getNewVlessEnc", a.getNewVlessEnc)
|
g.GET("/getNewVlessEnc", a.getNewVlessEnc)
|
||||||
|
g.GET("/getGeofileVersions", a.getGeofileVersions)
|
||||||
|
|
||||||
g.POST("/stopXrayService", a.stopXrayService)
|
g.POST("/stopXrayService", a.stopXrayService)
|
||||||
g.POST("/restartXrayService", a.restartXrayService)
|
g.POST("/restartXrayService", a.restartXrayService)
|
||||||
|
|
@ -177,6 +178,12 @@ func (a *ServerController) syncUpdateGeofile(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), nil)
|
jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getGeofileVersions returns version metadata for all geofiles.
|
||||||
|
func (a *ServerController) getGeofileVersions(c *gin.Context) {
|
||||||
|
versions := a.serverService.GetGeofileVersions()
|
||||||
|
jsonObj(c, versions, nil)
|
||||||
|
}
|
||||||
|
|
||||||
// stopXrayService stops the Xray service.
|
// stopXrayService stops the Xray service.
|
||||||
func (a *ServerController) stopXrayService(c *gin.Context) {
|
func (a *ServerController) stopXrayService(c *gin.Context) {
|
||||||
err := a.serverService.StopXrayService()
|
err := a.serverService.StopXrayService()
|
||||||
|
|
|
||||||
|
|
@ -332,6 +332,9 @@
|
||||||
<a-list-item class="ant-version-list-item"
|
<a-list-item class="ant-version-list-item"
|
||||||
v-for="file, index in ['geosite.dat', 'geoip.dat']">
|
v-for="file, index in ['geosite.dat', 'geoip.dat']">
|
||||||
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ file ]]</a-tag>
|
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ file ]]</a-tag>
|
||||||
|
<span class="geofile-version-text" v-if="geofileVersions[file]">
|
||||||
|
[[ geofileVersions[file].version || '-' ]]
|
||||||
|
</span>
|
||||||
<a-icon type="reload" @click="updateGeofile(file)" class="mr-8" />
|
<a-icon type="reload" @click="updateGeofile(file)" class="mr-8" />
|
||||||
</a-list-item>
|
</a-list-item>
|
||||||
</a-list>
|
</a-list>
|
||||||
|
|
@ -827,6 +830,16 @@
|
||||||
table td, table th {
|
table td, table th {
|
||||||
padding: 2px 15px;
|
padding: 2px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.geofile-version-text {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .geofile-version-text {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
|
|
@ -902,6 +915,7 @@
|
||||||
logModal,
|
logModal,
|
||||||
xraylogModal,
|
xraylogModal,
|
||||||
backupModal,
|
backupModal,
|
||||||
|
geofileVersions: {},
|
||||||
loadingTip: '{{ i18n "loading"}}',
|
loadingTip: '{{ i18n "loading"}}',
|
||||||
showAlert: false,
|
showAlert: false,
|
||||||
showIp: false,
|
showIp: false,
|
||||||
|
|
@ -974,6 +988,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
versionModal.show(msg.obj);
|
versionModal.show(msg.obj);
|
||||||
|
this.loadGeofileVersions();
|
||||||
},
|
},
|
||||||
switchV2rayVersion(version) {
|
switchV2rayVersion(version) {
|
||||||
this.$confirm({
|
this.$confirm({
|
||||||
|
|
@ -1026,6 +1041,16 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async loadGeofileVersions() {
|
||||||
|
try {
|
||||||
|
const msg = await HttpUtil.get('/panel/api/server/getGeofileVersions');
|
||||||
|
if (msg.success && msg.obj) {
|
||||||
|
this.geofileVersions = msg.obj;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to load geofile versions:", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
async stopXrayService() {
|
async stopXrayService() {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.post('/panel/api/server/stopXrayService');
|
const msg = await HttpUtil.post('/panel/api/server/stopXrayService');
|
||||||
|
|
|
||||||
|
|
@ -1080,11 +1080,11 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFile := func(url, destPath string) error {
|
downloadFile := func(url, destPath string) (version string, err error) {
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err = http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.NewErrorf("Failed to create HTTP request for %s: %v", url, err)
|
return "", common.NewErrorf("Failed to create HTTP request for %s: %v", url, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var localFileModTime time.Time
|
var localFileModTime time.Time
|
||||||
|
|
@ -1095,10 +1095,26 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{}
|
var capturedVersion string
|
||||||
|
client := &http.Client{
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
if len(via) > 5 {
|
||||||
|
return common.NewErrorf("too many redirects for %s", url)
|
||||||
|
}
|
||||||
|
path := req.URL.Path
|
||||||
|
if idx := strings.Index(path, "/releases/download/"); idx != -1 {
|
||||||
|
rest := path[idx+len("/releases/download/"):]
|
||||||
|
slash := strings.Index(rest, "/")
|
||||||
|
if slash > 0 {
|
||||||
|
capturedVersion = rest[:slash]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.NewErrorf("Failed to download Geofile from %s: %v", url, err)
|
return "", common.NewErrorf("Failed to download Geofile from %s: %v", url, err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
|
@ -1126,46 +1142,57 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
|
||||||
// Handle 304 Not Modified
|
// Handle 304 Not Modified
|
||||||
if resp.StatusCode == http.StatusNotModified {
|
if resp.StatusCode == http.StatusNotModified {
|
||||||
updateFileModTime()
|
updateFileModTime()
|
||||||
return nil
|
return capturedVersion, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return common.NewErrorf("Failed to download Geofile from %s: received status code %d", url, resp.StatusCode)
|
return "", common.NewErrorf("Failed to download Geofile from %s: received status code %d", url, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Create(destPath)
|
file, err := os.Create(destPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.NewErrorf("Failed to create Geofile %s: %v", destPath, err)
|
return "", common.NewErrorf("Failed to create Geofile %s: %v", destPath, err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
_, err = io.Copy(file, resp.Body)
|
_, err = io.Copy(file, resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.NewErrorf("Failed to save Geofile %s: %v", destPath, err)
|
return "", common.NewErrorf("Failed to save Geofile %s: %v", destPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFileModTime()
|
updateFileModTime()
|
||||||
return nil
|
return capturedVersion, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var errorMessages []string
|
var errorMessages []string
|
||||||
|
versions := loadGeofileVersions(config.GetBinFolderPath())
|
||||||
|
|
||||||
if fileName == "" {
|
if fileName == "" {
|
||||||
// Download all geofiles
|
// Download all geofiles
|
||||||
for _, entry := range geofileAllowlist {
|
for _, entry := range geofileAllowlist {
|
||||||
destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
|
destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
|
||||||
if err := downloadFile(entry.URL, destPath); err != nil {
|
ver, err := downloadFile(entry.URL, destPath)
|
||||||
|
if err != nil {
|
||||||
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
|
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
|
||||||
|
} else {
|
||||||
|
versions.Update(entry.FileName, ver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
entry := geofileAllowlist[fileName]
|
entry := geofileAllowlist[fileName]
|
||||||
destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
|
destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
|
||||||
if err := downloadFile(entry.URL, destPath); err != nil {
|
ver, err := downloadFile(entry.URL, destPath)
|
||||||
|
if err != nil {
|
||||||
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
|
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
|
||||||
|
} else {
|
||||||
|
versions.Update(entry.FileName, ver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := saveGeofileVersions(config.GetBinFolderPath(), versions); err != nil {
|
||||||
|
logger.Warningf("Failed to save geofile versions: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
err := s.RestartXrayService()
|
err := s.RestartXrayService()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorMessages = append(errorMessages, fmt.Sprintf("Updated Geofile '%s' but Failed to start Xray: %v", fileName, err))
|
errorMessages = append(errorMessages, fmt.Sprintf("Updated Geofile '%s' but Failed to start Xray: %v", fileName, err))
|
||||||
|
|
@ -1178,6 +1205,52 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GeofileVersion holds version metadata for a single geofile.
|
||||||
|
type GeofileVersion struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
UpdatedAt string `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeofileVersions is a map of filename -> version metadata.
|
||||||
|
type GeofileVersions map[string]GeofileVersion
|
||||||
|
|
||||||
|
// Update sets or updates the version entry for a geofile.
|
||||||
|
func (v GeofileVersions) Update(fileName, version string) {
|
||||||
|
entry := v[fileName]
|
||||||
|
if version != "" {
|
||||||
|
entry.Version = version
|
||||||
|
}
|
||||||
|
entry.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
|
||||||
|
v[fileName] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func geofileVersionsPath(binFolder string) string {
|
||||||
|
return filepath.Join(binFolder, "geofile_versions.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadGeofileVersions(binFolder string) GeofileVersions {
|
||||||
|
versions := make(GeofileVersions)
|
||||||
|
data, err := os.ReadFile(geofileVersionsPath(binFolder))
|
||||||
|
if err != nil {
|
||||||
|
return versions
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal(data, &versions)
|
||||||
|
return versions
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveGeofileVersions(binFolder string, versions GeofileVersions) error {
|
||||||
|
data, err := json.MarshalIndent(versions, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(geofileVersionsPath(binFolder), data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGeofileVersions returns the current geofile version metadata.
|
||||||
|
func (s *ServerService) GetGeofileVersions() GeofileVersions {
|
||||||
|
return loadGeofileVersions(config.GetBinFolderPath())
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ServerService) GetNewX25519Cert() (any, error) {
|
func (s *ServerService) GetNewX25519Cert() (any, error) {
|
||||||
// Run the command
|
// Run the command
|
||||||
cmd := exec.Command(xray.GetBinaryPath(), "x25519")
|
cmd := exec.Command(xray.GetBinaryPath(), "x25519")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue