using System.Text.Json; using Microsoft.AspNetCore.Mvc; using RadicadeServer.API.Studio; namespace RadicadeServer.CDN { /// /// Step one of the client's two-step bundle download. The client asks /// /rooms/unity_assets/{assetId}/{target}/{version} and is handed a file name, then /// ranges over that file at /data/{Filename} (and, for a subroom's bundle, under the /// CDN's own /unity/{Filename} alias). /// /// The id arrives in two shapes and both have to be answered. A Studio manifest id is 40 /// alphanumerics and its baked bundles are named in the manifest's own metadata. A legacy /// bundle is stored under its file name (or that name minus .assetbundle), and that /// name is what the subroom's UnityAssetId carries - every imported room, the built-in /// rooms and every room bound from the admin panel. Answering only the 40-character shape /// leaves every legacy bundle unresolvable, and the client reports one 400 as /// "Error downloading room assets" for the whole room. /// [Route("rooms/")] public class UnityAssetsCDN : Controller { [HttpGet("unity_assets/{assetId}/{target}/{version}")] public async Task GetUnityAsset(string assetId, int target, int version) { return await ServeMetadataAsync(assetId, target, version); } [HttpGet("unity_assets/{assetId}")] public async Task GetUnityAsset(string assetId) { return await ServeMetadataAsync(assetId, 0, 1); } private async Task ServeMetadataAsync(string assetId, int target, int version) { if (!CdnStorageNames.IsServable(assetId) || target < 0 || target > byte.MaxValue || version < 0) return BadRequest(); string? filename = null; string? rejectedDependencies = null; if (StudioUnityAssetRegistry.IsValidAssetId(assetId)) { var asset = await StudioUnityAssetRegistry.ReadAsync(assetId, HttpContext.RequestAborted); var baked = asset?.BakedUnityAssets.FirstOrDefault(x => x.Target == target && x.Version == version); if (baked != null) { filename = baked.Filename; rejectedDependencies = baked.RejectedDependenciesFilename; } else if (asset != null) { // The manifest exists, so the file on disk under this id is that JSON // manifest and not a bundle. A target/version it does not name is a 404: // handing the manifest over as the bundle would have the client parse // metadata as an asset bundle. return NotFound(); } } // Reached for every legacy id, and for a 40-character id with no manifest behind it: // the bundle is the file stored under the name the subroom points at, with or without // the .assetbundle suffix an import appends. filename ??= CdnStorageNames.Resolve(assetId); if (filename == null) return NotFound(); var body = JsonSerializer.Serialize(new { UnityAssetId = assetId, Target = target, Version = version, Filename = filename, RejectedDependenciesFilename = rejectedDependencies }); return Content(body, "application/json"); } } }