unarr/internal/upgrade/cache.go

79 lines
1.9 KiB
Go

package upgrade
import (
"context"
"encoding/json"
"os"
"path/filepath"
"time"
"github.com/torrentclaw/unarr/internal/config"
)
const cacheTTL = 1 * time.Hour
// versionCache is the on-disk structure for cached version checks.
type versionCache struct {
Version string `json:"version"`
CheckedAt time.Time `json:"checkedAt"`
}
// cacheFilePathFn returns the path to the version cache file.
// Overridable in tests to avoid polluting the real cache.
// NOTE: not safe for parallel tests — callers must not use t.Parallel().
var cacheFilePathFn = func() string {
return filepath.Join(config.DataDir(), "latest-version.json")
}
// ReadCachedVersion returns the cached latest version if it's fresh (< cacheTTL).
// Returns empty string if cache is missing, stale, or corrupt.
func ReadCachedVersion() string {
data, err := os.ReadFile(cacheFilePathFn())
if err != nil {
return ""
}
var c versionCache
if json.Unmarshal(data, &c) != nil {
return ""
}
if time.Since(c.CheckedAt) > cacheTTL {
return ""
}
return c.Version
}
// writeCachedVersion writes the latest version to the cache file.
func writeCachedVersion(version string) {
c := versionCache{
Version: version,
CheckedAt: time.Now(),
}
data, err := json.Marshal(c)
if err != nil {
return
}
path := cacheFilePathFn()
os.MkdirAll(filepath.Dir(path), 0o755)
// Best-effort write — ignore errors
tmp := path + ".tmp"
if err := os.WriteFile(tmp, data, 0o644); err != nil {
return
}
if os.Rename(tmp, path) != nil {
os.Remove(tmp)
}
}
// CheckLatestCached returns the latest version, using cache when fresh.
// If cache is stale, fetches from GitHub and updates the cache.
func CheckLatestCached(ctx context.Context) (version string, fromCache bool, err error) {
if cached := ReadCachedVersion(); cached != "" {
return cached, true, nil
}
v, err := fetchLatestVersion(ctx)
if err != nil {
return "", false, err
}
writeCachedVersion(v)
return v, false, nil
}