using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
namespace DorkNet.Server.Startup;
/// Per-host static-files branching for admin, public site, and feed hosts.
public static class StaticHostExtensions
{
public static void MountStaticHost(this WebApplication app, string host, string subdir, bool spaFallback = false)
{
var staticHostLogger = app.Services.GetRequiredService()
.CreateLogger("DorkNet.Server.StaticHost");
var root = Path.Combine(app.Environment.ContentRootPath, "wwwroot", subdir);
staticHostLogger.LogInformation(
"[static-host] {Host} -> {Root} (exists: {Exists}, spaFallback: {Spa})",
host, root, Directory.Exists(root), spaFallback);
if (!Directory.Exists(root)) return;
var files = new PhysicalFileProvider(root);
var defaultFilesOpts = new DefaultFilesOptions { FileProvider = files };
var staticFilesOpts = new StaticFileOptions
{
FileProvider = files,
OnPrepareResponse = ctx =>
{
var headers = ctx.Context.Response.Headers;
headers.CacheControl = "no-cache, no-store, must-revalidate";
headers.Pragma = "no-cache";
headers.Expires = "0";
},
};
app.UseWhen(ctx => ctx.Request.Host.Host.Equals(host, StringComparison.OrdinalIgnoreCase),
branch =>
{
branch.Use(async (ctx, next) =>
{
var p = ctx.Request.Path.Value ?? "/";
var probe = files.GetFileInfo(p == "/" ? "/index.html" : p);
staticHostLogger.LogInformation(
"[static-host:{Host}] path={Path} probe={Probe} exists={Exists}",
host, p, probe.PhysicalPath ?? "", probe.Exists);
if (!p.StartsWith("/api/", StringComparison.OrdinalIgnoreCase))
ctx.SetEndpoint(null);
await next();
});
branch.UseDefaultFiles(defaultFilesOpts);
branch.UseStaticFiles(staticFilesOpts);
branch.Use(async (ctx, next) =>
{
if (ctx.Response.HasStarted) return;
var path = ctx.Request.Path.Value ?? "/";
if (path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase))
{
await next();
return;
}
if (ctx.Request.Method != "GET" && ctx.Request.Method != "HEAD")
{
await next();
return;
}
if (Path.HasExtension(path))
{
ctx.Response.StatusCode = StatusCodes.Status404NotFound;
return;
}
if (!spaFallback)
{
ctx.Response.StatusCode = StatusCodes.Status404NotFound;
return;
}
var index = files.GetFileInfo("/index.html");
if (!index.Exists)
{
ctx.Response.StatusCode = StatusCodes.Status404NotFound;
return;
}
ctx.Response.ContentType = "text/html; charset=utf-8";
var hdrs = ctx.Response.Headers;
hdrs.CacheControl = "no-cache, no-store, must-revalidate";
hdrs.Pragma = "no-cache";
hdrs.Expires = "0";
await using var stream = index.CreateReadStream();
await stream.CopyToAsync(ctx.Response.Body);
});
});
}
}