Skip to content

Commit a3b6b81

Browse files
authored
feat(12): cache formulajson (#13)
* moved update homebrew command * refactored brew service to support formulae cache file * fix copilot suggestion
1 parent 14e2048 commit a3b6b81

File tree

3 files changed

+112
-61
lines changed

3 files changed

+112
-61
lines changed

internal/services/app.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,27 +71,28 @@ func (s *AppService) GetApp() *tview.Application {
7171
}
7272

7373
func (s *AppService) Boot() (err error) {
74-
if err = s.BrewService.LoadAllFormulae(); err != nil {
74+
if s.brewVersion, err = s.BrewService.GetBrewVersion(); err != nil {
75+
// This error is critical, as we need Homebrew to function
76+
return fmt.Errorf("failed to get Homebrew version: %v", err)
77+
}
78+
79+
// Download and parse Homebrew formulae data
80+
if err = s.BrewService.SetupData(false); err != nil {
7581
return fmt.Errorf("failed to load Homebrew formulae: %v", err)
7682
}
7783

78-
s.packages = s.BrewService.GetAllFormulae()
84+
s.packages = s.BrewService.GetFormulae()
7985
*s.filteredPackages = *s.packages
8086

81-
if s.brewVersion, err = s.BrewService.GetCurrentBrewVersion(); err != nil {
82-
return fmt.Errorf("failed to get Homebrew version: %v", err)
83-
}
84-
8587
return nil
8688
}
8789

8890
func (s *AppService) updateHomeBrew() {
8991
s.layout.GetNotifier().ShowWarning("Updating Homebrew formulae...")
90-
if err := s.CommandService.UpdateHomebrew(); err != nil {
92+
if err := s.BrewService.UpdateHomebrew(); err != nil {
9193
s.layout.GetNotifier().ShowError("Could not update Homebrew formulae")
9294
return
9395
}
94-
9596
// Clear loading message and update results
9697
s.layout.GetNotifier().ShowSuccess("Homebrew formulae updated successfully")
9798
s.forceRefreshResults()
@@ -230,7 +231,7 @@ func (s *AppService) getPackageInstallationDetails(info *models.Formula) string
230231
return "[yellow::b]Installation[-]\nNot installed"
231232
}
232233

233-
packagePrefix, _ := s.BrewService.GetPrefixPath(info.Name)
234+
packagePrefix := s.BrewService.GetPrefixPath() + "/" + info.Name
234235
installedOnRequest := "No"
235236
if info.Installed[0].InstalledOnRequest {
236237
installedOnRequest = "Yes"
@@ -289,8 +290,11 @@ func (s *AppService) getAnalyticsInfo(info *models.Formula) string {
289290
}
290291

291292
func (s *AppService) forceRefreshResults() {
293+
_ = s.BrewService.SetupData(true)
294+
s.packages = s.BrewService.GetFormulae()
295+
*s.filteredPackages = *s.packages
296+
292297
s.app.QueueUpdateDraw(func() {
293-
_ = s.BrewService.LoadAllFormulae()
294298
s.search(s.layout.GetSearch().Field().GetText(), false)
295299
})
296300
}

internal/services/brew.go

Lines changed: 98 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,91 +3,86 @@ package services
33
import (
44
"bbrew/internal/models"
55
"encoding/json"
6+
"io"
67
"net/http"
8+
"os"
79
"os/exec"
10+
"path/filepath"
811
"sort"
912
"strconv"
1013
"strings"
11-
"sync"
1214
)
1315

14-
var prefixPathCache = make(map[string]string)
16+
const FormulaeAPIURL = "https://formulae.brew.sh/api/formula.json"
17+
const AnalyticsAPIURL = "https://formulae.brew.sh/api/analytics/install-on-request/90d.json"
1518

1619
type BrewServiceInterface interface {
17-
GetPrefixPath(packageName string) (path string, err error)
18-
GetAllFormulae() (formulae *[]models.Formula)
19-
LoadAllFormulae() (err error)
20-
GetCurrentBrewVersion() (version string, err error)
20+
GetPrefixPath() (path string)
21+
GetFormulae() (formulae *[]models.Formula)
22+
SetupData(forceDownload bool) (err error)
23+
GetBrewVersion() (version string, err error)
24+
UpdateHomebrew() error
2125
}
2226

2327
type BrewService struct {
24-
cache sync.Mutex
28+
// Package lists
2529
all *[]models.Formula
2630
installed *[]models.Formula
2731
remote *[]models.Formula
2832
analytics map[string]models.AnalyticsItem
33+
34+
brewVersion string
35+
prefixPath string
2936
}
3037

3138
var NewBrewService = func() BrewServiceInterface {
3239
return &BrewService{
33-
cache: sync.Mutex{},
3440
all: new([]models.Formula),
3541
installed: new([]models.Formula),
3642
remote: new([]models.Formula),
3743
}
3844
}
3945

40-
func (s *BrewService) GetPrefixPath(packageName string) (path string, err error) {
41-
s.cache.Lock()
42-
defer s.cache.Unlock()
43-
44-
var found bool
45-
if path, found = prefixPathCache[packageName]; found {
46-
return path, nil
46+
func (s *BrewService) GetPrefixPath() (path string) {
47+
if s.prefixPath != "" {
48+
return s.prefixPath
4749
}
4850

49-
cmd := exec.Command("brew", "--prefix", packageName)
51+
cmd := exec.Command("brew", "--prefix")
5052
output, err := cmd.Output()
5153
if err != nil {
52-
return "Unknown", err
54+
s.prefixPath = "Unknown"
55+
return
5356
}
5457

55-
path = strings.TrimSpace(string(output))
56-
prefixPathCache[packageName] = path
57-
return path, nil
58+
s.prefixPath = strings.TrimSpace(string(output))
59+
return s.prefixPath
5860
}
5961

60-
func (s *BrewService) GetAllFormulae() (formulae *[]models.Formula) {
61-
return s.all
62-
}
63-
64-
func (s *BrewService) LoadAllFormulae() (err error) {
65-
_ = s.loadInstalled()
66-
_ = s.loadRemote()
67-
_ = s.loadAnalytics()
68-
62+
func (s *BrewService) GetFormulae() (formulae *[]models.Formula) {
6963
packageMap := make(map[string]models.Formula)
7064

71-
// Add installed packages to the map
72-
for _, formula := range *s.installed {
73-
packageMap[formula.Name] = formula
74-
}
75-
7665
// Add remote packages to the map if they don't already exist
7766
for _, formula := range *s.remote {
7867
if _, exists := packageMap[formula.Name]; !exists {
7968
packageMap[formula.Name] = formula
8069
}
8170
}
8271

72+
// Add installed packages to the map
73+
for _, formula := range *s.installed {
74+
packageMap[formula.Name] = formula
75+
}
76+
8377
*s.all = make([]models.Formula, 0, len(packageMap))
8478
for _, formula := range packageMap {
85-
// patch analytics info
79+
// Merge analytics data if available
8680
if a, exists := s.analytics[formula.Name]; exists && a.Number > 0 {
8781
downloads, _ := strconv.Atoi(strings.ReplaceAll(a.Count, ",", ""))
8882
formula.Analytics90dRank = a.Number
8983
formula.Analytics90dDownloads = downloads
9084
}
85+
9186
*s.all = append(*s.all, formula)
9287
}
9388

@@ -96,6 +91,22 @@ func (s *BrewService) LoadAllFormulae() (err error) {
9691
return (*s.all)[i].Name < (*s.all)[j].Name
9792
})
9893

94+
return s.all
95+
}
96+
97+
func (s *BrewService) SetupData(forceDownload bool) (err error) {
98+
if err = s.loadInstalled(); err != nil {
99+
return err
100+
}
101+
102+
if err = s.loadRemote(forceDownload); err != nil {
103+
return err
104+
}
105+
106+
if err = s.loadAnalytics(); err != nil {
107+
return err
108+
}
109+
99110
return nil
100111
}
101112

@@ -120,24 +131,57 @@ func (s *BrewService) loadInstalled() (err error) {
120131
return nil
121132
}
122133

123-
func (s *BrewService) loadRemote() (err error) {
124-
resp, err := http.Get("https://formulae.brew.sh/api/formula.json")
134+
func (s *BrewService) loadRemote(forceDownload bool) (err error) {
135+
homeDir, err := os.UserHomeDir()
136+
if err != nil {
137+
return err
138+
}
139+
140+
bbrewDir := filepath.Join(homeDir, ".bbrew") // TODO: Move to config
141+
formulaFile := filepath.Join(bbrewDir, "formula.json")
142+
if _, err := os.Stat(bbrewDir); os.IsNotExist(err) {
143+
if err := os.MkdirAll(bbrewDir, 0755); err != nil {
144+
return err
145+
}
146+
}
147+
148+
// Check if we should use the cached file
149+
if !forceDownload {
150+
if _, err := os.Stat(formulaFile); err == nil {
151+
data, err := os.ReadFile(formulaFile)
152+
if err == nil {
153+
*s.remote = make([]models.Formula, 0)
154+
if err := json.Unmarshal(data, &s.remote); err == nil {
155+
return nil
156+
}
157+
}
158+
}
159+
}
160+
161+
resp, err := http.Get(FormulaeAPIURL)
125162
if err != nil {
126163
return err
127164
}
128165
defer resp.Body.Close()
129166

167+
body, err := io.ReadAll(resp.Body)
168+
if err != nil {
169+
return err
170+
}
171+
130172
*s.remote = make([]models.Formula, 0)
131-
err = json.NewDecoder(resp.Body).Decode(&s.remote)
173+
err = json.Unmarshal(body, s.remote)
132174
if err != nil {
133175
return err
134176
}
135177

178+
// Cache the remote formulae data
179+
_ = os.WriteFile(formulaFile, body, 0600)
136180
return nil
137181
}
138182

139183
func (s *BrewService) loadAnalytics() (err error) {
140-
resp, err := http.Get("https://formulae.brew.sh/api/analytics/install-on-request/90d.json")
184+
resp, err := http.Get(AnalyticsAPIURL)
141185
if err != nil {
142186
return err
143187
}
@@ -155,16 +199,28 @@ func (s *BrewService) loadAnalytics() (err error) {
155199
}
156200

157201
s.analytics = analyticsByFormula
158-
159202
return nil
160203
}
161204

162-
func (s *BrewService) GetCurrentBrewVersion() (version string, err error) {
205+
func (s *BrewService) GetBrewVersion() (version string, err error) {
206+
if s.brewVersion != "" {
207+
return s.brewVersion, nil
208+
}
209+
163210
cmd := exec.Command("brew", "--version")
164211
output, err := cmd.Output()
165212
if err != nil {
166213
return "", err
167214
}
168215

169-
return strings.TrimSpace(string(output)), nil
216+
s.brewVersion = strings.TrimSpace(string(output))
217+
return s.brewVersion, nil
218+
}
219+
220+
func (s *BrewService) UpdateHomebrew() error {
221+
cmd := exec.Command("brew", "update")
222+
if err := cmd.Run(); err != nil {
223+
return err
224+
}
225+
return nil
170226
}

internal/services/command.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ type CommandServiceInterface interface {
1414
UpdatePackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error
1515
RemovePackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error
1616
InstallPackage(info models.Formula, app *tview.Application, outputView *tview.TextView) error
17-
UpdateHomebrew() error
1817
}
1918

2019
type CommandService struct{}
@@ -43,14 +42,6 @@ func (s *CommandService) InstallPackage(info models.Formula, app *tview.Applicat
4342
return s.executeCommand(app, cmd, outputView)
4443
}
4544

46-
func (s *CommandService) UpdateHomebrew() error {
47-
cmd := exec.Command("brew", "update")
48-
if err := cmd.Run(); err != nil {
49-
return err
50-
}
51-
return nil
52-
}
53-
5445
func (s *CommandService) executeCommand(
5546
app *tview.Application,
5647
cmd *exec.Cmd,

0 commit comments

Comments
 (0)