weather/cache.go
2026-01-28 12:24:50 -05:00

151 lines
3.4 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(),
}
return writeJSON(filepath.Join(c.dir, locationCacheFile), payload)
}
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) 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
}
if err := os.Remove(filepath.Join(c.dir, forecastCacheFile)); 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
}