mirror of
https://github.com/chubin/wttr.in
synced 2025-01-26 10:45:01 +00:00
218 lines
5.2 KiB
Go
218 lines
5.2 KiB
Go
package location
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/samonzeweb/godb"
|
|
"github.com/samonzeweb/godb/adapters/sqlite"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/zsefvlol/timezonemapper"
|
|
|
|
"github.com/chubin/wttr.in/internal/config"
|
|
"github.com/chubin/wttr.in/internal/types"
|
|
)
|
|
|
|
// Cache is an implemenation of DB/file-based cache.
|
|
//
|
|
// At the moment, it is an implementation for the location cache,
|
|
// but it should be generalized to cache everything.
|
|
type Cache struct {
|
|
config *config.Config
|
|
db *godb.DB
|
|
searcher *Searcher
|
|
indexField string
|
|
filesCacheDir string
|
|
}
|
|
|
|
// NewCache returns new cache reader for the specified config.
|
|
func NewCache(config *config.Config) (*Cache, error) {
|
|
var (
|
|
db *godb.DB
|
|
err error
|
|
)
|
|
|
|
if config.Geo.LocationCacheType == types.CacheTypeDB {
|
|
log.Debugln("using db for location cache")
|
|
db, err = godb.Open(sqlite.Adapter, config.Geo.LocationCacheDB)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Debugln("db file:", config.Geo.LocationCacheDB)
|
|
|
|
// Needed for "upsert" implementation in Put()
|
|
db.UseErrorParser()
|
|
}
|
|
|
|
return &Cache{
|
|
config: config,
|
|
db: db,
|
|
indexField: "name",
|
|
filesCacheDir: config.Geo.LocationCache,
|
|
searcher: NewSearcher(config),
|
|
}, nil
|
|
}
|
|
|
|
// Resolve returns location information for specified location.
|
|
// If the information is found in the cache, it is returned.
|
|
// If it is not found, the external service is queried,
|
|
// and the result is stored in the cache.
|
|
func (c *Cache) Resolve(location string) (*Location, error) {
|
|
location = normalizeLocationName(location)
|
|
|
|
loc, err := c.Read(location)
|
|
if !errors.Is(err, types.ErrNotFound) {
|
|
return loc, err
|
|
}
|
|
|
|
log.Debugln("geo/location: not found in cache:", location)
|
|
loc, err = c.searcher.Search(location)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
loc.Name = location
|
|
loc.Timezone = latLngToTimezoneString(loc.Lat, loc.Lon)
|
|
|
|
err = c.Put(location, loc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return loc, nil
|
|
}
|
|
|
|
// Read returns location information from the cache, if found,
|
|
// or types.ErrNotFound if not found. If the entry is found, but its format
|
|
// is invalid, types.ErrInvalidCacheEntry is returned.
|
|
func (c *Cache) Read(addr string) (*Location, error) {
|
|
if c.config.Geo.LocationCacheType == types.CacheTypeFiles {
|
|
return c.readFromCacheFile(addr)
|
|
}
|
|
|
|
return c.readFromCacheDB(addr)
|
|
}
|
|
|
|
func (c *Cache) readFromCacheFile(name string) (*Location, error) {
|
|
var (
|
|
fileLoc = struct {
|
|
Latitude float64 `json:"latitude"`
|
|
Longitude float64 `json:"longitude"`
|
|
Timezone string `json:"timezone"`
|
|
Address string `json:"address"`
|
|
}{}
|
|
location Location
|
|
)
|
|
|
|
bytes, err := os.ReadFile(c.cacheFile(name))
|
|
if err != nil {
|
|
return nil, types.ErrNotFound
|
|
}
|
|
err = json.Unmarshal(bytes, &fileLoc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// normalize name
|
|
name = strings.TrimSpace(
|
|
strings.TrimRight(
|
|
strings.TrimLeft(name, `"`), `"`))
|
|
|
|
timezone := fileLoc.Timezone
|
|
if timezone == "" {
|
|
timezone = timezonemapper.LatLngToTimezoneString(fileLoc.Latitude, fileLoc.Longitude)
|
|
}
|
|
|
|
location = Location{
|
|
Name: name,
|
|
Lat: fmt.Sprint(fileLoc.Latitude),
|
|
Lon: fmt.Sprint(fileLoc.Longitude),
|
|
Timezone: timezone,
|
|
Fullname: fileLoc.Address,
|
|
}
|
|
|
|
return &location, nil
|
|
}
|
|
|
|
func (c *Cache) readFromCacheDB(addr string) (*Location, error) {
|
|
result := Location{}
|
|
err := c.db.Select(&result).
|
|
Where(c.indexField+" = ?", addr).
|
|
Do()
|
|
|
|
if strings.Contains(fmt.Sprint(err), "no rows in result set") {
|
|
return nil, types.ErrNotFound
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("readFromCacheDB: %w", err)
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
func (c *Cache) Put(addr string, loc *Location) error {
|
|
log.Infoln("geo/location: storing in cache:", loc)
|
|
if c.config.Geo.IPCacheType == types.CacheTypeDB {
|
|
return c.putToCacheDB(loc)
|
|
}
|
|
|
|
return c.putToCacheFile(addr, loc)
|
|
}
|
|
|
|
func (c *Cache) putToCacheDB(loc *Location) error {
|
|
err := c.db.Insert(loc).Do()
|
|
if strings.Contains(fmt.Sprint(err), "UNIQUE constraint failed") {
|
|
return c.db.Update(loc).Do()
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (c *Cache) putToCacheFile(addr string, loc fmt.Stringer) error {
|
|
return os.WriteFile(c.cacheFile(addr), []byte(loc.String()), 0o600)
|
|
}
|
|
|
|
// cacheFile returns path to the cache entry for addr.
|
|
func (c *Cache) cacheFile(item string) string {
|
|
return path.Join(c.filesCacheDir, item)
|
|
}
|
|
|
|
// normalizeLocationName converts name into the standard location form
|
|
// with the following steps:
|
|
// - remove excessive spaces,
|
|
// - remove quotes,
|
|
// - convert to lover case.
|
|
func normalizeLocationName(name string) string {
|
|
name = strings.ReplaceAll(name, `"`, " ")
|
|
name = strings.ReplaceAll(name, `'`, " ")
|
|
name = strings.TrimSpace(name)
|
|
name = strings.Join(strings.Fields(name), " ")
|
|
|
|
return strings.ToLower(name)
|
|
}
|
|
|
|
// latLngToTimezoneString returns timezone for lat, lon,
|
|
// or an empty string if they are invalid.
|
|
func latLngToTimezoneString(lat, lon string) string {
|
|
latFloat, err := strconv.ParseFloat(lat, 64)
|
|
if err != nil {
|
|
log.Errorln("geoloc: latLngToTimezoneString:", err)
|
|
|
|
return ""
|
|
}
|
|
lonFloat, err := strconv.ParseFloat(lon, 64)
|
|
if err != nil {
|
|
log.Errorln("geoloc: latLngToTimezoneString:", err)
|
|
|
|
return ""
|
|
}
|
|
|
|
return timezonemapper.LatLngToTimezoneString(latFloat, lonFloat)
|
|
}
|