179 lines
4.1 KiB
Go
179 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
locationCacheFile = "location.json"
|
|
forecastCacheFile = "forecast.json"
|
|
)
|
|
|
|
type Cache struct {
|
|
dir string
|
|
}
|
|
|
|
type LocationCache struct {
|
|
Query string `json:"query"`
|
|
Location Location `json:"location"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type ForecastCache struct {
|
|
Location Location `json:"location"`
|
|
Unit string `json:"unit"`
|
|
Period ForecastPeriod `json:"period"`
|
|
FetchedAt time.Time `json:"fetched_at"`
|
|
}
|
|
|
|
func NewCache(dir string) *Cache {
|
|
return &Cache{dir: dir}
|
|
}
|
|
|
|
func (c *Cache) SaveLocation(query string, loc Location) error {
|
|
if err := c.ensureDir(); err != nil {
|
|
return err
|
|
}
|
|
payload := LocationCache{
|
|
Query: query,
|
|
Location: loc,
|
|
UpdatedAt: time.Now().UTC(),
|
|
}
|
|
if err := writeJSON(filepath.Join(c.dir, locationCacheFile), payload); err != nil {
|
|
return err
|
|
}
|
|
return c.ClearForecastAndRadar()
|
|
}
|
|
|
|
func (c *Cache) LoadLocation() (Location, bool, error) {
|
|
path := filepath.Join(c.dir, locationCacheFile)
|
|
var payload LocationCache
|
|
if err := readJSON(path, &payload); err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return Location{}, false, nil
|
|
}
|
|
return Location{}, false, err
|
|
}
|
|
return payload.Location, true, nil
|
|
}
|
|
|
|
func (c *Cache) UpdateLocation(loc Location) error {
|
|
path := filepath.Join(c.dir, locationCacheFile)
|
|
var payload LocationCache
|
|
if err := readJSON(path, &payload); err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("no cached location found to update")
|
|
}
|
|
return err
|
|
}
|
|
|
|
payload.Location = loc
|
|
payload.UpdatedAt = time.Now().UTC()
|
|
return writeJSON(path, payload)
|
|
}
|
|
|
|
func (c *Cache) SaveForecast(loc Location, unit string, period ForecastPeriod) error {
|
|
if err := c.ensureDir(); err != nil {
|
|
return err
|
|
}
|
|
payload := ForecastCache{
|
|
Location: loc,
|
|
Unit: unit,
|
|
Period: period,
|
|
FetchedAt: time.Now().UTC(),
|
|
}
|
|
return writeJSON(filepath.Join(c.dir, forecastCacheFile), payload)
|
|
}
|
|
|
|
func (c *Cache) LoadForecast(loc Location, unit string, ttl time.Duration) (ForecastPeriod, bool, error) {
|
|
path := filepath.Join(c.dir, forecastCacheFile)
|
|
var payload ForecastCache
|
|
if err := readJSON(path, &payload); err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return ForecastPeriod{}, false, nil
|
|
}
|
|
return ForecastPeriod{}, false, err
|
|
}
|
|
|
|
if payload.Unit != unit {
|
|
return ForecastPeriod{}, false, nil
|
|
}
|
|
if !sameLocation(payload.Location, loc) {
|
|
return ForecastPeriod{}, false, nil
|
|
}
|
|
if time.Since(payload.FetchedAt) > ttl {
|
|
return ForecastPeriod{}, false, nil
|
|
}
|
|
|
|
return payload.Period, true, nil
|
|
}
|
|
|
|
func (c *Cache) ensureDir() error {
|
|
if c.dir == "" {
|
|
return fmt.Errorf("cache directory is empty")
|
|
}
|
|
return os.MkdirAll(c.dir, 0o755)
|
|
}
|
|
|
|
func (c *Cache) Clear() error {
|
|
if c.dir == "" {
|
|
return fmt.Errorf("cache directory is empty")
|
|
}
|
|
if err := os.Remove(filepath.Join(c.dir, locationCacheFile)); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
return c.ClearForecastAndRadar()
|
|
}
|
|
|
|
func (c *Cache) ClearForecastAndRadar() error {
|
|
if c.dir == "" {
|
|
return fmt.Errorf("cache directory is empty")
|
|
}
|
|
if err := os.Remove(filepath.Join(c.dir, forecastCacheFile)); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
if err := os.Remove(filepath.Join(c.dir, "radar.gif")); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readJSON(path string, target any) error {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return json.Unmarshal(data, target)
|
|
}
|
|
|
|
func writeJSON(path string, value any) error {
|
|
data, err := json.MarshalIndent(value, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, data, 0o644)
|
|
}
|
|
|
|
func sameLocation(a, b Location) bool {
|
|
return a.Lat == b.Lat && a.Lon == b.Lon
|
|
}
|
|
|
|
func cacheRoot() (string, error) {
|
|
if override := os.Getenv("WEATHER_CACHE_DIR"); override != "" {
|
|
return override, nil
|
|
}
|
|
if base := os.Getenv("XDG_CACHE_HOME"); base != "" {
|
|
return filepath.Join(base, "weather"), nil
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(home, ".cache", "weather"), nil
|
|
}
|