provider.go

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