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; } }