elfisa-pharmacy/src/Elfisa.Core/UpdateService.cs
Exest 3d89fac6e0
Some checks failed
Desktop tests / test (push) Has been cancelled
тесты: автотесты десктопа (xUnit) + CI
- проект tests/Elfisa.Tests: 56 юнит-тестов на Elfisa.Core + Elfisa.Avalonia:
  версии апдейтера, срок JWT-сессии, ApiClient (URL/ошибки/ретраи/парсинг),
  DTO-контракты, черновики, корзина, дрилдаун прайса (МГ/базовое имя)
- тест-швы: internal-методы через InternalsVisibleTo, ApiClient(HttpMessageHandler)
- CI .github/workflows/desktop-tests.yml — dotnet test при пуше (GitHub/Gitea Actions)
- README с описанием покрытия

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 11:51:50 +03:00

143 lines
6.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Net.Http;
using System.Text.Json;
namespace Elfisa.Core;
/// <summary>Результат проверки обновления.</summary>
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; } = "";
}
/// <summary>
/// Проверка и скачивание обновлений Avalonia-клиента.
/// Смотрит в Gitea Releases (тот же репозиторий, что и старый WinForms-апдейтер),
/// установщик тянет с CDN. Никаких токенов — репозиторий и CDN публичны.
/// </summary>
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";
/// <summary>Стабильный URL установщика на CDN (всегда последняя версия).</summary>
public const string SetupUrl = "https://cdn.24pharmdata.ru/ElfisaPharmacy-Setup.exe";
/// <summary>Проверяет наличие новой версии. Возвращает null при офлайне/ошибке (не мешаем работе).</summary>
public static async Task<UpdateInfo?> 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<UpdateInfo?> 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<UpdateInfo?> 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; }
}
/// <summary>Лог проверки в %TEMP%\elfisa-update.log — для диагностики на машине клиента.</summary>
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 { /* лог не критичен */ }
}
/// <summary>Скачивает установщик с CDN во временный файл. Возвращает путь.</summary>
public static async Task<string> DownloadSetupAsync(IProgress<double>? 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;
}
/// <summary>Разбирает тег вида v2.0.4 / 2.0.4 / 2.0.4-beta в Version.</summary>
internal 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;
}
}