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)
16
17type ProviderClient interface {
18 GetProviders() ([]catwalk.Provider, error)
19}
20
21var (
22 providerOnce sync.Once
23 providerList []catwalk.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 []catwalk.Provider) error {
48 slog.Info("Saving cached provider data", "path", path)
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) ([]catwalk.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 []catwalk.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() ([]catwalk.Provider, error) {
78 catwalkURL := cmp.Or(os.Getenv("CATWALK_URL"), defaultCatwalkURL)
79 client := catwalk.NewWithURL(catwalkURL)
80 path := providerCacheFileData()
81 return loadProvidersOnce(client, path)
82}
83
84func loadProvidersOnce(client ProviderClient, path string) ([]catwalk.Provider, error) {
85 var err error
86 providerOnce.Do(func() {
87 providerList, err = loadProviders(client, path)
88 })
89 if err != nil {
90 return nil, err
91 }
92 return providerList, nil
93}
94
95func loadProviders(client ProviderClient, path string) (providerList []catwalk.Provider, err error) {
96 // if cache is not stale, load from it
97 stale, exists := isCacheStale(path)
98 if !stale {
99 slog.Info("Using cached provider data", "path", path)
100 providerList, err = loadProvidersFromCache(path)
101 if len(providerList) > 0 && err == nil {
102 go func() {
103 slog.Info("Updating provider cache in background", "path", path)
104 updated, uerr := client.GetProviders()
105 if len(updated) > 0 && uerr == nil {
106 _ = saveProvidersInCache(path, updated)
107 }
108 }()
109 return
110 }
111 }
112
113 slog.Info("Getting live provider data", "path", path)
114 providerList, err = client.GetProviders()
115 if len(providerList) > 0 && err == nil {
116 err = saveProvidersInCache(path, providerList)
117 return
118 }
119 if !exists {
120 err = fmt.Errorf("failed to load providers")
121 return
122 }
123 slog.Info("Loading provider data from cache", "path", path)
124 providerList, err = loadProvidersFromCache(path)
125 return
126}
127
128func isCacheStale(path string) (stale, exists bool) {
129 info, err := os.Stat(path)
130 if err != nil {
131 return true, false
132 }
133 return time.Since(info.ModTime()) > 24*time.Hour, true
134}