Skip to content

Commit f967501

Browse files
committed
feat(geolite): implement GeoLite2 database download from cloud
1 parent 29b83da commit f967501

File tree

18 files changed

+702
-35
lines changed

18 files changed

+702
-35
lines changed

.claude/settings.local.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
"Bash(cp:*)",
1818
"mcp__eslint__lint-files",
1919
"Bash(go generate:*)",
20-
"Bash(pnpm eslint:*)"
20+
"Bash(pnpm eslint:*)",
21+
"Read(//workspaces/cosy/settings/**)",
22+
"Bash(go doc:*)"
2123
],
2224
"deny": []
2325
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ internal/**/*.gen.go
2525
log-index/
2626
*.prof
2727
*.test
28+
GeoLite2-City.mmdb

api/geolite/download.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package geolite
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/0xJacky/Nginx-UI/internal/geolite"
7+
"github.com/gin-gonic/gin"
8+
"github.com/gorilla/websocket"
9+
"github.com/uozi-tech/cosy/logger"
10+
)
11+
12+
const (
13+
StatusInfo = "info"
14+
StatusError = "error"
15+
StatusProgress = "progress"
16+
)
17+
18+
type DownloadProgressResp struct {
19+
Status string `json:"status"`
20+
Progress float64 `json:"progress"`
21+
Message string `json:"message"`
22+
}
23+
24+
func DownloadGeoLiteDB(c *gin.Context) {
25+
var upgrader = websocket.Upgrader{
26+
CheckOrigin: func(r *http.Request) bool {
27+
return true
28+
},
29+
}
30+
31+
// Upgrade HTTP to WebSocket
32+
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
33+
if err != nil {
34+
logger.Error(err)
35+
return
36+
}
37+
defer ws.Close()
38+
39+
sendMessage := func(status, message string, progress float64) {
40+
if err := ws.WriteJSON(DownloadProgressResp{
41+
Status: status,
42+
Progress: progress,
43+
Message: message,
44+
}); err != nil {
45+
logger.Error("Failed to send WebSocket message:", err)
46+
}
47+
}
48+
49+
// Check if database already exists
50+
if geolite.DBExists() {
51+
sendMessage(StatusInfo, "Database already exists, removing old version...", 0)
52+
// Optionally remove old database here if you want to force re-download
53+
}
54+
55+
sendMessage(StatusInfo, "Starting download...", 0)
56+
57+
// Download progress channel
58+
downloadProgressChan := make(chan float64, 100)
59+
downloadDone := make(chan error, 1)
60+
61+
// Start download in goroutine
62+
go func() {
63+
downloadDone <- geolite.DownloadGeoLiteDB(downloadProgressChan)
64+
}()
65+
66+
// Track download progress (0-50%)
67+
downloadComplete := false
68+
for !downloadComplete {
69+
select {
70+
case progress := <-downloadProgressChan:
71+
// Scale download progress to 0-50%
72+
scaledProgress := progress * 0.5
73+
sendMessage(StatusProgress, "Downloading GeoLite2 database...", scaledProgress)
74+
case err := <-downloadDone:
75+
if err != nil {
76+
sendMessage(StatusError, "Download failed: "+err.Error(), 0)
77+
return
78+
}
79+
downloadComplete = true
80+
sendMessage(StatusInfo, "Download complete", 50)
81+
}
82+
}
83+
84+
sendMessage(StatusInfo, "Decompressing database...", 50)
85+
86+
// Decompress progress channel
87+
decompressProgressChan := make(chan float64, 100)
88+
decompressDone := make(chan error, 1)
89+
90+
// Start decompression in goroutine
91+
go func() {
92+
decompressDone <- geolite.DecompressGeoLiteDB(decompressProgressChan)
93+
}()
94+
95+
// Track decompression progress (50-100%)
96+
decompressComplete := false
97+
for !decompressComplete {
98+
select {
99+
case progress := <-decompressProgressChan:
100+
// Scale decompress progress to 50-100%
101+
scaledProgress := 50 + (progress * 0.5)
102+
sendMessage(StatusProgress, "Decompressing database...", scaledProgress)
103+
case err := <-decompressDone:
104+
if err != nil {
105+
sendMessage(StatusError, "Decompression failed: "+err.Error(), 50)
106+
return
107+
}
108+
decompressComplete = true
109+
sendMessage(StatusInfo, "Database ready", 100)
110+
}
111+
}
112+
113+
sendMessage(StatusInfo, "GeoLite2 database downloaded and installed successfully", 100)
114+
}

