unarr/internal/upgrade/cache.go
Deivid Soto 3e0f3a5a64
Some checks failed
Release / release (push) Failing after 0s
Release / docker (push) Has been skipped
Release / virustotal (push) Failing after 0s
feat(cli): upgrade command, rich status, and version cache
- Replace `upgrade` stub with real command (alias for `self-update`)
- Also register `update` as alias: `unarr update` works too
- Rewrite `status` to show full config, disk usage, daemon state, and
  update availability with colored sections
- Add version check cache (1h TTL) so `status` is instant on repeat runs
- Guard against division by zero on empty filesystems
- Guard against negative durations from clock skew
- Guard against stale PID via heartbeat recency check (2 min)
- Add comprehensive test coverage across agent, engine, upgrade, usenet,
  arr, library, mediaserver, and UI packages
- Improve Makefile coverage target to exclude cmd/ glue code
- Fix stream handler resource cleanup and ffprobe error handling
2026-03-31 22:05:43 +02:00

75 lines
1.8 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"`
}
// cacheFilePath returns the path to the version cache file.
func cacheFilePath() 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(cacheFilePath())
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 := cacheFilePath()
os.MkdirAll(filepath.Dir(path), 0o755)
// Best-effort write — ignore errors
tmp := path + ".tmp"
if err := os.WriteFile(tmp, data, 0o644); err != nil {
return
}
os.Rename(tmp, path)
}
// 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
}