mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
Merge remote-tracking branch 'origin/march-2023-03-21' into fix/2023-room-instances-publish-permissions
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using DorkNet.Server.Services;
|
||||
|
||||
namespace DorkNet.Server.Controllers.Site;
|
||||
|
||||
/// <summary>
|
||||
/// The client builds CI publishes, for the site's Downloads page and the
|
||||
/// Quest page's "latest build" install. Files are streamed from the downloads
|
||||
/// bucket (<see cref="DownloadsService"/>) with Range support, so the browser
|
||||
/// installer can read an .xapk's zip directory and entries without fetching
|
||||
/// the whole file. Only files the manifest lists are served.
|
||||
///
|
||||
/// The builds contain Rec Room's own game files, so the routes answer only on
|
||||
/// the site's own host (the apex) and 404 on every other host the server
|
||||
/// accepts: the game's API/CDN subdomains are typically left open for the
|
||||
/// client, while the site sits behind an access proxy (e.g. Cloudflare Access).
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[AllowAnonymous]
|
||||
[Route("api/site/v1/downloads")]
|
||||
public class DownloadsController(DownloadsService downloads, DomainConfig domain) : ControllerBase
|
||||
{
|
||||
private bool Available => downloads.IsConfigured
|
||||
&& string.Equals(Request.Host.Host, domain.Apex, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>GET /api/site/v1/downloads — the manifest as published
|
||||
/// (snake_case keys), or 404 when downloads aren't set up or the request
|
||||
/// isn't for the site's host.</summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetManifest(CancellationToken ct)
|
||||
{
|
||||
if (!Available) return NotFound(new { error = "Downloads aren't set up on this server." });
|
||||
var manifest = await downloads.GetManifestAsync(ct);
|
||||
Response.Headers.CacheControl = "no-cache";
|
||||
return Content((manifest ?? new System.Text.Json.Nodes.JsonObject { ["releases"] = new System.Text.Json.Nodes.JsonArray() })
|
||||
.ToJsonString(), "application/json");
|
||||
}
|
||||
|
||||
/// <summary>GET /api/site/v1/downloads/files/{path} — one listed file,
|
||||
/// streamed; honours Range (206).</summary>
|
||||
[HttpGet("files/{**path}")]
|
||||
public async Task GetFile(string path, CancellationToken ct)
|
||||
{
|
||||
if (!Available) { Response.StatusCode = StatusCodes.Status404NotFound; return; }
|
||||
var manifest = await downloads.GetManifestAsync(ct);
|
||||
var entry = manifest is null ? null : DownloadsService.FindFile(manifest, path);
|
||||
if (entry is null) { Response.StatusCode = StatusCodes.Status404NotFound; return; }
|
||||
|
||||
using var upstream = await downloads.GetFileAsync(path, Request.Headers.Range.ToString(), ct);
|
||||
Response.StatusCode = (int)upstream.StatusCode;
|
||||
if (!upstream.IsSuccessStatusCode) return;
|
||||
|
||||
var content = upstream.Content.Headers;
|
||||
if (content.ContentLength is long length) Response.ContentLength = length;
|
||||
if (content.ContentRange is not null) Response.Headers.ContentRange = content.ContentRange.ToString();
|
||||
Response.Headers.AcceptRanges = "bytes";
|
||||
if (upstream.Headers.ETag is not null) Response.Headers.ETag = upstream.Headers.ETag.ToString();
|
||||
Response.Headers.CacheControl = "private, no-cache";
|
||||
Response.ContentType = "application/octet-stream";
|
||||
var name = (string?)entry["name"] ?? Path.GetFileName(path);
|
||||
Response.Headers.ContentDisposition = new System.Net.Mime.ContentDisposition { FileName = name }.ToString();
|
||||
|
||||
await using var body = await upstream.Content.ReadAsStreamAsync(ct);
|
||||
await body.CopyToAsync(Response.Body, 1 << 16, ct);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user