Initial scaffolding: PS_Launcher v0.2

Client (C# / .NET 8 / WPF, MVVM via CommunityToolkit.Mvvm):
- PSLauncher.App: WPF UI dark theme (Epic-style sidebar + hero + big play button)
  with UpdateAvailableDialog rendering Markdown release notes via Markdig.Wpf.
- PSLauncher.Core: services for installation registry (scans Proserve v{X.Y.Z}/),
  process launcher, manifest fetch, SHA-256 integrity, HTTP download with
  progress, ZIP install via .tmp + atomic rename, update orchestrator.
- PSLauncher.Models: RemoteManifest, InstalledVersion, LocalConfig DTOs.

Server (PHP 8 for OVH mutualisé, deployed under www/PS_Launcher/):
- Front controller + routes /manifest and /releasenotes/{version}.
- Static signed-manifest workflow with tools/sign-manifest.php CLI to
  recompute SHA-256 and sizeBytes after each ZIP upload.
- .htaccess: HTTPS redirect, rewrite, security headers.
- config.example.php template; real config.php is gitignored.

Cohabiting versions: each release lives in its own Proserve v{version}/ folder
under installRoot. Old versions are never deleted automatically.

Roadmap: v0.3 = HTTP Range resume + Polly retry + state.json,
v0.4 = MySQL license + Ed25519 signatures + DPAPI, v0.5 = settings/UX polish.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 08:54:45 +02:00
commit 1c8c6803e8
48 changed files with 2269 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using PSLauncher.Models;
namespace PSLauncher.Core.Manifests;
public sealed class ManifestService : IManifestService
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
private readonly HttpClient _http;
private readonly Func<string> _serverBaseUrlProvider;
private readonly ILogger<ManifestService> _logger;
public ManifestService(HttpClient http, Func<string> serverBaseUrlProvider, ILogger<ManifestService> logger)
{
_http = http;
_serverBaseUrlProvider = serverBaseUrlProvider;
_logger = logger;
}
public async Task<RemoteManifest> FetchAsync(CancellationToken ct)
{
var url = TrimSlash(_serverBaseUrlProvider()) + "/manifest";
_logger.LogInformation("Fetching manifest: {Url}", url);
using var resp = await _http.GetAsync(url, ct).ConfigureAwait(false);
resp.EnsureSuccessStatusCode();
var manifest = await resp.Content.ReadFromJsonAsync<RemoteManifest>(JsonOptions, ct).ConfigureAwait(false)
?? throw new InvalidOperationException("Empty manifest");
return manifest;
}
public async Task<string> FetchReleaseNotesAsync(string url, CancellationToken ct)
{
_logger.LogInformation("Fetching release notes: {Url}", url);
using var resp = await _http.GetAsync(url, ct).ConfigureAwait(false);
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
}
private static string TrimSlash(string s) => s.TrimEnd('/');
}