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
|
|
|
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"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 18:49:44 +02:00
|
|
|
// 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 {
|
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
|
|
|
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 {
|
2026-04-06 18:49:44 +02:00
|
|
|
data, err := os.ReadFile(cacheFilePathFn())
|
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
|
|
|
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
|
|
|
|
|
}
|
2026-04-06 18:49:44 +02:00
|
|
|
path := cacheFilePathFn()
|
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
|
|
|
os.MkdirAll(filepath.Dir(path), 0o755)
|
|
|
|
|
// Best-effort write — ignore errors
|
|
|
|
|
tmp := path + ".tmp"
|
|
|
|
|
if err := os.WriteFile(tmp, data, 0o644); err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-06 18:49:44 +02:00
|
|
|
if os.Rename(tmp, path) != nil {
|
|
|
|
|
os.Remove(tmp)
|
|
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|