elfisa-pharmacy/src/Elfisa.Core/UpdateService.cs
Exest 4e341f76c3 версия 2.0.7: надёжный автоапдейт (CDN) + кнопка проверки + без лимита листания
- UpdateService: проверка через публичный CDN latest.json (тот же хост, что и
  установщик — надёжнее поддомена git), fallback на Gitea, лог в %TEMP%\elfisa-update.log
- Настройки: кнопка «Проверить обновления» (ручной триггер + видимый результат)
- Прайс-лист: убран лимит показа (300 → весь прайс/все совпадения одним запросом)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 15:35:24 +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>
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;
}
}