- Add MaxMind GeoLite2-City mmdb auto-download and IP-based geo lookup (covers all 249 countries globally, replacing limited 8-country regex) - Add RenameOptions with 5 toggleable fields (flag/alias/country/city/index) stored as rename_options_json on collections - Collection rename pipeline uses DetectGeoWithServer(name, server) which tries GeoIP lookup first, falls back to name-based regex matching - Chinese country/city names + emoji flag prefix - Country-city dedup: skip city when it equals country (e.g. 香港/香港) - URI fragment encoding: url.QueryEscape -> url.PathEscape (%20 not +) - Egress cache key excludes name field so renamed nodes still match cache - Frontend: 5 toggle buttons in collection edit form, default all on - Migrations: 0004 (source alias + collection rename_enabled), 0005 (collection rename_options_json)
97 lines
2.5 KiB
Go
97 lines
2.5 KiB
Go
package geoip
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// GeoLite2-City mmdb download sources (tried in order)
|
|
// P3TERX/GeoLite.mmdb — GitHub-hosted, updated weekly, direct .mmdb (no tar)
|
|
mmdbMirrorURL = "https://raw.githubusercontent.com/P3TERX/GeoLite.mmdb/download/GeoLite2-City.mmdb"
|
|
// jsdelivr CDN mirror of the same repo (China-accessible)
|
|
mmdbFallbackURL = "https://cdn.jsdelivr.net/gh/P3TERX/GeoLite.mmdb@download/GeoLite2-City.mmdb"
|
|
// fastgit mirror (China-accessible)
|
|
mmdbFastgitURL = "https://raw.fastgit.org/P3TERX/GeoLite.mmdb/download/GeoLite2-City.mmdb"
|
|
|
|
mmdbDownloadTimeout = 120 * time.Second
|
|
)
|
|
|
|
// DownloadMmdb downloads the GeoLite2-City mmdb file to the given path.
|
|
// It tries multiple mirrors in order.
|
|
func DownloadMmdb(destPath string) error {
|
|
// Ensure parent directory exists
|
|
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
|
|
return fmt.Errorf("create mmdb dir: %w", err)
|
|
}
|
|
|
|
urls := []string{mmdbMirrorURL, mmdbFallbackURL, mmdbFastgitURL}
|
|
var lastErr error
|
|
for _, u := range urls {
|
|
if err := downloadDirect(destPath, u); err == nil {
|
|
return nil
|
|
} else {
|
|
lastErr = err
|
|
}
|
|
}
|
|
return fmt.Errorf("all download sources failed, last error: %w", lastErr)
|
|
}
|
|
|
|
func downloadDirect(destPath, url string) error {
|
|
client := &http.Client{Timeout: mmdbDownloadTimeout}
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("User-Agent", "sub-store/1.0")
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 200 {
|
|
return fmt.Errorf("download failed: HTTP %d", resp.StatusCode)
|
|
}
|
|
out, err := os.Create(destPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
if _, err := io.Copy(out, resp.Body); err != nil {
|
|
os.Remove(destPath)
|
|
return err
|
|
}
|
|
// Verify it's a valid mmdb by checking the file size (basic sanity)
|
|
if !isValidMmdb(destPath) {
|
|
os.Remove(destPath)
|
|
return fmt.Errorf("downloaded file is not a valid mmdb")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isValidMmdb(path string) bool {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer f.Close()
|
|
info, err := f.Stat()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return info.Size() > 1024
|
|
}
|
|
|
|
// EnsureMmdb ensures the mmdb file exists, downloading it if needed.
|
|
// Returns nil if the file already exists or was downloaded successfully.
|
|
func EnsureMmdb(path string) error {
|
|
if _, err := os.Stat(path); err == nil {
|
|
return nil // already exists
|
|
}
|
|
return DownloadMmdb(path)
|
|
}
|