feat(geolite): implement GeoLite2 database download from cloud

This commit is contained in:
0xJacky
2025-10-04 13:23:33 +00:00
parent 29b83da8cb
commit f967501412
18 changed files with 703 additions and 36 deletions
+3 -1
View File
@@ -17,7 +17,9 @@
"Bash(cp:*)",
"mcp__eslint__lint-files",
"Bash(go generate:*)",
"Bash(pnpm eslint:*)"
"Bash(pnpm eslint:*)",
"Read(//workspaces/cosy/settings/**)",
"Bash(go doc:*)"
],
"deny": []
}
+1
View File
@@ -25,3 +25,4 @@ internal/**/*.gen.go
log-index/
*.prof
*.test
GeoLite2-City.mmdb
+114
View File
@@ -0,0 +1,114 @@
package geolite
import (
"net/http"
"github.com/0xJacky/Nginx-UI/internal/geolite"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/uozi-tech/cosy/logger"
)
const (
StatusInfo = "info"
StatusError = "error"
StatusProgress = "progress"
)
type DownloadProgressResp struct {
Status string `json:"status"`
Progress float64 `json:"progress"`
Message string `json:"message"`
}
func DownloadGeoLiteDB(c *gin.Context) {
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// Upgrade HTTP to WebSocket
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error(err)
return
}
defer ws.Close()
sendMessage := func(status, message string, progress float64) {
if err := ws.WriteJSON(DownloadProgressResp{
Status: status,
Progress: progress,
Message: message,
}); err != nil {
logger.Error("Failed to send WebSocket message:", err)
}
}
// Check if database already exists
if geolite.DBExists() {
sendMessage(StatusInfo, "Database already exists, removing old version...", 0)
// Optionally remove old database here if you want to force re-download
}
sendMessage(StatusInfo, "Starting download...", 0)
// Download progress channel
downloadProgressChan := make(chan float64, 100)
downloadDone := make(chan error, 1)
// Start download in goroutine
go func() {
downloadDone <- geolite.DownloadGeoLiteDB(downloadProgressChan)
}()
// Track download progress (0-50%)
downloadComplete := false
for !downloadComplete {
select {
case progress := <-downloadProgressChan:
// Scale download progress to 0-50%
scaledProgress := progress * 0.5
sendMessage(StatusProgress, "Downloading GeoLite2 database...", scaledProgress)
case err := <-downloadDone:
if err != nil {
sendMessage(StatusError, "Download failed: "+err.Error(), 0)
return
}
downloadComplete = true
sendMessage(StatusInfo, "Download complete", 50)
}
}
sendMessage(StatusInfo, "Decompressing database...", 50)
// Decompress progress channel
decompressProgressChan := make(chan float64, 100)
decompressDone := make(chan error, 1)
// Start decompression in goroutine
go func() {
decompressDone <- geolite.DecompressGeoLiteDB(decompressProgressChan)
}()
// Track decompression progress (50-100%)
decompressComplete := false
for !decompressComplete {
select {
case progress := <-decompressProgressChan:
// Scale decompress progress to 50-100%
scaledProgress := 50 + (progress * 0.5)
sendMessage(StatusProgress, "Decompressing database...", scaledProgress)
case err := <-decompressDone:
if err != nil {
sendMessage(StatusError, "Decompression failed: "+err.Error(), 50)
return
}
decompressComplete = true
sendMessage(StatusInfo, "Database ready", 100)
}
}
sendMessage(StatusInfo, "GeoLite2 database downloaded and installed successfully", 100)
}
+38
View File
@@ -0,0 +1,38 @@
package geolite
import (
"net/http"
"os"
"time"
"github.com/0xJacky/Nginx-UI/internal/geolite"
"github.com/gin-gonic/gin"
"github.com/uozi-tech/cosy"
)
type StatusResp struct {
Exists bool `json:"exists"`
Path string `json:"path"`
Size int64 `json:"size"`
LastModified string `json:"last_modified"`
}
func GetStatus(c *gin.Context) {
dbPath := geolite.GetDBPath()
resp := StatusResp{
Exists: geolite.DBExists(),
Path: dbPath,
}
if resp.Exists {
fileInfo, err := os.Stat(dbPath)
if err != nil {
cosy.ErrHandler(c, err)
return
}
resp.Size = fileInfo.Size()
resp.LastModified = fileInfo.ModTime().Format(time.RFC3339)
}
c.JSON(http.StatusOK, resp)
}
+3
View File
@@ -11,6 +11,7 @@ declare module 'vue' {
AAlert: typeof import('ant-design-vue/es')['Alert']
AApp: typeof import('ant-design-vue/es')['App']
AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
AAvatar: typeof import('ant-design-vue/es')['Avatar']
ABadge: typeof import('ant-design-vue/es')['Badge']
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
@@ -69,6 +70,7 @@ declare module 'vue' {
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypographyParagraph: typeof import('ant-design-vue/es')['TypographyParagraph']
ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
AutoCertFormAutoCertForm: typeof import('./src/components/AutoCertForm/AutoCertForm.vue')['default']
@@ -84,6 +86,7 @@ declare module 'vue' {
ConfigHistoryDiffViewer: typeof import('./src/components/ConfigHistory/DiffViewer.vue')['default']
DevDebugPanelDevDebugPanel: typeof import('./src/components/DevDebugPanel/DevDebugPanel.vue')['default']
FooterToolbarFooterToolBar: typeof import('./src/components/FooterToolbar/FooterToolBar.vue')['default']
GeoLiteDownloadGeoLiteDownload: typeof import('./src/components/GeoLiteDownload/GeoLiteDownload.vue')['default']
ICPICP: typeof import('./src/components/ICP/ICP.vue')['default']
InspectConfigInspectConfig: typeof import('./src/components/InspectConfig/InspectConfig.vue')['default']
LLMChatMessage: typeof import('./src/components/LLM/ChatMessage.vue')['default']
+16
View File
@@ -0,0 +1,16 @@
import { http } from '@uozi-admin/request'
export interface GeoLiteStatus {
exists: boolean
path: string
size: number
last_modified: string
}
const geolite = {
async getStatus() {
return http.get<GeoLiteStatus>('geolite/status')
},
}
export default geolite
@@ -0,0 +1,222 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import { CheckCircleOutlined, DownloadOutlined, InfoCircleOutlined } from '@ant-design/icons-vue'
import geolite from '@/api/geolite'
import { formatDateTime } from '@/lib/helper'
import websocket from '@/lib/websocket'
interface Emits {
(e: 'downloadComplete'): void
}
const emit = defineEmits<Emits>()
// GeoLite database state
const geoLiteStatus = ref({
exists: false,
path: '',
size: 0,
last_modified: '',
})
const geoLiteLoading = ref(false)
const downloading = ref(false)
const downloadProgress = ref(0)
const downloadStatus = ref('active') as Ref<'normal' | 'active' | 'success' | 'exception'>
const downloadMessage = ref('')
const progressStrokeColor = {
from: '#108ee9',
to: '#87d068',
}
const downloadProgressComputed = computed(() => {
return Number.parseFloat(downloadProgress.value.toFixed(1))
})
// Check GeoLite database status
async function checkGeoLiteStatus() {
try {
geoLiteLoading.value = true
const status = await geolite.getStatus()
geoLiteStatus.value = status
}
catch (e) {
console.error('Failed to check GeoLite status:', e)
}
finally {
geoLiteLoading.value = false
}
}
// Download GeoLite database
function downloadGeoLiteDB() {
downloading.value = true
downloadStatus.value = 'active'
downloadProgress.value = 0
downloadMessage.value = $gettext('Starting download...')
const ws = websocket('/api/geolite/download', false)
let isFailed = false
let currentPhase = 'download' // 'download' or 'decompress'
ws.onopen = () => {
// WebSocket connected, server will start download
}
ws.onmessage = async m => {
const r = JSON.parse(m.data)
// Update message and detect phase changes
if (r.message) {
downloadMessage.value = r.message
// Detect phase transition
if (r.message.toLowerCase().includes('decompress')) {
currentPhase = 'decompress'
}
}
switch (r.status) {
case 'info':
// Info messages handled above
break
case 'progress': {
// Map progress to correct range based on phase
const actualProgress = currentPhase === 'download'
? (r.progress / 100) * 50 // Download phase: 0-50%
: 50 + (r.progress / 100) * 50 // Decompress phase: 50-100%
downloadProgress.value = Math.min(actualProgress, 100)
break
}
case 'error':
downloadStatus.value = 'exception'
isFailed = true
break
default:
break
}
}
ws.onerror = () => {
isFailed = true
downloadStatus.value = 'exception'
downloadMessage.value = $gettext('Download failed')
}
ws.onclose = async () => {
if (isFailed) {
downloading.value = false
return
}
downloadStatus.value = 'success'
downloadProgress.value = 100
downloadMessage.value = $gettext('Download complete')
// Refresh status
await checkGeoLiteStatus()
// Emit completion event
emit('downloadComplete')
// Reset after 2 seconds
setTimeout(() => {
downloading.value = false
downloadProgress.value = 0
downloadMessage.value = ''
}, 2000)
}
}
// Auto-check status on mount
onMounted(() => {
checkGeoLiteStatus()
})
// Expose methods for parent components
defineExpose({
checkGeoLiteStatus,
downloadGeoLiteDB,
})
</script>
<template>
<div>
<AAlert
v-if="!geoLiteStatus.exists && !downloading"
:message="$gettext('GeoLite2 Database Required')"
type="info"
show-icon
:icon="h(InfoCircleOutlined)"
class="mb-3"
>
<template #description>
<div class="space-y-2">
<p>{{ $gettext('The GeoLite2 database is required for offline geographic IP analysis. Please download it to enable this feature.') }}</p>
<p class="text-sm">
{{ $gettext('Alternatively, if you cannot download the database, you can manually place') }}
<code class="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded">GeoLite2-City.mmdb</code>
{{ $gettext('in the same directory as') }}
<code class="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded">app.ini</code>
</p>
</div>
</template>
</AAlert>
<AAlert
v-else-if="geoLiteStatus.exists && !downloading"
:message="$gettext('GeoLite2 Database Installed')"
type="success"
show-icon
:icon="h(CheckCircleOutlined)"
class="mb-3"
banner
/>
<div class="space-y-3">
<!-- Download Button -->
<div class="flex items-center space-x-3">
<AButton
v-if="!geoLiteStatus.exists"
type="primary"
:loading="geoLiteLoading"
:disabled="downloading"
@click="downloadGeoLiteDB"
>
<DownloadOutlined />
{{ $gettext('Download GeoLite2 Database') }}
</AButton>
<AButton
v-else
:loading="geoLiteLoading"
:disabled="downloading"
@click="downloadGeoLiteDB"
>
<DownloadOutlined />
{{ $gettext('Re-download Database') }}
</AButton>
<ATypographyText v-if="geoLiteStatus.exists && !downloading" type="secondary" class="text-xs">
{{ $gettext('Last updated:') }} {{ formatDateTime(geoLiteStatus.last_modified) }}
</ATypographyText>
</div>
<!-- Inline Progress Bar -->
<div v-if="downloading" class="download-progress-section">
<AProgress
:stroke-color="progressStrokeColor"
:percent="downloadProgressComputed"
:status="downloadStatus"
/>
<ATypographyText v-if="downloadMessage" type="secondary" class="text-sm mt-2">
{{ downloadMessage }}
</ATypographyText>
</div>
</div>
</div>
</template>
<style scoped lang="less">
</style>
@@ -0,0 +1 @@
export { default } from './GeoLiteDownload.vue'
+4 -3
View File
@@ -1,11 +1,12 @@
import type { NginxPerformanceInfo } from '@/api/ngx'
import ngx from '@/api/ngx'
import { formatDateTime } from '@/lib/helper'
export function useNginxPerformance() {
const loading = ref(false)
const error = ref('')
const nginxInfo = ref<NginxPerformanceInfo | null>(null)
const lastUpdateTime = ref<Date | null>(null)
const lastUpdateTime = ref<string>('')
// stub_status availability
const stubStatusEnabled = ref(false)
@@ -16,12 +17,12 @@ export function useNginxPerformance() {
const formattedUpdateTime = computed(() => {
if (!lastUpdateTime.value)
return $gettext('Unknown')
return lastUpdateTime.value.toLocaleString()
return formatDateTime(lastUpdateTime.value)
})
// Update the last update time
function updateLastUpdateTime() {
lastUpdateTime.value = new Date()
lastUpdateTime.value = new Date().toISOString()
}
// Check stub_status availability and get initial data
@@ -1,5 +1,7 @@
<script setup lang="tsx">
import { CheckCircleOutlined, CloseCircleOutlined, HeartOutlined, InfoCircleOutlined, MailOutlined, ThunderboltOutlined, WarningOutlined } from '@ant-design/icons-vue'
import { CheckCircleOutlined, CloseCircleOutlined, DownloadOutlined, HeartOutlined, InfoCircleOutlined, MailOutlined, ThunderboltOutlined, WarningOutlined } from '@ant-design/icons-vue'
import geolite from '@/api/geolite'
import GeoLiteDownload from '@/components/GeoLiteDownload'
interface Props {
loading?: boolean
@@ -18,6 +20,29 @@ const emit = defineEmits<Emits>()
const visible = defineModel<boolean>('visible', { required: true })
// GeoLite database status
const geoLiteDownloadRef = ref<InstanceType<typeof GeoLiteDownload>>()
const geoLiteExists = ref(false)
// Check GeoLite status when modal opens
watch(visible, async newVal => {
if (newVal) {
try {
const status = await geolite.getStatus()
geoLiteExists.value = status.exists
}
catch (e) {
console.error('Failed to check GeoLite status:', e)
geoLiteExists.value = false
}
}
})
// Update GeoLite status when download completes
function onGeoLiteDownloadComplete() {
geoLiteExists.value = true
}
const systemRequirements = [
{
title: $gettext('CPU'),
@@ -73,6 +98,7 @@ function handleCancel() {
:title="$gettext('Enable Advanced Log Indexing')"
:confirm-loading="loading"
:ok-text="$gettext('Enable Indexing')"
:ok-button-props="{ disabled: !geoLiteExists }"
:cancel-text="$gettext('Cancel')"
width="720px"
@ok="handleConfirm"
@@ -146,6 +172,21 @@ function handleCancel() {
<ADivider />
<!-- GeoLite Database Section -->
<div>
<ATypographyTitle :level="4" class="mb-3">
<DownloadOutlined class="mr-2 text-purple-500" />
{{ $gettext('GeoLite2 Database') }}
</ATypographyTitle>
<GeoLiteDownload
ref="geoLiteDownloadRef"
@download-complete="onGeoLiteDownloadComplete"
/>
</div>
<ADivider />
<!-- Performance Statistics -->
<div>
<ATypographyTitle :level="4" class="mb-3">
+8 -1
View File
@@ -5,6 +5,7 @@ import {
AuthSettings,
CertSettings,
ExternalNotify,
GeoLiteSettings,
HTTPSettings,
LogrotateSettings,
NginxSettings,
@@ -107,9 +108,15 @@ onMounted(() => {
>
<LogrotateSettings />
</ATabPane>
<ATabPane
key="geolite"
:tab="$gettext('GeoLite')"
>
<GeoLiteSettings />
</ATabPane>
</ATabs>
</div>
<FooterToolBar v-if="activeKey !== 'external_notify'">
<FooterToolBar v-if="activeKey !== 'external_notify' && activeKey !== 'geolite'">
<AButton
type="primary"
@click="systemSettingsStore.save"
@@ -0,0 +1,17 @@
<script setup lang="ts">
import GeoLiteDownload from '@/components/GeoLiteDownload'
</script>
<template>
<AForm layout="vertical">
<AFormItem :label="$gettext('GeoLite2 Database')">
<ATypographyParagraph type="secondary">
{{ $gettext('The GeoLite2 database provides geographic information for IP addresses. This is used for offline geographic analysis in log analytics.') }}
</ATypographyParagraph>
<GeoLiteDownload />
</AFormItem>
</AForm>
</template>
<style lang="less" scoped>
</style>
+1
View File
@@ -2,6 +2,7 @@ export { default as AppSettings } from './AppSettings.vue'
export { default as AuthSettings } from './AuthSettings.vue'
export { default as CertSettings } from './CertSettings.vue'
export { default as ExternalNotify } from './ExternalNotify.vue'
export { default as GeoLiteSettings } from './GeoLiteSettings.vue'
export { default as HTTPSettings } from './HTTPSettings.vue'
export { default as LogrotateSettings } from './LogrotateSettings.vue'
export { default as NginxSettings } from './NginxSettings.vue'
Binary file not shown.
+197
View File
@@ -0,0 +1,197 @@
package geolite
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"github.com/ulikunitz/xz"
"github.com/uozi-tech/cosy"
"github.com/uozi-tech/cosy/settings"
)
const (
DownloadURL = "http://cloud.nginxui.com/geolite/GeoLite2-City.mmdb.xz"
)
type DownloadProgressWriter struct {
io.Writer
totalSize int64
currentSize int64
progressChan chan<- float64
lastReported float64
reportInterval float64 // Report only when progress changes by this amount
}
func (pw *DownloadProgressWriter) Write(p []byte) (int, error) {
n, err := pw.Writer.Write(p)
pw.currentSize += int64(n)
progress := float64(pw.currentSize) / float64(pw.totalSize) * 100
// Debounce: only send updates when progress changes by reportInterval or reaches 100%
if progress-pw.lastReported >= pw.reportInterval || progress >= 100 {
select {
case pw.progressChan <- progress:
pw.lastReported = progress
default:
}
}
return n, err
}
// GetDBPath returns the path to the GeoLite2 database file
func GetDBPath() string {
confDir := filepath.Dir(settings.ConfPath)
return filepath.Join(confDir, "GeoLite2-City.mmdb")
}
// GetDBXZPath returns the path to the compressed GeoLite2 database file
func GetDBXZPath() string {
confDir := filepath.Dir(settings.ConfPath)
return filepath.Join(confDir, "GeoLite2-City.mmdb.xz")
}
// DownloadGeoLiteDB downloads the GeoLite2 database
func DownloadGeoLiteDB(progressChan chan float64) error {
client := &http.Client{}
req, err := http.NewRequest("GET", DownloadURL, nil)
if err != nil {
return cosy.WrapErrorWithParams(ErrDownloadFailed, err.Error())
}
resp, err := client.Do(req)
if err != nil {
return cosy.WrapErrorWithParams(ErrDownloadFailed, err.Error())
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return cosy.WrapErrorWithParams(ErrDownloadFailed, fmt.Sprintf("status code: %d", resp.StatusCode))
}
totalSize, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
if err != nil {
return cosy.WrapErrorWithParams(ErrFailedToGetFileSize, err.Error())
}
xzPath := GetDBXZPath()
file, err := os.Create(xzPath)
if err != nil {
return cosy.WrapErrorWithParams(ErrFailedToCreateFile, err.Error())
}
defer file.Close()
progressWriter := &DownloadProgressWriter{
Writer: file,
totalSize: totalSize,
progressChan: progressChan,
reportInterval: 1.0, // Report every 1% change
}
_, err = io.Copy(progressWriter, resp.Body)
if err != nil {
os.Remove(xzPath) // Clean up on error
return cosy.WrapErrorWithParams(ErrFailedToSaveFile, err.Error())
}
return nil
}
// DecompressGeoLiteDB decompresses the .xz file to .mmdb
func DecompressGeoLiteDB(progressChan chan float64) error {
xzPath := GetDBXZPath()
dbPath := GetDBPath()
// Open compressed file
xzFile, err := os.Open(xzPath)
if err != nil {
return cosy.WrapErrorWithParams(ErrFailedToOpenFile, err.Error())
}
defer xzFile.Close()
// Get compressed file size
fileInfo, err := xzFile.Stat()
if err != nil {
return cosy.WrapErrorWithParams(ErrFailedToGetFileSize, err.Error())
}
compressedSize := fileInfo.Size()
// Create XZ reader
xzReader, err := xz.NewReader(xzFile)
if err != nil {
return cosy.WrapErrorWithParams(ErrFailedToCreateXZReader, err.Error())
}
// Create output file
outFile, err := os.Create(dbPath)
if err != nil {
return cosy.WrapErrorWithParams(ErrFailedToCreateFile, err.Error())
}
defer outFile.Close()
// Decompress with progress tracking
buf := make([]byte, 64*1024) // 64KB buffer for better performance
var decompressedSize int64
var lastReportedProgress float64
const reportInterval = 2.0 // Report every 2% change
// Estimate: XZ typically compresses to 10-20% of original size
// We'll use 15% (compression ratio ~6.67) as middle estimate
const estimatedCompressionRatio = 6.67
estimatedTotalSize := float64(compressedSize) * estimatedCompressionRatio
for {
n, readErr := xzReader.Read(buf)
if n > 0 {
if _, writeErr := outFile.Write(buf[:n]); writeErr != nil {
os.Remove(dbPath) // Clean up on error
return cosy.WrapErrorWithParams(ErrFailedToWriteData, writeErr.Error())
}
decompressedSize += int64(n)
// Calculate progress based on estimated total size
progress := (float64(decompressedSize) / estimatedTotalSize) * 100
if progress > 99 {
progress = 99 // Cap at 99% until actually complete
}
// Debounce: only send updates when progress changes significantly
if progress-lastReportedProgress >= reportInterval || readErr == io.EOF {
select {
case progressChan <- progress:
lastReportedProgress = progress
default:
}
}
}
if readErr == io.EOF {
// Send 100% on completion
select {
case progressChan <- 100:
default:
}
break
}
if readErr != nil {
os.Remove(dbPath) // Clean up on error
return cosy.WrapErrorWithParams(ErrFailedToReadData, readErr.Error())
}
}
// Delete the .xz file after successful decompression
if err := os.Remove(xzPath); err != nil {
// Log but don't fail if we can't delete the compressed file
return cosy.WrapErrorWithParams(ErrFailedToDeleteCompressed, err.Error())
}
return nil
}
// DBExists checks if the GeoLite2 database file exists
func DBExists() bool {
_, err := os.Stat(GetDBPath())
return err == nil
}
+18
View File
@@ -0,0 +1,18 @@
package geolite
import "github.com/uozi-tech/cosy"
var (
e = cosy.NewErrorScope("geolite")
ErrDownloadFailed = e.New(60000, "failed to download GeoLite2 database: {0}")
ErrDecompressionFailed = e.New(60001, "failed to decompress GeoLite2 database: {0}")
ErrDatabaseNotFound = e.New(60002, "GeoLite2 database not found at {0}")
ErrFailedToGetFileSize = e.New(60003, "failed to get file size: {0}")
ErrFailedToCreateFile = e.New(60004, "failed to create file: {0}")
ErrFailedToSaveFile = e.New(60005, "failed to save downloaded file: {0}")
ErrFailedToOpenFile = e.New(60006, "failed to open file: {0}")
ErrFailedToCreateXZReader = e.New(60007, "failed to create xz reader: {0}")
ErrFailedToWriteData = e.New(60008, "failed to write decompressed data: {0}")
ErrFailedToReadData = e.New(60009, "failed to read compressed data: {0}")
ErrFailedToDeleteCompressed = e.New(60010, "decompression succeeded but failed to delete compressed file: {0}")
)
+15 -30
View File
@@ -1,21 +1,14 @@
package geolite
package geolite
import (
"bytes"
_ "embed"
"fmt"
"io"
"net"
"sync"
"github.com/oschwald/geoip2-golang"
"github.com/ulikunitz/xz"
"github.com/uozi-tech/cosy/geoip"
)
//go:embed GeoLite2-City.mmdb.xz
var embeddedCityDB []byte
type IPLocation struct {
RegionCode string `json:"region_code"`
Province string `json:"province"`
@@ -41,33 +34,25 @@ func GetService() (*Service, error) {
}
func (s *Service) init() error {
// Load embedded compressed database
if len(embeddedCityDB) > 0 {
if err := s.loadEmbeddedCityDB(); err != nil {
return fmt.Errorf("failed to load embedded city database: %v", err)
}
return nil
// Load database from file (memory-mapped for efficiency)
dbPath := GetDBPath()
if !DBExists() {
return fmt.Errorf("GeoLite2 database not found at %s. Please download it first", dbPath)
}
return fmt.Errorf("no embedded GeoLite2 City database available")
if err := s.loadFromFile(dbPath); err != nil {
return fmt.Errorf("failed to load GeoLite2 database: %v", err)
}
return nil
}
func (s *Service) loadEmbeddedCityDB() error {
// Decompress the embedded database
xzReader, err := xz.NewReader(bytes.NewReader(embeddedCityDB))
func (s *Service) loadFromFile(path string) error {
// Open database file with memory mapping (more efficient than loading into memory)
cityDB, err := geoip2.Open(path)
if err != nil {
return fmt.Errorf("failed to create xz reader: %v", err)
}
decompressedData, err := io.ReadAll(xzReader)
if err != nil {
return fmt.Errorf("failed to decompress database: %v", err)
}
// Create geoip2 reader from decompressed data
cityDB, err := geoip2.FromBytes(decompressedData)
if err != nil {
return fmt.Errorf("failed to create geoip2 reader: %v", err)
return fmt.Errorf("failed to open GeoLite2 database: %v", err)
}
s.cityDB = cityDB
+3
View File
@@ -12,6 +12,7 @@ import (
"github.com/0xJacky/Nginx-UI/api/crypto"
"github.com/0xJacky/Nginx-UI/api/event"
"github.com/0xJacky/Nginx-UI/api/external_notify"
"github.com/0xJacky/Nginx-UI/api/geolite"
"github.com/0xJacky/Nginx-UI/api/license"
"github.com/0xJacky/Nginx-UI/api/llm"
"github.com/0xJacky/Nginx-UI/api/nginx"
@@ -96,6 +97,7 @@ func InitRouter() {
external_notify.InitRouter(g)
backup.InitAutoBackupRouter(g)
nginxLog.InitRouter(g)
g.GET("/geolite/status", geolite.GetStatus)
}
// Authorization required and websocket request
@@ -113,6 +115,7 @@ func InitRouter() {
system.InitWebSocketRouter(w)
nginx.InitWebSocketRouter(w)
cluster.InitWebSocketRouter(w)
w.GET("/geolite/download", geolite.DownloadGeoLiteDB)
}
}
}