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