DB 적재 초기구조
This commit is contained in:
@@ -2,17 +2,33 @@ package geo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/oschwald/geoip2-golang"
|
||||
)
|
||||
|
||||
// ErrInvalidIP is returned when an IP cannot be parsed.
|
||||
var ErrInvalidIP = errors.New("invalid ip address")
|
||||
|
||||
type Resolver struct {
|
||||
db *geoip2.Reader
|
||||
// ErrNotFound is returned when a backend cannot resolve the IP.
|
||||
var ErrNotFound = errors.New("location not found")
|
||||
|
||||
type Backend string
|
||||
|
||||
const (
|
||||
BackendMMDB Backend = "mmdb"
|
||||
BackendPostgres Backend = "postgres"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Backend Backend
|
||||
MMDBPath string
|
||||
DatabaseURL string
|
||||
LookupQuery string
|
||||
}
|
||||
|
||||
type Resolver interface {
|
||||
Lookup(string) (Location, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type Location struct {
|
||||
@@ -25,56 +41,23 @@ type Location struct {
|
||||
Longitude float64
|
||||
}
|
||||
|
||||
func NewResolver(dbPath string) (*Resolver, error) {
|
||||
if dbPath == "" {
|
||||
return nil, errors.New("db path is required")
|
||||
func NewResolver(cfg Config) (Resolver, error) {
|
||||
switch cfg.Backend {
|
||||
case "", BackendMMDB:
|
||||
return newMMDBResolver(cfg.MMDBPath)
|
||||
case BackendPostgres:
|
||||
return newPostgresResolver(cfg.DatabaseURL, cfg.LookupQuery)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported backend %q", cfg.Backend)
|
||||
}
|
||||
|
||||
db, err := geoip2.Open(dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Resolver{db: db}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) Close() error {
|
||||
return r.db.Close()
|
||||
}
|
||||
|
||||
func (r *Resolver) Lookup(ipStr string) (Location, error) {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return Location{}, ErrInvalidIP
|
||||
}
|
||||
|
||||
record, err := r.db.City(ip)
|
||||
if err != nil {
|
||||
return Location{}, err
|
||||
}
|
||||
|
||||
country := record.Country.Names["en"]
|
||||
region := ""
|
||||
if len(record.Subdivisions) > 0 {
|
||||
region = record.Subdivisions[0].Names["en"]
|
||||
}
|
||||
|
||||
city := record.City.Names["en"]
|
||||
|
||||
addressParts := make([]string, 0, 3)
|
||||
for _, part := range []string{city, region, country} {
|
||||
func buildAddress(parts ...string) string {
|
||||
addressParts := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if part != "" {
|
||||
addressParts = append(addressParts, part)
|
||||
}
|
||||
}
|
||||
|
||||
return Location{
|
||||
IP: ip.String(),
|
||||
Country: country,
|
||||
Region: region,
|
||||
City: city,
|
||||
Address: strings.Join(addressParts, ", "),
|
||||
Latitude: record.Location.Latitude,
|
||||
Longitude: record.Location.Longitude,
|
||||
}, nil
|
||||
return strings.Join(addressParts, ", ")
|
||||
}
|
||||
|
||||
59
internal/geo/resolver_mmdb.go
Normal file
59
internal/geo/resolver_mmdb.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package geo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/oschwald/geoip2-golang"
|
||||
)
|
||||
|
||||
type mmdbResolver struct {
|
||||
db *geoip2.Reader
|
||||
}
|
||||
|
||||
func newMMDBResolver(dbPath string) (Resolver, error) {
|
||||
if dbPath == "" {
|
||||
return nil, errors.New("mmdb path is required")
|
||||
}
|
||||
|
||||
db, err := geoip2.Open(dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mmdbResolver{db: db}, nil
|
||||
}
|
||||
|
||||
func (r *mmdbResolver) Close() error {
|
||||
return r.db.Close()
|
||||
}
|
||||
|
||||
func (r *mmdbResolver) Lookup(ipStr string) (Location, error) {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return Location{}, ErrInvalidIP
|
||||
}
|
||||
|
||||
record, err := r.db.City(ip)
|
||||
if err != nil {
|
||||
return Location{}, err
|
||||
}
|
||||
|
||||
country := record.Country.Names["en"]
|
||||
region := ""
|
||||
if len(record.Subdivisions) > 0 {
|
||||
region = record.Subdivisions[0].Names["en"]
|
||||
}
|
||||
|
||||
city := record.City.Names["en"]
|
||||
|
||||
return Location{
|
||||
IP: ip.String(),
|
||||
Country: country,
|
||||
Region: region,
|
||||
City: city,
|
||||
Address: buildAddress(city, region, country),
|
||||
Latitude: record.Location.Latitude,
|
||||
Longitude: record.Location.Longitude,
|
||||
}, nil
|
||||
}
|
||||
98
internal/geo/resolver_postgres.go
Normal file
98
internal/geo/resolver_postgres.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package geo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
const defaultLookupQuery = `
|
||||
SELECT
|
||||
ip::text,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
latitude,
|
||||
longitude
|
||||
FROM geoip.lookup_city($1);
|
||||
`
|
||||
|
||||
type postgresResolver struct {
|
||||
db *sql.DB
|
||||
lookupQuery string
|
||||
}
|
||||
|
||||
func newPostgresResolver(databaseURL, lookupQuery string) (Resolver, error) {
|
||||
if databaseURL == "" {
|
||||
return nil, errors.New("database url is required for postgres backend")
|
||||
}
|
||||
|
||||
db, err := sql.Open("pgx", databaseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(10)
|
||||
db.SetMaxIdleConns(2)
|
||||
db.SetConnMaxIdleTime(5 * time.Minute)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if lookupQuery == "" {
|
||||
lookupQuery = defaultLookupQuery
|
||||
}
|
||||
|
||||
return &postgresResolver{
|
||||
db: db,
|
||||
lookupQuery: lookupQuery,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *postgresResolver) Close() error {
|
||||
return r.db.Close()
|
||||
}
|
||||
|
||||
func (r *postgresResolver) Lookup(ipStr string) (Location, error) {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return Location{}, ErrInvalidIP
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
row := r.db.QueryRowContext(ctx, r.lookupQuery, ip.String())
|
||||
|
||||
var (
|
||||
resolvedIP string
|
||||
country, region sql.NullString
|
||||
city sql.NullString
|
||||
latitude, longitude sql.NullFloat64
|
||||
)
|
||||
|
||||
if err := row.Scan(&resolvedIP, &country, ®ion, &city, &latitude, &longitude); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return Location{}, ErrNotFound
|
||||
}
|
||||
return Location{}, err
|
||||
}
|
||||
|
||||
return Location{
|
||||
IP: resolvedIP,
|
||||
Country: country.String,
|
||||
Region: region.String,
|
||||
City: city.String,
|
||||
Address: buildAddress(city.String, region.String, country.String),
|
||||
Latitude: latitude.Float64,
|
||||
Longitude: longitude.Float64,
|
||||
}, nil
|
||||
}
|
||||
@@ -12,7 +12,10 @@ func TestLookupValidIP(t *testing.T) {
|
||||
t.Skipf("mmdb not available at %s: %v", dbPath, err)
|
||||
}
|
||||
|
||||
resolver, err := NewResolver(dbPath)
|
||||
resolver, err := NewResolver(Config{
|
||||
Backend: BackendMMDB,
|
||||
MMDBPath: dbPath,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open db: %v", err)
|
||||
}
|
||||
@@ -26,10 +29,6 @@ func TestLookupValidIP(t *testing.T) {
|
||||
if loc.IP != "1.1.1.1" {
|
||||
t.Errorf("unexpected IP: %s", loc.IP)
|
||||
}
|
||||
// Ensure coordinates are populated for sanity.
|
||||
if loc.Latitude == 0 && loc.Longitude == 0 {
|
||||
t.Errorf("expected non-zero coordinates, got lat=%f lon=%f", loc.Latitude, loc.Longitude)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupInvalidIP(t *testing.T) {
|
||||
@@ -38,7 +37,10 @@ func TestLookupInvalidIP(t *testing.T) {
|
||||
t.Skipf("mmdb not available at %s: %v", dbPath, err)
|
||||
}
|
||||
|
||||
resolver, err := NewResolver(dbPath)
|
||||
resolver, err := NewResolver(Config{
|
||||
Backend: BackendMMDB,
|
||||
MMDBPath: dbPath,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open db: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user