using System.Net.Http;
using System.Text.Json;
namespace Elfisa.Core;
/// Результат проверки обновления.
public sealed class UpdateInfo
{
public bool Available { get; init; }
public string CurrentVersion { get; init; } = "";
public string LatestVersion { get; init; } = "";
public string ReleaseUrl { get; init; } = "";
public string SetupUrl { get; init; } = "";
}
///
/// Проверка и скачивание обновлений Avalonia-клиента.
/// Смотрит в Gitea Releases (тот же репозиторий, что и старый WinForms-апдейтер),
/// установщик тянет с CDN. Никаких токенов — репозиторий и CDN публичны.
///
public static class UpdateService
{
// Первичный источник — публичный CDN (тот же хост, откуда качается установщик;
// надёжнее приватных API и отдельных поддоменов). Fallback — Gitea Releases.
private const string LatestJsonUrl = "https://cdn.24pharmdata.ru/latest.json";
private const string ReleasesLatest =
"https://git.24pharmdata.ru/api/v1/repos/pharmdata/elfisa-pharmacy/releases/latest";
/// Стабильный URL установщика на CDN (всегда последняя версия).
public const string SetupUrl = "https://cdn.24pharmdata.ru/ElfisaPharmacy-Setup.exe";
/// Проверяет наличие новой версии. Возвращает null при офлайне/ошибке (не мешаем работе).
public static async Task CheckAsync(Version current)
{
var info = await TryCdnAsync(current).ConfigureAwait(false)
?? await TryGiteaAsync(current).ConfigureAwait(false);
Log(info is null
? $"current={current}: проверка не удалась (нет ответа CDN и Gitea)"
: $"current={current} latest={info.LatestVersion} available={info.Available}");
return info;
}
// CDN latest.json: {"version":"2.0.7","setup":"https://cdn.24pharmdata.ru/ElfisaPharmacy-Setup.exe"}
private static async Task TryCdnAsync(Version current)
{
try
{
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(8) };
var json = await http.GetStringAsync(LatestJsonUrl).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
var ver = root.TryGetProperty("version", out var v) ? v.GetString() : null;
var latest = ParseVersion(ver);
if (latest is null) return null;
var setup = root.TryGetProperty("setup", out var s) ? s.GetString() : null;
return new UpdateInfo
{
Available = latest > current,
CurrentVersion = current.ToString(),
LatestVersion = latest.ToString(),
ReleaseUrl = "",
SetupUrl = string.IsNullOrWhiteSpace(setup) ? SetupUrl : setup!,
};
}
catch (Exception ex) { Log("CDN latest.json недоступен: " + ex.Message); return null; }
}
private static async Task TryGiteaAsync(Version current)
{
try
{
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(8) };
http.DefaultRequestHeaders.Add("Accept", "application/json");
var json = await http.GetStringAsync(ReleasesLatest).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
var tag = root.TryGetProperty("tag_name", out var t) ? t.GetString() : null;
var latest = ParseVersion(tag);
if (latest is null) return null;
var htmlUrl = root.TryGetProperty("html_url", out var h) ? h.GetString() : null;
return new UpdateInfo
{
Available = latest > current,
CurrentVersion = current.ToString(),
LatestVersion = latest.ToString(),
ReleaseUrl = htmlUrl ?? "",
SetupUrl = SetupUrl,
};
}
catch (Exception ex) { Log("Gitea releases/latest недоступен: " + ex.Message); return null; }
}
/// Лог проверки в %TEMP%\elfisa-update.log — для диагностики на машине клиента.
private static void Log(string msg)
{
try
{
var path = Path.Combine(Path.GetTempPath(), "elfisa-update.log");
File.AppendAllText(path, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} {msg}{Environment.NewLine}");
}
catch { /* лог не критичен */ }
}
/// Скачивает установщик с CDN во временный файл. Возвращает путь.
public static async Task DownloadSetupAsync(IProgress? progress, CancellationToken ct = default)
{
var path = Path.Combine(Path.GetTempPath(), "ElfisaPharmacy-Setup.exe");
using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
using var resp = await http.GetAsync(SetupUrl, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
resp.EnsureSuccessStatusCode();
var total = resp.Content.Headers.ContentLength ?? -1;
await using var src = await resp.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
await using var dst = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
var buffer = new byte[81920];
long readTotal = 0;
int n;
while ((n = await src.ReadAsync(buffer, ct).ConfigureAwait(false)) > 0)
{
await dst.WriteAsync(buffer.AsMemory(0, n), ct).ConfigureAwait(false);
readTotal += n;
if (total > 0) progress?.Report(readTotal * 100.0 / total);
}
return path;
}
/// Разбирает тег вида v2.0.4 / 2.0.4 / 2.0.4-beta в Version.
private static Version? ParseVersion(string? tag)
{
if (string.IsNullOrWhiteSpace(tag)) return null;
tag = tag.Trim();
if (tag.StartsWith("v", StringComparison.OrdinalIgnoreCase)) tag = tag[1..];
var dash = tag.IndexOf('-');
if (dash > 0) tag = tag[..dash];
switch (tag.Split('.').Length)
{
case 1: tag += ".0.0.0"; break;
case 2: tag += ".0.0"; break;
case 3: tag += ".0"; break;
}
return Version.TryParse(tag, out var v) ? v : null;
}
}