Files
OpenNest/OpenNest.Server/UpdateRoutes.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

33 lines
1.3 KiB
C#

namespace OpenNest.Server;
/// <summary>
/// <c>/updates/{channel}/</c>: the desktop app's Velopack feed, mirrored from GitHub by
/// <see cref="UpdateMirror"/>. Velopack's <c>SimpleWebSource</c> reads
/// <c>releases.win.json</c> (ignoring the query it appends) and then the package it names. A
/// channel with nothing mirrored yet answers 503 so clients fall back to GitHub for that check.
/// </summary>
internal static class UpdateRoutes
{
public static void MapUpdates(this IEndpointRouteBuilder app)
{
app.MapGet("/api/updates", (UpdateMirror mirror) => Results.Ok(mirror.GetStatus()));
app.MapGet("/updates/{channel}/{file}", (string channel, string file, UpdateMirror mirror) =>
{
if (!UpdateMirror.IsChannel(channel))
return Results.NotFound();
if (file == UpdateMirror.FeedFileName)
{
return mirror.GetFeed(channel) is { } feed
? Results.Bytes(feed, "application/json")
: Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
return mirror.GetPackagePath(channel, file) is { } path
? Results.File(path, "application/octet-stream", enableRangeProcessing: true)
: Results.NotFound();
});
}
}