api/geolite/status.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package geolite
2+
3+
import (
4+
"net/http"
5+
"os"
6+
"time"
7+
8+
"github.com/0xJacky/Nginx-UI/internal/geolite"
9+
"github.com/gin-gonic/gin"
10+
"github.com/uozi-tech/cosy"
11+
)
12+
13+
type StatusResp struct {
14+
Exists bool `json:"exists"`
15+
Path string `json:"path"`
16+
Size int64 `json:"size"`
17+
LastModified string `json:"last_modified"`
18+
}
19+
20+
func GetStatus(c *gin.Context) {
21+
dbPath := geolite.GetDBPath()
22+
resp := StatusResp{
23+
Exists: geolite.DBExists(),
24+
Path: dbPath,
25+
}
26+
27+
if resp.Exists {
28+
fileInfo, err := os.Stat(dbPath)
29+
if err != nil {
30+
cosy.ErrHandler(c, err)
31+
return
32+
}
33+
resp.Size = fileInfo.Size()
34+
resp.LastModified = fileInfo.ModTime().Format(time.RFC3339)
35+
}
36+
37+
c.JSON(http.StatusOK, resp)
38+
}

app/components.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ declare module 'vue' {
1111
AAlert: typeof import('ant-design-vue/es')['Alert']
1212
AApp: typeof import('ant-design-vue/es')['App']
1313
AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
14+
AAvatar: typeof import('ant-design-vue/es')['Avatar']
1415
ABadge: typeof import('ant-design-vue/es')['Badge']
1516
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
1617
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
@@ -69,6 +70,7 @@ declare module 'vue' {
6970
ATag: typeof import('ant-design-vue/es')['Tag']
7071
ATextarea: typeof import('ant-design-vue/es')['Textarea']
7172
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
73+
ATypographyParagraph: typeof import('ant-design-vue/es')['TypographyParagraph']
7274
ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
7375
ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
7476
AutoCertFormAutoCertForm: typeof import('./src/components/AutoCertForm/AutoCertForm.vue')['default']
@@ -84,6 +86,7 @@ declare module 'vue' {
8486
ConfigHistoryDiffViewer: typeof import('./src/components/ConfigHistory/DiffViewer.vue')['default']
8587
DevDebugPanelDevDebugPanel: typeof import('./src/components/DevDebugPanel/DevDebugPanel.vue')['default']
8688
FooterToolbarFooterToolBar: typeof import('./src/components/FooterToolbar/FooterToolBar.vue')['default']
89+
GeoLiteDownloadGeoLiteDownload: typeof import('./src/components/GeoLiteDownload/GeoLiteDownload.vue')['default']
8790
ICPICP: typeof import('./src/components/ICP/ICP.vue')['default']
8891
InspectConfigInspectConfig: typeof import('./src/components/InspectConfig/InspectConfig.vue')['default']
8992
LLMChatMessage: typeof import('./src/components/LLM/ChatMessage.vue')['default']

app/src/api/geolite.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { http } from '@uozi-admin/request'
2+
3+
export interface GeoLiteStatus {
4+
exists: boolean
5+
path: string
6+
size: number
7+
last_modified: string
8+
}
9+
10+
const geolite = {
11+
async getStatus() {
12+
return http.get<GeoLiteStatus>('geolite/status')
13+
},
14+
}
15+
16+
export default geolite

0 commit comments

Comments
 (0)