provider.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. package config
  2. import (
  3. "cmp"
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "log/slog"
  9. "os"
  10. "path/filepath"
  11. "runtime"
  12. "slices"
  13. "strings"
  14. "sync"
  15. "time"
  16. "github.com/charmbracelet/catwalk/pkg/catwalk"
  17. "github.com/charmbracelet/catwalk/pkg/embedded"
  18. "github.com/charmbracelet/crush/internal/agent/hyper"
  19. "github.com/charmbracelet/crush/internal/csync"
  20. "github.com/charmbracelet/crush/internal/home"
  21. "github.com/charmbracelet/x/etag"
  22. )
  23. type syncer[T any] interface {
  24. Get(context.Context) (T, error)
  25. }
  26. var (
  27. providerOnce sync.Once
  28. providerList []catwalk.Provider
  29. providerErr error
  30. )
  31. // file to cache provider data
  32. func cachePathFor(name string) string {
  33. xdgDataHome := os.Getenv("XDG_DATA_HOME")
  34. if xdgDataHome != "" {
  35. return filepath.Join(xdgDataHome, appName, name+".json")
  36. }
  37. // return the path to the main data directory
  38. // for windows, it should be in `%LOCALAPPDATA%/crush/`
  39. // for linux and macOS, it should be in `$HOME/.local/share/crush/`
  40. if runtime.GOOS == "windows" {
  41. localAppData := os.Getenv("LOCALAPPDATA")
  42. if localAppData == "" {
  43. localAppData = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
  44. }
  45. return filepath.Join(localAppData, appName, name+".json")
  46. }
  47. return filepath.Join(home.Dir(), ".local", "share", appName, name+".json")
  48. }
  49. // UpdateProviders updates the Catwalk providers list from a specified source.
  50. func UpdateProviders(pathOrURL string) error {
  51. var providers []catwalk.Provider
  52. pathOrURL = cmp.Or(pathOrURL, os.Getenv("CATWALK_URL"), defaultCatwalkURL)
  53. switch {
  54. case pathOrURL == "embedded":
  55. providers = embedded.GetAll()
  56. case strings.HasPrefix(pathOrURL, "http://") || strings.HasPrefix(pathOrURL, "https://"):
  57. var err error
  58. providers, err = catwalk.NewWithURL(pathOrURL).GetProviders(context.Background(), "")
  59. if err != nil {
  60. return fmt.Errorf("failed to fetch providers from Catwalk: %w", err)
  61. }
  62. default:
  63. content, err := os.ReadFile(pathOrURL)
  64. if err != nil {
  65. return fmt.Errorf("failed to read file: %w", err)
  66. }
  67. if err := json.Unmarshal(content, &providers); err != nil {
  68. return fmt.Errorf("failed to unmarshal provider data: %w", err)
  69. }
  70. if len(providers) == 0 {
  71. return fmt.Errorf("no providers found in the provided source")
  72. }
  73. }
  74. if err := newCache[[]catwalk.Provider](cachePathFor("providers")).Store(providers); err != nil {
  75. return fmt.Errorf("failed to save providers to cache: %w", err)
  76. }
  77. slog.Info("Providers updated successfully", "count", len(providers), "from", pathOrURL, "to", cachePathFor)
  78. return nil
  79. }
  80. // UpdateHyper updates the Hyper provider information from a specified URL.
  81. func UpdateHyper(pathOrURL string) error {
  82. if !hyper.Enabled() {
  83. return fmt.Errorf("hyper not enabled")
  84. }
  85. var provider catwalk.Provider
  86. pathOrURL = cmp.Or(pathOrURL, hyper.BaseURL())
  87. switch {
  88. case pathOrURL == "embedded":
  89. provider = hyper.Embedded()
  90. case strings.HasPrefix(pathOrURL, "http://") || strings.HasPrefix(pathOrURL, "https://"):
  91. client := realHyperClient{baseURL: pathOrURL}
  92. var err error
  93. provider, err = client.Get(context.Background(), "")
  94. if err != nil {
  95. return fmt.Errorf("failed to fetch provider from Hyper: %w", err)
  96. }
  97. default:
  98. content, err := os.ReadFile(pathOrURL)
  99. if err != nil {
  100. return fmt.Errorf("failed to read file: %w", err)
  101. }
  102. if err := json.Unmarshal(content, &provider); err != nil {
  103. return fmt.Errorf("failed to unmarshal provider data: %w", err)
  104. }
  105. }
  106. if err := newCache[catwalk.Provider](cachePathFor("hyper")).Store(provider); err != nil {
  107. return fmt.Errorf("failed to save Hyper provider to cache: %w", err)
  108. }
  109. slog.Info("Hyper provider updated successfully", "from", pathOrURL, "to", cachePathFor("hyper"))
  110. return nil
  111. }
  112. var (
  113. catwalkSyncer = &catwalkSync{}
  114. hyperSyncer = &hyperSync{}
  115. )
  116. // Providers returns the list of providers, taking into account cached results
  117. // and whether or not auto update is enabled.
  118. //
  119. // It will:
  120. // 1. if auto update is disabled, it'll return the embedded providers at the
  121. // time of release.
  122. // 2. load the cached providers
  123. // 3. try to get the fresh list of providers, and return either this new list,
  124. // the cached list, or the embedded list if all others fail.
  125. func Providers(cfg *Config) ([]catwalk.Provider, error) {
  126. providerOnce.Do(func() {
  127. var wg sync.WaitGroup
  128. var errs []error
  129. providers := csync.NewSlice[catwalk.Provider]()
  130. autoupdate := !cfg.Options.DisableProviderAutoUpdate
  131. ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
  132. defer cancel()
  133. wg.Go(func() {
  134. catwalkURL := cmp.Or(os.Getenv("CATWALK_URL"), defaultCatwalkURL)
  135. client := catwalk.NewWithURL(catwalkURL)
  136. path := cachePathFor("providers")
  137. catwalkSyncer.Init(client, path, autoupdate)
  138. items, err := catwalkSyncer.Get(ctx)
  139. if err != nil {
  140. catwalkURL := fmt.Sprintf("%s/v2/providers", cmp.Or(os.Getenv("CATWALK_URL"), defaultCatwalkURL))
  141. errs = append(errs, fmt.Errorf("Crush was unable to fetch an updated list of providers from %s. Consider setting CRUSH_DISABLE_PROVIDER_AUTO_UPDATE=1 to use the embedded providers bundled at the time of this Crush release. You can also update providers manually. For more info see crush update-providers --help.\n\nCause: %w", catwalkURL, providerErr)) //nolint:staticcheck
  142. return
  143. }
  144. providers.Append(items...)
  145. })
  146. wg.Go(func() {
  147. if !hyper.Enabled() {
  148. return
  149. }
  150. path := cachePathFor("hyper")
  151. hyperSyncer.Init(realHyperClient{baseURL: hyper.BaseURL()}, path, autoupdate)
  152. item, err := hyperSyncer.Get(ctx)
  153. if err != nil {
  154. errs = append(errs, fmt.Errorf("Crush was unable to fetch updated information from Hyper: %w", err)) //nolint:staticcheck
  155. return
  156. }
  157. providers.Append(item)
  158. })
  159. wg.Wait()
  160. providerList = slices.Collect(providers.Seq())
  161. providerErr = errors.Join(errs...)
  162. })
  163. return providerList, providerErr
  164. }
  165. type cache[T any] struct {
  166. path string
  167. }
  168. func newCache[T any](path string) cache[T] {
  169. return cache[T]{path: path}
  170. }
  171. func (c cache[T]) Get() (T, string, error) {
  172. var v T
  173. data, err := os.ReadFile(c.path)
  174. if err != nil {
  175. return v, "", fmt.Errorf("failed to read provider cache file: %w", err)
  176. }
  177. if err := json.Unmarshal(data, &v); err != nil {
  178. return v, "", fmt.Errorf("failed to unmarshal provider data from cache: %w", err)
  179. }
  180. return v, etag.Of(data), nil
  181. }
  182. func (c cache[T]) Store(v T) error {
  183. slog.Info("Saving provider data to disk", "path", c.path)
  184. if err := os.MkdirAll(filepath.Dir(c.path), 0o755); err != nil {
  185. return fmt.Errorf("failed to create directory for provider cache: %w", err)
  186. }
  187. data, err := json.Marshal(v)
  188. if err != nil {
  189. return fmt.Errorf("failed to marshal provider data: %w", err)
  190. }
  191. if err := os.WriteFile(c.path, data, 0o644); err != nil {
  192. return fmt.Errorf("failed to write provider data to cache: %w", err)
  193. }
  194. return nil
  195. }