Add MaxMind GeoIP2 database service with refreshable interface

This commit is contained in:
2026-04-09 21:49:46 +05:00
parent 2b277c8ee9
commit 64e4a138e3
+187
View File
@@ -0,0 +1,187 @@
package mmdb
import (
"context"
"errors"
"fmt"
"net/netip"
"os"
"sync"
"git.kor-elf.net/kor-elf-shield/geoip2"
"git.kor-elf.net/kor-elf-shield/geoip2/internal/pkg"
oschwaldGeoip2 "github.com/oschwald/geoip2-golang/v2"
)
// download is an interface for downloading MaxMind GeoIP2 database.
type download interface {
// Download downloads MaxMind GeoIP2 database.
Download(dir string, ctx context.Context) error
}
type service struct {
// download is an interface for downloading MaxMind GeoIP2 database.
download download
// info is a function that returns information about the IP address.
info info
logger geoip2.Logger
// dir is a directory for storing MaxMind GeoIP2 database.
dir *pkg.Dir
// isRefreshing is a flag that indicates whether the database is being refreshed.
isRefreshing bool
// mu is a mutex for synchronizing access to the reader.
mu sync.RWMutex
// reader is a MaxMind GeoIP2 database reader.
reader *oschwaldGeoip2.Reader
}
// info is a function that returns information about the IP address.
// @param ip - IP address
// @param reader - MaxMind GeoIP2 database reader
// @return geoip2.Info - information about the IP address
type info func(ip netip.Addr, reader *oschwaldGeoip2.Reader) (geoip2.Info, error)
// NewMMDB creates a new MaxMind GeoIP2 database service.
// @param download - an interface for downloading MaxMind GeoIP2 database
// @param info - a function that returns information about the IP address
// @param logger - a logger
// @param dir - a directory for storing MaxMind GeoIP2 database
// @return geoip2.RefreshableGeoIP2 - a MaxMind GeoIP2 database service
func NewMMDB(download download, info info, logger geoip2.Logger, dir *pkg.Dir) geoip2.RefreshableGeoIP2 {
s := &service{
download: download,
logger: logger,
dir: dir,
info: info,
mu: sync.RWMutex{},
}
s.init()
return s
}
// Info returns information about the IP address.
// @param ip - IP address
// @return geoip2.Info - information about the IP address
// @return error - an error if the IP address could not be found
func (s *service) Info(ip netip.Addr) (geoip2.Info, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if s.reader == nil {
return geoip2.Info{}, errors.New("geoip reader is not ready")
}
return s.info(ip, s.reader)
}
// Refresh refreshes the MaxMind GeoIP2 database.
// @param ctx - a context
// @return error - an error if the database could not be refreshed
func (s *service) Refresh(ctx context.Context) error {
if s.isRefreshing {
return fmt.Errorf("process is refreshing")
}
s.isRefreshing = true
defer func() {
s.isRefreshing = false
}()
if err := s.fetch(ctx); err != nil {
return err
}
newReader, err := s.openReader()
if err != nil {
return err
}
s.mu.Lock()
oldReader := s.reader
s.reader = newReader
s.mu.Unlock()
if oldReader != nil {
_ = oldReader.Close()
}
return nil
}
// Close closes the MaxMind GeoIP2 database.
// @return error - an error if the database could not be closed
func (s *service) Close() error {
if s.reader != nil {
return s.reader.Close()
}
return nil
}
// init initializes the MaxMind GeoIP2 database service.
func (s *service) init() {
reader, err := s.openReader()
if err != nil {
go func() {
// If there is no data yet, then we try to get the data.
if err := s.Refresh(context.Background()); err != nil {
s.logger.Error(err)
}
}()
return
}
s.mu.Lock()
defer s.mu.Unlock()
s.reader = reader
}
// openReader opens the MaxMind GeoIP2 database reader.
// @return (*oschwaldGeoip2.Reader, error) - a MaxMind GeoIP2 database reader and an error if the database could not be opened
// 1. If the database is already open, then it is returned.
// 2. If the database is not open, then it is opened and returned.
func (s *service) openReader() (*oschwaldGeoip2.Reader, error) {
path, err := pkg.FindMMDBFile(s.dir.PathCurrentDir)
if err != nil {
return nil, err
}
return oschwaldGeoip2.Open(path)
}
// fetch downloads the MaxMind GeoIP2 database.
// @param ctx - a context
// @return error - an error if the database could not be downloaded
func (s *service) fetch(ctx context.Context) error {
tmpDir, err := s.dir.CreateRandomTmpDir()
if err != nil {
return err
}
defer func() {
if err := os.RemoveAll(tmpDir); err != nil {
s.logger.Error(err)
}
}()
if err := s.download.Download(tmpDir, ctx); err != nil {
return err
}
tmpDirForFiles, err := s.dir.CreateRandomTmpDir()
if err != nil {
return err
}
defer func() {
if err := os.RemoveAll(tmpDir); err != nil {
s.logger.Error(err)
}
}()
if err := pkg.MovingFiles(tmpDir, tmpDirForFiles); err != nil {
return err
}
if err := s.dir.ReplaceDirToCurrent(tmpDirForFiles); err != nil {
return err
}
return nil
}