thecr3w-dev/sdk/windows/TheCr3w.WindowsUpdate.Sdk/TheCr3wUpdater.cs

132 lines
5.2 KiB
C#

using System.Diagnostics;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.Json;
using TheCr3w.Shared.Crypto;
using TheCr3w.Shared.Models;
namespace TheCr3w.WindowsUpdate.Sdk;
public class UpdateCheckResult
{
public bool UpdateAvailable { get; set; }
public string AppId { get; set; } = string.Empty;
public Guid ReleaseId { get; set; }
public string LatestVersion { get; set; } = string.Empty;
public int VersionCode { get; set; }
public string ReleaseChannel { get; set; } = "stable";
public string? ReleaseNotes { get; set; }
public bool Mandatory { get; set; }
public long FileSize { get; set; }
public string Sha256 { get; set; } = string.Empty;
public string? SignatureEd25519 { get; set; }
public string? DownloadUrl { get; set; }
}
public class TheCr3wUpdaterOptions
{
public string ApiBaseUrl { get; set; } = "https://app.thecr3wnet.com";
public string AppId { get; set; } = string.Empty;
public string CurrentVersion { get; set; } = "1.0.0";
public int CurrentVersionCode { get; set; } = 1;
public string Channel { get; set; } = "stable";
public string Architecture { get; set; } = "x86_64";
public string TrustedPublicKeyHex { get; set; } = string.Empty;
}
public class TheCr3wUpdater
{
private readonly TheCr3wUpdaterOptions _options;
private readonly HttpClient _httpClient;
public TheCr3wUpdater(TheCr3wUpdaterOptions options, HttpClient? httpClient = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_httpClient = httpClient ?? new HttpClient { BaseAddress = new Uri(options.ApiBaseUrl) };
}
/// <summary>
/// Odpytuje Native Update API o dostępność nowszej wersji aplikacji.
/// </summary>
public async Task<UpdateCheckResult> CheckForUpdateAsync(CancellationToken cancellationToken = default)
{
var url = $"/api/v1/updates/check?app_id={Uri.EscapeDataString(_options.AppId)}" +
$"&current_version={Uri.EscapeDataString(_options.CurrentVersion)}" +
$"&version_code={_options.CurrentVersionCode}" +
$"&platform=windows" +
$"&architecture={Uri.EscapeDataString(_options.Architecture)}" +
$"&channel={Uri.EscapeDataString(_options.Channel)}";
var response = await _httpClient.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<UpdateCheckResult>(cancellationToken: cancellationToken);
return result ?? new UpdateCheckResult { UpdateAvailable = false };
}
/// <summary>
/// Pobiera plik instalacyjny z Pre-signed URL bezpośrednio z Cloudflare R2 i weryfikuje sumę SHA256.
/// </summary>
public async Task<string> DownloadAndVerifyAsync(
UpdateCheckResult update,
string destinationFilePath,
IProgress<double>? progress = null,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(update.DownloadUrl))
throw new InvalidOperationException("Brak adresu URL pobierania.");
using var response = await _httpClient.GetAsync(update.DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength ?? update.FileSize;
await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
await using var fileStream = new FileStream(destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
var buffer = new byte[8192];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
{
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
totalBytesRead += bytesRead;
if (totalBytes > 0 && progress != null)
{
progress.Report((double)totalBytesRead / totalBytes);
}
}
await fileStream.FlushAsync(cancellationToken);
fileStream.Close();
// Weryfikacja sumy kontrolnej SHA256
await using var verifyStream = File.OpenRead(destinationFilePath);
var computedSha256 = await Ed25519Service.ComputeSha256Async(verifyStream, cancellationToken);
if (!string.Equals(computedSha256, update.Sha256, StringComparison.OrdinalIgnoreCase))
{
File.Delete(destinationFilePath);
throw new CryptographicException($"Błąd integralności pliku! Wyliczony SHA256: {computedSha256}, Oczekiwany: {update.Sha256}");
}
return destinationFilePath;
}
/// <summary>
/// Uruchamia zewnętrzny instalator i zamyka bieżącą aplikację.
/// </summary>
public static void LaunchInstallerAndExit(string installerPath, string args = "")
{
var psi = new ProcessStartInfo
{
FileName = installerPath,
Arguments = args,
UseShellExecute = true
};
Process.Start(psi);
Environment.Exit(0);
}
}