Files
OpenNest/OpenNest.Server/UpdateMirrorService.cs
T
ajandClaude Opus 5.5 277ca36f42 feat(server): mirror the desktop update feed for Database-mode clients
Every installed copy checked GitHub Releases on its own, so a shop's
workstations shared GitHub's 60-requests-per-hour unauthenticated limit
per public address and each downloaded the ~90 MB package over the
internet. Shops large enough to hit that already run the nest server.

The server now mirrors the Velopack feed at /updates/{stable|dev}: one
GitHub API request per 30-minute refresh, each package downloaded once
and checked against the feed's size and SHA-256 before it is served. A
channel is mirrored only while clients ask for it, so stable-only shops
never download development builds. Database-mode clients read the
server first and fall back to GitHub when it has no feed (old server,
mirror off, not mirrored yet), downloading each package from whichever
source listed it. A hand-set feed URL still takes precedence.
OPENNEST_UPDATES=off turns the mirror off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:05:20 -04:00

42 lines
1.2 KiB
C#

namespace OpenNest.Server;
/// <summary>
/// Refreshes the <see cref="UpdateMirror"/> every <see cref="Interval"/>, and at once when a
/// client asks for a channel the mirror does not hold yet.
/// </summary>
public sealed class UpdateMirrorService : BackgroundService
{
public static readonly TimeSpan Interval = TimeSpan.FromMinutes(30);
private readonly UpdateMirror _mirror;
private readonly SemaphoreSlim _wake = new(0, 1);
public UpdateMirrorService(UpdateMirror mirror)
{
_mirror = mirror;
_mirror.RefreshRequested += Wake;
}
public override void Dispose()
{
_mirror.RefreshRequested -= Wake;
base.Dispose();
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await _mirror.RefreshAsync(stoppingToken);
try { await _wake.WaitAsync(Interval, stoppingToken); }
catch (OperationCanceledException) { return; }
}
}
private void Wake()
{
try { _wake.Release(); }
catch (SemaphoreFullException) { } // A wake is already pending.
}
}