144 lines
3.6 KiB
Go
144 lines
3.6 KiB
Go
package update
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/mathismqn/godeez/internal/buildinfo"
|
|
"github.com/mathismqn/godeez/internal/fsutil"
|
|
)
|
|
|
|
const noCheckEnv = "GODEEZ_NO_UPDATE_CHECK"
|
|
|
|
const (
|
|
// cacheTTL keeps the check to roughly once a day, which is often enough
|
|
// to notice a release without hitting the GitHub API on every command.
|
|
cacheTTL = 24 * time.Hour
|
|
|
|
// checkTimeout is deliberately short. The check is a courtesy running
|
|
// alongside a download, so it gives up quickly rather than delaying
|
|
// anything the user actually asked for.
|
|
checkTimeout = 3 * time.Second
|
|
)
|
|
|
|
type cacheEntry struct {
|
|
CheckedAt time.Time `json:"checked_at"`
|
|
LatestVersion string `json:"latest_version"`
|
|
}
|
|
|
|
func cachePath() (string, error) {
|
|
dir, err := os.UserCacheDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Join(dir, "godeez", "update.json"), nil
|
|
}
|
|
|
|
// readCache returns the cached result, or false if there is nothing usable.
|
|
// Every failure, including a corrupt or unreadable file, is reported the same
|
|
// way: the caller simply checks again, so there is nothing to distinguish.
|
|
func readCache() (cacheEntry, bool) {
|
|
path, err := cachePath()
|
|
if err != nil {
|
|
return cacheEntry{}, false
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return cacheEntry{}, false
|
|
}
|
|
|
|
var entry cacheEntry
|
|
if err := json.Unmarshal(data, &entry); err != nil {
|
|
return cacheEntry{}, false
|
|
}
|
|
if entry.LatestVersion == "" || time.Since(entry.CheckedAt) > cacheTTL {
|
|
return cacheEntry{}, false
|
|
}
|
|
|
|
return entry, true
|
|
}
|
|
|
|
func writeCache(version string) error {
|
|
path, err := cachePath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := fsutil.EnsureDir(filepath.Dir(path)); err != nil {
|
|
return err
|
|
}
|
|
|
|
data, err := json.Marshal(cacheEntry{CheckedAt: time.Now(), LatestVersion: version})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(path, data, 0644)
|
|
}
|
|
|
|
// check returns the latest version if it is newer than the running one, or
|
|
// "" if it is not. The cache is written even when the release turns out not
|
|
// to be newer, since the point is to record that GitHub was asked recently,
|
|
// and a failure to write it is ignored: an uncacheable check still works, it
|
|
// just repeats.
|
|
func check(ctx context.Context) (string, error) {
|
|
if entry, ok := readCache(); ok {
|
|
return latestIfNewer(entry.LatestVersion), nil
|
|
}
|
|
|
|
release, err := New().Latest(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
latest := release.Version()
|
|
_ = writeCache(latest)
|
|
|
|
return latestIfNewer(latest), nil
|
|
}
|
|
|
|
func latestIfNewer(latest string) string {
|
|
if IsNewer(buildinfo.Version(), latest) {
|
|
return latest
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// StartCheck begins a background update check and returns a channel that
|
|
// yields the newer version, if there is one, and is closed either way.
|
|
//
|
|
// It runs concurrently so the check never delays the command the user ran,
|
|
// and the channel is buffered so the goroutine exits even if nobody reads the
|
|
// result. Errors are swallowed: a failed check is not something to report.
|
|
//
|
|
// The check is skipped entirely for development builds, which have no version
|
|
// to compare, and whenever GODEEZ_NO_UPDATE_CHECK is set, which is the escape
|
|
// hatch for packagers and offline use. Both cases close the channel
|
|
// immediately so callers need no special handling.
|
|
func StartCheck(ctx context.Context) <-chan string {
|
|
ch := make(chan string, 1)
|
|
|
|
if os.Getenv(noCheckEnv) != "" || buildinfo.IsDev() {
|
|
close(ch)
|
|
return ch
|
|
}
|
|
|
|
go func() {
|
|
defer close(ch)
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, checkTimeout)
|
|
defer cancel()
|
|
|
|
if latest, err := check(ctx); err == nil && latest != "" {
|
|
ch <- latest
|
|
}
|
|
}()
|
|
|
|
return ch
|
|
}
|