@@ -210,36 +210,67 @@ func (s *BrewService) GetPackages() (packages *[]models.Package) {
210210// SetupData initializes the BrewService by loading installed packages, remote formulae, casks, and analytics data.
211211func (s * BrewService ) SetupData (forceDownload bool ) (err error ) {
212212 // Load formulae
213- if err = s .loadInstalled (); err != nil {
213+ if err = s .loadInstalled (forceDownload ); err != nil {
214214 return fmt .Errorf ("failed to load installed formulae: %w" , err )
215215 }
216216
217217 if err = s .loadRemote (forceDownload ); err != nil {
218218 return fmt .Errorf ("failed to load remote formulae: %w" , err )
219219 }
220220
221- if err = s .loadAnalytics (); err != nil {
221+ if err = s .loadAnalytics (forceDownload ); err != nil {
222222 return fmt .Errorf ("failed to load formulae analytics: %w" , err )
223223 }
224224
225225 // Load casks
226- if err = s .loadInstalledCasks (); err != nil {
226+ if err = s .loadInstalledCasks (forceDownload ); err != nil {
227227 return fmt .Errorf ("failed to load installed casks: %w" , err )
228228 }
229229
230230 if err = s .loadRemoteCasks (forceDownload ); err != nil {
231231 return fmt .Errorf ("failed to load remote casks: %w" , err )
232232 }
233233
234- if err = s .loadCaskAnalytics (); err != nil {
234+ if err = s .loadCaskAnalytics (forceDownload ); err != nil {
235235 return fmt .Errorf ("failed to load cask analytics: %w" , err )
236236 }
237237
238238 return nil
239239}
240240
241- // loadInstalled retrieves the list of installed Homebrew formulae and updates their local paths.
242- func (s * BrewService ) loadInstalled () (err error ) {
241+ // loadInstalled retrieves installed formulae, optionally using cache.
242+ func (s * BrewService ) loadInstalled (forceDownload bool ) (err error ) {
243+ cacheDir := getCacheDir ()
244+ installedFile := filepath .Join (cacheDir , "installed.json" )
245+ if _ , err := os .Stat (cacheDir ); os .IsNotExist (err ) {
246+ if err := os .MkdirAll (cacheDir , 0750 ); err != nil {
247+ return err
248+ }
249+ }
250+
251+ // Check if we should use the cached file
252+ if ! forceDownload {
253+ if fileInfo , err := os .Stat (installedFile ); err == nil {
254+ // Only use cache if file is not empty (size > 10 bytes for "[]" or "{}")
255+ if fileInfo .Size () > 10 {
256+ // #nosec G304 -- installedFile path is safely constructed from UserHomeDir and sanitized with filepath.Join
257+ data , err := os .ReadFile (installedFile )
258+ if err == nil && len (data ) > 0 {
259+ * s .installed = make ([]models.Formula , 0 )
260+ if err := json .Unmarshal (data , & s .installed ); err == nil {
261+ // Mark all installed Packages as locally installed and set LocalPath
262+ prefix := s .GetPrefixPath ()
263+ for i := range * s .installed {
264+ (* s .installed )[i ].LocallyInstalled = true
265+ (* s .installed )[i ].LocalPath = filepath .Join (prefix , "Cellar" , (* s .installed )[i ].Name )
266+ }
267+ return nil
268+ }
269+ }
270+ }
271+ }
272+ }
273+
243274 cmd := exec .Command ("brew" , "info" , "--json=v1" , "--installed" )
244275 output , err := cmd .Output ()
245276 if err != nil {
@@ -259,11 +290,46 @@ func (s *BrewService) loadInstalled() (err error) {
259290 (* s .installed )[i ].LocalPath = filepath .Join (prefix , "Cellar" , (* s .installed )[i ].Name )
260291 }
261292
293+ // Cache the installed formulae data
294+ _ = os .WriteFile (installedFile , output , 0600 )
262295 return nil
263296}
264297
265- // loadInstalledCasks retrieves the list of installed Homebrew casks.
266- func (s * BrewService ) loadInstalledCasks () (err error ) {
298+ // loadInstalledCasks retrieves installed casks, optionally using cache.
299+ func (s * BrewService ) loadInstalledCasks (forceDownload bool ) (err error ) {
300+ cacheDir := getCacheDir ()
301+ installedCasksFile := filepath .Join (cacheDir , "installed-casks.json" )
302+ if _ , err := os .Stat (cacheDir ); os .IsNotExist (err ) {
303+ if err := os .MkdirAll (cacheDir , 0750 ); err != nil {
304+ return err
305+ }
306+ }
307+
308+ // Check if we should use the cached file
309+ if ! forceDownload {
310+ if fileInfo , err := os .Stat (installedCasksFile ); err == nil {
311+ // Only use cache if file is not empty (size > 10 bytes for minimal JSON)
312+ if fileInfo .Size () > 10 {
313+ // #nosec G304 -- installedCasksFile path is safely constructed from UserHomeDir and sanitized with filepath.Join
314+ data , err := os .ReadFile (installedCasksFile )
315+ if err == nil && len (data ) > 0 {
316+ var response struct {
317+ Casks []models.Cask `json:"casks"`
318+ }
319+ if err := json .Unmarshal (data , & response ); err == nil {
320+ * s .installedCasks = response .Casks
321+ // Mark all installed casks as locally installed
322+ for i := range * s .installedCasks {
323+ (* s .installedCasks )[i ].LocallyInstalled = true
324+ (* s .installedCasks )[i ].IsCask = true
325+ }
326+ return nil
327+ }
328+ }
329+ }
330+ }
331+ }
332+
267333 // Get list of installed cask names
268334 listCmd := exec .Command ("brew" , "list" , "--cask" )
269335 listOutput , err := listCmd .Output ()
@@ -307,6 +373,8 @@ func (s *BrewService) loadInstalledCasks() (err error) {
307373 (* s .installedCasks )[i ].IsCask = true
308374 }
309375
376+ // Cache the installed casks data
377+ _ = os .WriteFile (installedCasksFile , infoOutput , 0600 )
310378 return nil
311379}
312380
@@ -322,13 +390,16 @@ func (s *BrewService) loadRemote(forceDownload bool) (err error) {
322390
323391 // Check if we should use the cached file
324392 if ! forceDownload {
325- if _ , err := os .Stat (formulaFile ); err == nil {
326- // #nosec G304 -- formulaFile path is safely constructed from UserHomeDir and sanitized with filepath.Join
327- data , err := os .ReadFile (formulaFile )
328- if err == nil {
329- * s .remote = make ([]models.Formula , 0 )
330- if err := json .Unmarshal (data , & s .remote ); err == nil {
331- return nil
393+ if fileInfo , err := os .Stat (formulaFile ); err == nil {
394+ // Only use cache if file has reasonable size (formulae list should be several MB)
395+ if fileInfo .Size () > 1000 {
396+ // #nosec G304 -- formulaFile path is safely constructed from UserHomeDir and sanitized with filepath.Join
397+ data , err := os .ReadFile (formulaFile )
398+ if err == nil && len (data ) > 0 {
399+ * s .remote = make ([]models.Formula , 0 )
400+ if err := json .Unmarshal (data , & s .remote ); err == nil && len (* s .remote ) > 0 {
401+ return nil
402+ }
332403 }
333404 }
334405 }
@@ -368,13 +439,16 @@ func (s *BrewService) loadRemoteCasks(forceDownload bool) (err error) {
368439
369440 // Check if we should use the cached file
370441 if ! forceDownload {
371- if _ , err := os .Stat (caskFile ); err == nil {
372- // #nosec G304 -- caskFile path is safely constructed from UserHomeDir and sanitized with filepath.Join
373- data , err := os .ReadFile (caskFile )
374- if err == nil {
375- * s .remoteCasks = make ([]models.Cask , 0 )
376- if err := json .Unmarshal (data , & s .remoteCasks ); err == nil {
377- return nil
442+ if fileInfo , err := os .Stat (caskFile ); err == nil {
443+ // Only use cache if file has reasonable size (cask list should be several MB)
444+ if fileInfo .Size () > 1000 {
445+ // #nosec G304 -- caskFile path is safely constructed from UserHomeDir and sanitized with filepath.Join
446+ data , err := os .ReadFile (caskFile )
447+ if err == nil && len (data ) > 0 {
448+ * s .remoteCasks = make ([]models.Cask , 0 )
449+ if err := json .Unmarshal (data , & s .remoteCasks ); err == nil && len (* s .remoteCasks ) > 0 {
450+ return nil
451+ }
378452 }
379453 }
380454 }
@@ -402,16 +476,51 @@ func (s *BrewService) loadRemoteCasks(forceDownload bool) (err error) {
402476 return nil
403477}
404478
405- // loadAnalytics retrieves the analytics data for Homebrew formulae from the API.
406- func (s * BrewService ) loadAnalytics () (err error ) {
479+ // loadAnalytics retrieves the analytics data for Homebrew formulae from the API and caches them locally.
480+ func (s * BrewService ) loadAnalytics (forceDownload bool ) (err error ) {
481+ cacheDir := getCacheDir ()
482+ analyticsFile := filepath .Join (cacheDir , "analytics.json" )
483+ if _ , err := os .Stat (cacheDir ); os .IsNotExist (err ) {
484+ if err := os .MkdirAll (cacheDir , 0750 ); err != nil {
485+ return err
486+ }
487+ }
488+
489+ // Check if we should use the cached file
490+ if ! forceDownload {
491+ if fileInfo , err := os .Stat (analyticsFile ); err == nil {
492+ // Only use cache if file has reasonable size (analytics should be > 1KB)
493+ if fileInfo .Size () > 100 {
494+ // #nosec G304 -- analyticsFile path is safely constructed from UserHomeDir and sanitized with filepath.Join
495+ data , err := os .ReadFile (analyticsFile )
496+ if err == nil && len (data ) > 0 {
497+ analytics := models.Analytics {}
498+ if err := json .Unmarshal (data , & analytics ); err == nil && len (analytics .Items ) > 0 {
499+ analyticsByFormula := map [string ]models.AnalyticsItem {}
500+ for _ , f := range analytics .Items {
501+ analyticsByFormula [f .Formula ] = f
502+ }
503+ s .analytics = analyticsByFormula
504+ return nil
505+ }
506+ }
507+ }
508+ }
509+ }
510+
407511 resp , err := http .Get (AnalyticsAPIURL )
408512 if err != nil {
409513 return err
410514 }
411515 defer resp .Body .Close ()
412516
517+ body , err := io .ReadAll (resp .Body )
518+ if err != nil {
519+ return err
520+ }
521+
413522 analytics := models.Analytics {}
414- err = json .NewDecoder ( resp . Body ). Decode ( & analytics )
523+ err = json .Unmarshal ( body , & analytics )
415524 if err != nil {
416525 return err
417526 }
@@ -422,19 +531,61 @@ func (s *BrewService) loadAnalytics() (err error) {
422531 }
423532
424533 s .analytics = analyticsByFormula
534+
535+ // Cache the analytics data
536+ _ = os .WriteFile (analyticsFile , body , 0600 )
425537 return nil
426538}
427539
428- // loadCaskAnalytics retrieves the analytics data for Homebrew casks from the API.
429- func (s * BrewService ) loadCaskAnalytics () (err error ) {
540+ // loadCaskAnalytics retrieves the analytics data for Homebrew casks from the API and caches them locally.
541+ func (s * BrewService ) loadCaskAnalytics (forceDownload bool ) (err error ) {
542+ cacheDir := getCacheDir ()
543+ caskAnalyticsFile := filepath .Join (cacheDir , "cask-analytics.json" )
544+ if _ , err := os .Stat (cacheDir ); os .IsNotExist (err ) {
545+ if err := os .MkdirAll (cacheDir , 0750 ); err != nil {
546+ return err
547+ }
548+ }
549+
550+ // Check if we should use the cached file
551+ if ! forceDownload {
552+ if fileInfo , err := os .Stat (caskAnalyticsFile ); err == nil {
553+ // Only use cache if file has reasonable size (analytics should be > 1KB)
554+ if fileInfo .Size () > 100 {
555+ // #nosec G304 -- caskAnalyticsFile path is safely constructed from UserHomeDir and sanitized with filepath.Join
556+ data , err := os .ReadFile (caskAnalyticsFile )
557+ if err == nil && len (data ) > 0 {
558+ analytics := models.Analytics {}
559+ if err := json .Unmarshal (data , & analytics ); err == nil && len (analytics .Items ) > 0 {
560+ analyticsByCask := map [string ]models.AnalyticsItem {}
561+ for _ , c := range analytics .Items {
562+ // Cask analytics use the "cask" field instead of "formula"
563+ caskName := c .Cask
564+ if caskName != "" {
565+ analyticsByCask [caskName ] = c
566+ }
567+ }
568+ s .caskAnalytics = analyticsByCask
569+ return nil
570+ }
571+ }
572+ }
573+ }
574+ }
575+
430576 resp , err := http .Get (CaskAnalyticsAPIURL )
431577 if err != nil {
432578 return err
433579 }
434580 defer resp .Body .Close ()
435581
582+ body , err := io .ReadAll (resp .Body )
583+ if err != nil {
584+ return err
585+ }
586+
436587 analytics := models.Analytics {}
437- err = json .NewDecoder ( resp . Body ). Decode ( & analytics )
588+ err = json .Unmarshal ( body , & analytics )
438589 if err != nil {
439590 return err
440591 }
@@ -449,6 +600,9 @@ func (s *BrewService) loadCaskAnalytics() (err error) {
449600 }
450601
451602 s .caskAnalytics = analyticsByCask
603+
604+ // Cache the cask analytics data
605+ _ = os .WriteFile (caskAnalyticsFile , body , 0600 )
452606 return nil
453607}
454608
0 commit comments