diff --git a/config/version b/config/version index 53cc14cd..22feccc7 100644 --- a/config/version +++ b/config/version @@ -1 +1 @@ -v1.8.1.0 +v1.8.1.1 diff --git a/docs/Tasktracking/2026-04-27-add-geofile-version-tracking.md b/docs/Tasktracking/2026-04-27-add-geofile-version-tracking.md new file mode 100644 index 00000000..d659bdb3 --- /dev/null +++ b/docs/Tasktracking/2026-04-27-add-geofile-version-tracking.md @@ -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` diff --git a/web/controller/server.go b/web/controller/server.go index 84415ad1..34f6a0d2 100644 --- a/web/controller/server.go +++ b/web/controller/server.go @@ -52,6 +52,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) { g.GET("/getNewmldsa65", a.getNewmldsa65) g.GET("/getNewmlkem768", a.getNewmlkem768) g.GET("/getNewVlessEnc", a.getNewVlessEnc) + g.GET("/getGeofileVersions", a.getGeofileVersions) g.POST("/stopXrayService", a.stopXrayService) g.POST("/restartXrayService", a.restartXrayService) @@ -177,6 +178,12 @@ func (a *ServerController) syncUpdateGeofile(c *gin.Context) { 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. func (a *ServerController) stopXrayService(c *gin.Context) { err := a.serverService.StopXrayService() diff --git a/web/html/index.html b/web/html/index.html index cecfa3f4..6c5195f2 100644 --- a/web/html/index.html +++ b/web/html/index.html @@ -332,6 +332,9 @@ [[ file ]] + + [[ geofileVersions[file].version || '-' ]] + @@ -827,6 +830,16 @@ table td, table th { padding: 2px 15px; } + + .geofile-version-text { + font-size: 11px; + color: #999; + margin-left: 8px; + } + + .dark .geofile-version-text { + color: #666; + } @@ -902,6 +915,7 @@ logModal, xraylogModal, backupModal, + geofileVersions: {}, loadingTip: '{{ i18n "loading"}}', showAlert: false, showIp: false, @@ -974,6 +988,7 @@ return; } versionModal.show(msg.obj); + this.loadGeofileVersions(); }, switchV2rayVersion(version) { 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() { this.loading(true); const msg = await HttpUtil.post('/panel/api/server/stopXrayService'); diff --git a/web/service/server.go b/web/service/server.go index dca560af..5188f9d2 100644 --- a/web/service/server.go +++ b/web/service/server.go @@ -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 - req, err := http.NewRequest("GET", url, nil) + req, err = http.NewRequest("GET", url, 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 @@ -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) 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() @@ -1126,46 +1142,57 @@ func (s *ServerService) UpdateGeofile(fileName string) error { // Handle 304 Not Modified if resp.StatusCode == http.StatusNotModified { updateFileModTime() - return nil + return capturedVersion, nil } 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) 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() _, err = io.Copy(file, resp.Body) 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() - return nil + return capturedVersion, nil } var errorMessages []string + versions := loadGeofileVersions(config.GetBinFolderPath()) if fileName == "" { // Download all geofiles for _, entry := range geofileAllowlist { 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)) + } else { + versions.Update(entry.FileName, ver) } } } else { entry := geofileAllowlist[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)) + } 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() if err != nil { 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 } +// 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) { // Run the command cmd := exec.Command(xray.GetBinaryPath(), "x25519")