135 lines
4.3 KiB
PHP
135 lines
4.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Setting;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
/**
|
|
* Resolves the newest published release tag for the configured channel and tells whether the
|
|
* installed version is behind it. Shared by the Versions page (which forces a fresh check on the
|
|
* explicit action) and by the global sidebar update badge + the scheduled refresh (which read /
|
|
* warm a short-lived cache so no page render or sidebar ever makes a network call).
|
|
*
|
|
* Anonymous + read-only against the public Git host's tag API — no token ever leaves the panel.
|
|
*/
|
|
class ReleaseChecker
|
|
{
|
|
private const CHANNELS = ['stable', 'beta'];
|
|
|
|
/** Cache the resolved tag long enough that the scheduled refresh keeps it continuously warm. */
|
|
private const TTL_MINUTES = 60;
|
|
|
|
private function cacheKey(string $channel): string
|
|
{
|
|
return 'clusev:latest-release:'.$channel;
|
|
}
|
|
|
|
public function channel(): string
|
|
{
|
|
$channel = (string) (Setting::get('release_channel', config('clusev.channel')) ?? 'stable');
|
|
|
|
return in_array($channel, self::CHANNELS, true) ? $channel : 'stable';
|
|
}
|
|
|
|
/** Last resolved latest tag (no network). Null when unknown. */
|
|
public function cachedLatest(?string $channel = null): ?string
|
|
{
|
|
$value = Cache::get($this->cacheKey($channel ?? $this->channel()));
|
|
|
|
return is_string($value) && $value !== '' ? $value : null;
|
|
}
|
|
|
|
/**
|
|
* Whether a newer release than the installed version is known from the cache (no network) — the
|
|
* single source for the sidebar badge. A stale cache below the installed version never counts.
|
|
*/
|
|
public function updateAvailable(): bool
|
|
{
|
|
$latest = $this->cachedLatest();
|
|
if ($latest === null) {
|
|
return false;
|
|
}
|
|
|
|
return version_compare(ltrim($latest, 'vV'), ltrim((string) config('clusev.version'), 'vV'), '>');
|
|
}
|
|
|
|
/**
|
|
* Force a fresh remote lookup, cache it, and return the newest tag for the channel. Null on any
|
|
* failure (offline, non-2xx, malformed) so callers degrade gracefully.
|
|
*/
|
|
public function refresh(?string $channel = null): ?string
|
|
{
|
|
$channel ??= $this->channel();
|
|
$tag = $this->fetchRemoteLatestTag($channel);
|
|
if ($tag !== null) {
|
|
Cache::put($this->cacheKey($channel), $tag, now()->addMinutes(self::TTL_MINUTES));
|
|
}
|
|
|
|
return $tag;
|
|
}
|
|
|
|
/**
|
|
* Newest tag from the public Git host's API (Gitea: /api/v1/repos/<owner>/<repo>/tags).
|
|
* Returns the version WITHOUT the leading "v" (e.g. "0.9.50"); null on any failure.
|
|
*/
|
|
public function fetchRemoteLatestTag(string $channel): ?string
|
|
{
|
|
$repo = (string) config('clusev.repository');
|
|
if (! preg_match('#^(https?://[^/]+)/([^/]+)/([^/]+?)(?:\.git)?/?$#', $repo, $m)) {
|
|
return null;
|
|
}
|
|
[, $base, $owner, $name] = $m;
|
|
|
|
try {
|
|
$res = Http::timeout(5)->acceptJson()
|
|
->get("{$base}/api/v1/repos/{$owner}/{$name}/tags", ['limit' => 50]);
|
|
|
|
if (! $res->successful()) {
|
|
return null;
|
|
}
|
|
|
|
$names = array_map(
|
|
static fn ($t): string => is_array($t) ? (string) ($t['name'] ?? '') : '',
|
|
(array) $res->json(),
|
|
);
|
|
|
|
return $this->newestVersion($names, $channel);
|
|
} catch (\Throwable $e) {
|
|
report($e);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Highest version among the tag names, honouring the channel (stable rejects prereleases).
|
|
* Returns the version without the "v" prefix.
|
|
*
|
|
* @param array<int, string> $tagNames
|
|
*/
|
|
public function newestVersion(array $tagNames, string $channel): ?string
|
|
{
|
|
$pattern = $channel === 'beta'
|
|
? '/^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?$/'
|
|
: '/^v?\d+\.\d+\.\d+$/';
|
|
|
|
$versions = [];
|
|
foreach (array_unique($tagNames) as $tag) {
|
|
$tag = trim((string) $tag);
|
|
if ($tag !== '' && preg_match($pattern, $tag)) {
|
|
$versions[] = ltrim($tag, 'vV');
|
|
}
|
|
}
|
|
|
|
if ($versions === []) {
|
|
return null;
|
|
}
|
|
|
|
usort($versions, fn (string $a, string $b): int => version_compare($b, $a));
|
|
|
|
return $versions[0];
|
|
}
|
|
}
|