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>
39 lines
1.2 KiB
PHP
39 lines
1.2 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace PSLauncher;
|
|
|
|
final class Response
|
|
{
|
|
public static function json($data, int $status = 200): void
|
|
{
|
|
http_response_code($status);
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Cache-Control: no-store');
|
|
echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
public static function error(string $code, string $message, int $status = 400, array $extra = []): void
|
|
{
|
|
self::json(array_merge(['error' => $code, 'message' => $message], $extra), $status);
|
|
}
|
|
|
|
public static function file(string $path, string $contentType, int $maxAgeSeconds = 0): void
|
|
{
|
|
if (!is_file($path)) {
|
|
self::error('not_found', 'File not found', 404);
|
|
}
|
|
header('Content-Type: ' . $contentType);
|
|
header('Content-Length: ' . filesize($path));
|
|
if ($maxAgeSeconds > 0) {
|
|
header(sprintf('Cache-Control: public, max-age=%d, must-revalidate', $maxAgeSeconds));
|
|
header('ETag: "' . md5_file($path) . '"');
|
|
} else {
|
|
header('Cache-Control: no-store');
|
|
}
|
|
readfile($path);
|
|
exit;
|
|
}
|
|
}
|