Files
AudioCore/core/services/Updater.php

232 lines
8.7 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace Core\Services;
use Throwable;
class Updater
{
private const CACHE_TTL_SECONDS = 21600;
private const MANIFEST_URL = 'https://lab.codemunkeh.io/codemunkeh/AudioCore/raw/branch/main/update.json';
public static function currentVersion(): string
{
$data = require __DIR__ . '/../version.php';
if (is_array($data) && !empty($data['version'])) {
return (string)$data['version'];
}
return '0.0.0';
}
public static function getStatus(bool $force = false): array
{
$current = self::currentVersion();
$manifestUrl = self::MANIFEST_URL;
$channel = trim(Settings::get('update_channel', 'stable'));
if ($channel === '') {
$channel = 'stable';
}
$cache = self::readCache();
if (
!$force
&& is_array($cache)
&& (string)($cache['manifest_url'] ?? '') === $manifestUrl
&& (string)($cache['channel'] ?? '') === $channel
&& ((int)($cache['fetched_at'] ?? 0) + self::CACHE_TTL_SECONDS) > time()
) {
return $cache;
}
$status = [
'ok' => false,
'configured' => true,
'current_version' => $current,
'latest_version' => $current,
'update_available' => false,
'channel' => $channel,
'manifest_url' => $manifestUrl,
'error' => '',
'checked_at' => gmdate('c'),
'download_url' => '',
'changelog_url' => '',
'notes' => '',
'fetched_at' => time(),
];
try {
$raw = self::fetchManifest($manifestUrl);
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
throw new \RuntimeException('Manifest JSON is invalid.');
}
$release = self::pickReleaseForChannel($decoded, $channel);
$latest = trim((string)($release['version'] ?? ''));
if ($latest === '') {
throw new \RuntimeException('Manifest does not include a version for selected channel.');
}
$status['ok'] = true;
$status['latest_version'] = $latest;
$status['download_url'] = (string)($release['download_url'] ?? '');
$status['changelog_url'] = (string)($release['changelog_url'] ?? '');
$status['notes'] = (string)($release['notes'] ?? '');
$status['update_available'] = version_compare($latest, $current, '>');
} catch (Throwable $e) {
$status['error'] = $e->getMessage();
}
self::writeCache($status);
return $status;
}
private static function pickReleaseForChannel(array $manifest, string $channel): array
{
if (isset($manifest['channels']) && is_array($manifest['channels'])) {
$channels = $manifest['channels'];
if (isset($channels[$channel]) && is_array($channels[$channel])) {
return $channels[$channel];
}
if (isset($channels['stable']) && is_array($channels['stable'])) {
return $channels['stable'];
}
}
return $manifest;
}
private static function cachePath(): string
{
return __DIR__ . '/../../storage/update_cache.json';
}
private static function readCache(): ?array
{
try {
$db = Database::get();
if ($db) {
$stmt = $db->query("
SELECT checked_at, channel, manifest_url, current_version, latest_version,
is_update_available, ok, error_text, payload_json
FROM ac_update_checks
ORDER BY id DESC
LIMIT 1
");
$row = $stmt ? $stmt->fetch() : null;
if (is_array($row)) {
$payload = json_decode((string)($row['payload_json'] ?? ''), true);
if (is_array($payload)) {
$payload['channel'] = (string)($row['channel'] ?? ($payload['channel'] ?? 'stable'));
$payload['manifest_url'] = (string)($row['manifest_url'] ?? ($payload['manifest_url'] ?? ''));
$payload['current_version'] = (string)($row['current_version'] ?? ($payload['current_version'] ?? '0.0.0'));
$payload['latest_version'] = (string)($row['latest_version'] ?? ($payload['latest_version'] ?? '0.0.0'));
$payload['update_available'] = ((int)($row['is_update_available'] ?? 0) === 1);
$payload['ok'] = ((int)($row['ok'] ?? 0) === 1);
$payload['error'] = (string)($row['error_text'] ?? ($payload['error'] ?? ''));
$checkedAt = (string)($row['checked_at'] ?? '');
if ($checkedAt !== '') {
$payload['checked_at'] = $checkedAt;
$payload['fetched_at'] = strtotime($checkedAt) ?: ($payload['fetched_at'] ?? 0);
}
return $payload;
}
}
}
} catch (Throwable $e) {
}
$path = self::cachePath();
if (!is_file($path)) {
return null;
}
$raw = @file_get_contents($path);
if ($raw === false) {
return null;
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : null;
}
private static function writeCache(array $data): void
{
try {
$db = Database::get();
if ($db) {
$stmt = $db->prepare("
INSERT INTO ac_update_checks
(checked_at, channel, manifest_url, current_version, latest_version, is_update_available, ok, error_text, payload_json)
VALUES (NOW(), :channel, :manifest_url, :current_version, :latest_version, :is_update_available, :ok, :error_text, :payload_json)
");
$stmt->execute([
':channel' => (string)($data['channel'] ?? 'stable'),
':manifest_url' => (string)($data['manifest_url'] ?? ''),
':current_version' => (string)($data['current_version'] ?? '0.0.0'),
':latest_version' => (string)($data['latest_version'] ?? '0.0.0'),
':is_update_available' => !empty($data['update_available']) ? 1 : 0,
':ok' => !empty($data['ok']) ? 1 : 0,
':error_text' => (string)($data['error'] ?? ''),
':payload_json' => json_encode($data, JSON_UNESCAPED_SLASHES),
]);
}
} catch (Throwable $e) {
}
$path = self::cachePath();
@file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
private static function fetchManifest(string $manifestUrl): string
{
$ctx = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => 10,
'header' => "User-Agent: AudioCore-Updater/1.0\r\nAccept: application/json\r\n",
],
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$raw = @file_get_contents($manifestUrl, false, $ctx);
if (is_string($raw) && $raw !== '') {
return $raw;
}
if (!function_exists('curl_init')) {
throw new \RuntimeException('Unable to fetch manifest (file_get_contents failed and cURL is unavailable).');
}
$ch = curl_init($manifestUrl);
if ($ch === false) {
throw new \RuntimeException('Unable to fetch manifest (failed to initialize cURL).');
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'User-Agent: AudioCore-Updater/1.0',
'Accept: application/json',
]);
$body = curl_exec($ch);
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if (!is_string($body) || $body === '') {
$detail = $err !== '' ? $err : ('HTTP ' . $httpCode);
throw new \RuntimeException('Unable to fetch manifest via cURL: ' . $detail);
}
if ($httpCode >= 400) {
throw new \RuntimeException('Manifest request failed with HTTP ' . $httpCode . '.');
}
return $body;
}
}