provider.go

  1package config
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"log/slog"
  7	"os"
  8	"path/filepath"
  9	"runtime"
 10	"sync"
 11	"time"
 12
 13	"github.com/charmbracelet/crush/internal/fur/client"
 14	"github.com/charmbracelet/crush/internal/fur/provider"
 15)
 16
 17type ProviderClient interface {
 18	GetProviders() ([]provider.Provider, error)
 19}
 20
 21var (
 22	providerOnce sync.Once
 23	providerList []provider.Provider
 24)
 25
 26// file to cache provider data
 27func providerCacheFileData() string {
 28	xdgDataHome := os.Getenv("XDG_DATA_HOME")
 29	if xdgDataHome != "" {
 30		return filepath.Join(xdgDataHome, appName, "providers.json")
 31	}
 32
 33	// return the path to the main data directory
 34	// for windows, it should be in `%LOCALAPPDATA%/crush/`
 35	// for linux and macOS, it should be in `$HOME/.local/share/crush/`
 36	if runtime.GOOS == "windows" {
 37		localAppData := os.Getenv("LOCALAPPDATA")
 38		if localAppData == "" {
 39			localAppData = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
 40		}
 41		return filepath.Join(localAppData, appName, "providers.json")
 42	}
 43
 44	return filepath.Join(os.Getenv("HOME"), ".local", "share", appName, "providers.json")
 45}
 46
 47func saveProvidersInCache(path string, providers []provider.Provider) error {
 48	slog.Info("Caching provider data")
 49	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
 50		return fmt.Errorf("failed to create directory for provider cache: %w", err)
 51	}
 52
 53	data, err := json.MarshalIndent(providers, "", "  ")
 54	if err != nil {
 55		return fmt.Errorf("failed to marshal provider data: %w", err)
 56	}
 57
 58	if err := os.WriteFile(path, data, 0o644); err != nil {
 59		return fmt.Errorf("failed to write provider data to cache: %w", err)
 60	}
 61	return nil
 62}
 63
 64func loadProvidersFromCache(path string) ([]provider.Provider, error) {
 65	data, err := os.ReadFile(path)
 66	if err != nil {
 67		return nil, fmt.Errorf("failed to read provider cache file: %w", err)
 68	}
 69
 70	var providers []provider.Provider
 71	if err := json.Unmarshal(data, &providers); err != nil {
 72		return nil, fmt.Errorf("failed to unmarshal provider data from cache: %w", err)
 73	}
 74	return providers, nil
 75}
 76
 77func Providers() ([]provider.Provider, error) {
 78	client := client.New()
 79	path := providerCacheFileData()
 80	return loadProvidersOnce(client, path)
 81}
 82
 83func loadProvidersOnce(client ProviderClient, path string) ([]provider.Provider, error) {
 84	var err error
 85	providerOnce.Do(func() {
 86		providerList, err = loadProviders(client, path)
 87	})
 88	if err != nil {
 89		return nil, err
 90	}
 91	return providerList, nil
 92}
 93
 94func loadProviders(client ProviderClient, path string) (providerList []provider.Provider, err error) {
 95	// if cache is not stale, load from it
 96	stale, exists := isCacheStale(path)
 97	if !stale {
 98		slog.Info("Using cached provider data")
 99		providerList, err = loadProvidersFromCache(path)
100		if len(providerList) > 0 && err == nil {
101			go func() {
102				slog.Info("Updating provider cache in background")
103				updated, uerr := client.GetProviders()
104				if len(updated) == 0 && uerr == nil {
105					_ = saveProvidersInCache(path, updated)
106				}
107			}()
108			return
109		}
110	}
111
112	slog.Info("Getting live provider data")
113	providerList, err = client.GetProviders()
114	if len(providerList) > 0 && err == nil {
115		err = saveProvidersInCache(path, providerList)
116		return
117	}
118	if !exists {
119		err = fmt.Errorf("failed to load providers")
120		return
121	}
122	providerList, err = loadProvidersFromCache(path)
123	return
124}
125
126func isCacheStale(path string) (stale, exists bool) {
127	info, err := os.Stat(path)
128	if err != nil {
129		return true, false
130	}
131	return time.Since(info.ModTime()) > 24*time.Hour, true
132}