55 "fmt"
66 "io"
77 "net/http"
8+ "os"
9+ "path/filepath"
10+ "runtime"
811 "time"
912
1013 "github.com/hashicorp/go-version"
@@ -13,13 +16,19 @@ import (
1316var (
1417 timeout = time .Second * 1
1518 apiFormat = "https://api.github.com/repos/%s/releases/latest"
19+ cacheTTL = 24 * time .Hour
1620)
1721
1822type releaseInfo struct {
1923 version string
2024 tarURL string
2125}
2226
27+ type cacheEntry struct {
28+ CheckedAt time.Time `json:"checked_at"`
29+ LatestVersion string `json:"latest_version"`
30+ }
31+
2332func fetchInfo (url string ) (* releaseInfo , error ) {
2433 httpClient := http.Client {Timeout : timeout }
2534 req , err := http .NewRequest (http .MethodGet , url , nil )
@@ -77,17 +86,83 @@ func compareVersions(current, latest string) (bool, error) {
7786// CheckForUpdate checks GitHub for a newer release and returns an update
7887// message if one is available. Returns an empty string if up-to-date or
7988// if the check fails.
89+ //
90+ // Results are cached for 24 hours to avoid hitting GitHub on every invocation.
91+ // The cache is stored at ~/.config/raystack/<repo>/state.json.
8092func CheckForUpdate (currentVersion , repo string ) string {
93+ // Check cache first.
94+ if latest , ok := readCache (repo ); ok {
95+ return buildMessage (currentVersion , latest )
96+ }
97+
98+ // Fetch from GitHub.
8199 releaseURL := fmt .Sprintf (apiFormat , repo )
82100 info , err := fetchInfo (releaseURL )
83101 if err != nil {
84102 return ""
85103 }
86104
87- isLatest , err := compareVersions (currentVersion , info .version )
105+ // Cache the result.
106+ writeCache (repo , info .version )
107+
108+ return buildMessage (currentVersion , info .version )
109+ }
110+
111+ func buildMessage (current , latest string ) string {
112+ isLatest , err := compareVersions (current , latest )
88113 if err != nil || isLatest {
89114 return ""
90115 }
116+ return fmt .Sprintf ("A new release (%s) is available. consider updating to latest version." , latest )
117+ }
118+
119+ func cachePath (repo string ) string {
120+ dir := configDir ()
121+ return filepath .Join (dir , "raystack" , repo , "state.json" )
122+ }
123+
124+ func readCache (repo string ) (string , bool ) {
125+ data , err := os .ReadFile (cachePath (repo ))
126+ if err != nil {
127+ return "" , false
128+ }
129+
130+ var entry cacheEntry
131+ if err := json .Unmarshal (data , & entry ); err != nil {
132+ return "" , false
133+ }
134+
135+ if time .Since (entry .CheckedAt ) > cacheTTL {
136+ return "" , false
137+ }
138+
139+ return entry .LatestVersion , true
140+ }
141+
142+ func writeCache (repo , latestVersion string ) {
143+ path := cachePath (repo )
144+ os .MkdirAll (filepath .Dir (path ), 0755 )
145+
146+ entry := cacheEntry {
147+ CheckedAt : time .Now (),
148+ LatestVersion : latestVersion ,
149+ }
150+ data , err := json .Marshal (entry )
151+ if err != nil {
152+ return
153+ }
154+ os .WriteFile (path , data , 0644 )
155+ }
91156
92- return fmt .Sprintf ("A new release (%s) is available. consider updating to latest version." , info .version )
157+ func configDir () string {
158+ if dir := os .Getenv ("XDG_CONFIG_HOME" ); dir != "" {
159+ return dir
160+ }
161+ if runtime .GOOS == "windows" {
162+ if dir := os .Getenv ("APPDATA" ); dir != "" {
163+ return dir
164+ }
165+ }
166+ home , _ := os .UserHomeDir ()
167+ return filepath .Join (home , ".config" )
93168}
0 commit comments