package main import ( "encoding/json" "io/ioutil" "net/http" "regexp" "time" "golang.org/x/mod/semver" ) const updateCheckURL = "https://api.github.com/repos/knadh/listmonk/releases/latest" type remoteUpdateResp struct { Version string `json:"tag_name"` URL string `json:"html_url"` } // AppUpdate contains information of a new update available to the app that // is sent to the frontend. type AppUpdate struct { Version string `json:"version"` URL string `json:"url"` } var reSemver = regexp.MustCompile(`-(.*)`) // checkUpdates is a blocking function that checks for updates to the app // at the given intervals. On detecting a new update (new semver), it // sets the global update status that renders a prompt on the UI. func checkUpdates(curVersion string, interval time.Duration, app *App) { // Strip -* suffix. curVersion = reSemver.ReplaceAllString(curVersion, "") time.Sleep(time.Second * 1) ticker := time.NewTicker(interval) defer ticker.Stop() for ; true; <-ticker.C { resp, err := http.Get(updateCheckURL) if err != nil { app.log.Printf("error checking for remote update: %v", err) continue } if resp.StatusCode != 200 { app.log.Printf("non 200 response on remote update check: %d", resp.StatusCode) continue } b, err := ioutil.ReadAll(resp.Body) if err != nil { app.log.Printf("error reading remote update payload: %v", err) continue } resp.Body.Close() var up remoteUpdateResp if err := json.Unmarshal(b, &up); err != nil { app.log.Printf("error unmarshalling remote update payload: %v", err) continue } // There is an update. Set it on the global app state. if semver.IsValid(up.Version) { v := reSemver.ReplaceAllString(up.Version, "") if semver.Compare(v, curVersion) > 0 { app.Lock() app.update = &AppUpdate{ Version: up.Version, URL: up.URL, } app.Unlock() app.log.Printf("new update %s found", up.Version) } } } }