From 6bc5a0b1404645117b0b800167d78680767ef70e Mon Sep 17 00:00:00 2001 From: Spirtix Date: Sat, 2 Dec 2023 17:20:02 +0100 Subject: [PATCH] local asset server --- src/Configuration/AssetServerConfig.cs | 13 ++++ src/Middleware/AssetMiddleware.cs | 99 ++++++++++++++++++++++++++ src/Program.cs | 14 ++++ 3 files changed, 126 insertions(+) create mode 100644 src/Configuration/AssetServerConfig.cs create mode 100644 src/Middleware/AssetMiddleware.cs diff --git a/src/Configuration/AssetServerConfig.cs b/src/Configuration/AssetServerConfig.cs new file mode 100644 index 0000000..fd63c5c --- /dev/null +++ b/src/Configuration/AssetServerConfig.cs @@ -0,0 +1,13 @@ +namespace sodoff.Configuration; +public class AssetServerConfig { + public bool Enabled { get; set; } = false; + public int Port { get; set; } = 5001; + public string URLPrefix { get; set; } = string.Empty; + public AssetServerMode Mode { get; set; } + public string ProviderURL { get; set; } = string.Empty; + public bool SubstituteMissingLocalAssets { get; set; } = false; +} + +public enum AssetServerMode { + None, Partial, Full +} diff --git a/src/Middleware/AssetMiddleware.cs b/src/Middleware/AssetMiddleware.cs new file mode 100644 index 0000000..3546950 --- /dev/null +++ b/src/Middleware/AssetMiddleware.cs @@ -0,0 +1,99 @@ +using Microsoft.Extensions.Options; +using sodoff.Configuration; + +namespace sodoff.Middleware; +public class AssetMiddleware +{ + private readonly RequestDelegate _next; + private readonly IOptions config; + + public AssetMiddleware(RequestDelegate next, IOptions config) + { + _next = next; + this.config = config; + } + + public async Task Invoke(HttpContext context) + { + if (context.Connection.LocalPort == config.Value.Port) + await GetAssetAsync(context); + else + await _next(context); + } + + private async Task GetAssetAsync(HttpContext context) + { + string path = context.Request.Path; + + if (path is null || !string.IsNullOrEmpty(config.Value.URLPrefix) && !path.StartsWith("/" + config.Value.URLPrefix) || config.Value.Mode == AssetServerMode.None) { + context.Response.StatusCode = 400; + return; + } + + string assetPath = path.Remove(0, config.Value.URLPrefix.Length + 1); + + string localPath = GetLocalPath("assets/" + assetPath); + + if (localPath == string.Empty) { + if (config.Value.Mode == AssetServerMode.Partial) + await GetRemoteAsset(context, assetPath); + else + context.Response.StatusCode = 404; + } + else { + context.Response.Headers["Content-Type"] = "application/octet-stream"; + await context.Response.SendFileAsync(Path.GetFullPath(localPath)); + } + } + + private async Task GetRemoteAsset(HttpContext context, string path) + { + HttpClient client = new HttpClient(); + try { + var response = await client.GetAsync(config.Value.ProviderURL + path); + string? contentType = response.Content.Headers.ContentType?.MediaType; + + if (contentType is null) { + context.Response.StatusCode = 404; + } + else if (contentType.StartsWith("application/octet-stream") || contentType.StartsWith("image/jpeg")) { + context.Response.Headers["Content-Type"] = contentType; + await response.Content.CopyToAsync(context.Response.Body); + } + else { + context.Response.ContentType = contentType; + await context.Response.WriteAsync(await response.Content.ReadAsStringAsync()); + } + } + catch (Exception) { + context.Response.StatusCode = 404; + } + } + + private string GetLocalPath(string path) + { + if (File.Exists(path)) return path; + + string[] qualityTiers = { "/High/", "/Mid/", "/Low/" }; + + if (config.Value.SubstituteMissingLocalAssets) + { + foreach (var tier in qualityTiers) + { + if (path.Contains(tier)) + { + foreach (var otherTier in qualityTiers) + { + if (otherTier != tier) + { + string otherPath = path.Replace(tier, otherTier); + if (File.Exists(otherPath)) return otherPath; + } + } + } + } + } + + return string.Empty; + } +} diff --git a/src/Program.cs b/src/Program.cs index 03f6181..0b338b5 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,4 +1,7 @@ using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using sodoff.Configuration; +using sodoff.Middleware; using sodoff.Model; using sodoff.Services; using sodoff.Utils; @@ -8,6 +11,7 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. +builder.Services.Configure(builder.Configuration.GetSection("AssetServer")); builder.Services.AddControllers(options => { options.OutputFormatters.Add(new XmlSerializerOutputFormatter(new XmlWriterSettings() { OmitXmlDeclaration = false })); options.OutputFormatters.RemoveType(); @@ -27,6 +31,13 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +bool assetServer = builder.Configuration.GetSection("AssetServer").GetValue("Enabled"); +int assetPort = builder.Configuration.GetSection("AssetServer").GetValue("Port"); +if (assetServer) + builder.Services.Configure(options => { + options.ListenAnyIP(assetPort); + }); + var app = builder.Build(); using var scope = app.Services.CreateScope(); @@ -35,6 +46,9 @@ scope.ServiceProvider.GetRequiredService().Database.EnsureCreated(); // Configure the HTTP request pipeline. +if (assetServer) + app.UseMiddleware(); + app.UseAuthorization(); app.MapControllers();