BililiveRecorder/BililiveRecorder.Web/Startup.cs

280 lines
12 KiB
C#
Raw Permalink Normal View History

2022-04-03 13:34:55 +08:00
using System;
2021-05-30 19:16:20 +08:00
using System.Diagnostics;
2022-04-03 13:34:55 +08:00
using System.IO;
using System.Threading.Tasks;
2022-04-09 16:43:05 +08:00
using AutoMapper;
2021-05-30 19:16:20 +08:00
using BililiveRecorder.Core;
using BililiveRecorder.Web.Graphql;
2022-04-03 15:34:22 +08:00
using BililiveRecorder.Web.Models.Rest;
2021-05-30 19:16:20 +08:00
using GraphQL;
2022-04-03 14:52:11 +08:00
using GraphQL.Execution;
2021-05-30 19:16:20 +08:00
using GraphQL.Server;
2022-04-03 14:52:11 +08:00
using GraphQL.SystemReactive;
2021-05-30 19:16:20 +08:00
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.AspNetCore.StaticFiles.Infrastructure;
2021-05-30 19:16:20 +08:00
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.FileProviders;
2021-05-30 19:16:20 +08:00
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
2022-04-03 13:34:55 +08:00
using Microsoft.OpenApi.Models;
2021-05-30 19:16:20 +08:00
namespace BililiveRecorder.Web
{
public class Startup
{
// TODO 减少引用的依赖数量
public Startup(IConfiguration configuration, IWebHostEnvironment environment)
{
this.Configuration = configuration;
this.Environment = environment;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment Environment { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
// GraphQL API 使用 Newtonsoft.Json 会同步调用 System.IO.StreamReader.Peek
// 来源是 GraphQL.Server.Transports.AspNetCore.NewtonsoftJson.GraphQLRequestDeserializer.DeserializeFromJsonBodyAsync
// System.Text.Json 不使用构造函数传属性,所以无法创建 Optional<T> (需要加 attribute回头改
// TODO 改 Optional<T>
2021-05-30 19:16:20 +08:00
services.Configure<KestrelServerOptions>(o => o.AllowSynchronousIO = true);
// 如果 IRecorder 没有被注册过才会添加,模拟调试用
// 实际运行时在 BililiveRecorder.Web.Program 里会加上真的 IRecorder
2021-05-30 19:16:20 +08:00
services.TryAddSingleton<IRecorder>(new FakeRecorderForWeb());
2022-04-09 16:43:05 +08:00
#if DEBUG
// TODO 移动到一个单独的测试项目里
var configuration = new MapperConfiguration(cfg => cfg.AddProfile<DataMappingProfile>());
configuration.AssertConfigurationIsValid();
2022-04-09 16:43:05 +08:00
#endif
2021-05-30 19:16:20 +08:00
services
.AddAutoMapper(c => c.AddProfile<DataMappingProfile>())
2022-06-09 16:04:29 +08:00
.AddCors(o => o.AddDefaultPolicy(policy =>
{
policy
.SetIsOriginAllowed(origin => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
.WithExposedHeaders("Content-Length")
.SetPreflightMaxAge(TimeSpan.FromMinutes(5))
;
}));
2022-04-03 14:52:11 +08:00
services
.AddSingleton(new ManifestEmbeddedFileProvider(typeof(Startup).Assembly))
.AddSingleton(sp => new CompositeFileProvider(sp.GetRequiredService<IWebHostEnvironment>().WebRootFileProvider, sp.GetRequiredService<ManifestEmbeddedFileProvider>()));
2022-06-07 13:44:50 +08:00
2022-04-03 14:52:11 +08:00
// Graphql API
GraphQL.MicrosoftDI.GraphQLBuilderExtensions.AddGraphQL(services)
.AddServer(true)
.AddWebSockets()
.AddNewtonsoftJson()
.AddSchema<RecorderSchema>()
.AddSubscriptionDocumentExecuter()
.AddDefaultEndpointSelectorPolicy()
.AddGraphTypes(typeof(RecorderSchema).Assembly)
.Configure<ErrorInfoProviderOptions>(opt => opt.ExposeExceptionStackTrace = this.Environment.IsDevelopment() || Debugger.IsAttached)
.ConfigureExecution(options =>
2021-05-30 19:16:20 +08:00
{
options.EnableMetrics = this.Environment.IsDevelopment() || Debugger.IsAttached;
2022-04-03 14:52:11 +08:00
var logger = options.RequestServices?.GetRequiredService<ILogger<Startup>>();
options.UnhandledExceptionDelegate = ctx => logger?.LogError(ctx.OriginalException, "Graphql Error");
2021-05-30 19:16:20 +08:00
})
;
2022-04-03 13:34:55 +08:00
// REST API
services
.AddSwaggerGen(c =>
{
2022-06-12 14:44:29 +08:00
c.UseAllOfForInheritance();
c.UseOneOfForPolymorphism();
2022-04-03 13:34:55 +08:00
c.SwaggerDoc("brec", new OpenApiInfo
{
Title = "录播姬 REST API",
2022-06-07 18:35:46 +08:00
Description = "录播姬网站 [rec.danmuji.org](https://rec.danmuji.org/) \n录播姬 GitHub [BililiveRecorder/BililiveRecorder](https://github.com/BililiveRecorder/BililiveRecorder) \n\n" +
2022-04-03 14:52:11 +08:00
"除了 REST API 以外,录播姬还有 Graphql API 可以使用。\n\n" +
"API 中的 objectId 在重启后会重新生成,不保存到配置文件。",
2022-04-03 13:34:55 +08:00
Version = "v1"
});
var filePath = Path.Combine(AppContext.BaseDirectory, "BililiveRecorder.Web.xml");
c.IncludeXmlComments(filePath);
})
.AddRouting(c =>
{
c.LowercaseUrls = true;
c.LowercaseQueryStrings = true;
})
.AddMvcCore(option =>
{
})
.AddApiExplorer()
.AddNewtonsoftJson();
2021-05-30 19:16:20 +08:00
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
const string PAGE404 = "/404.html";
if (app.ApplicationServices.GetService<DisableOpenAccessWarningConfig>() is null)
{
app.UseMiddleware<OpenAccessWarningMiddleware>();
}
2022-06-09 16:45:07 +08:00
app
.UseCors()
.UseMiddleware<BasicAuthMiddleware>()
.UseWebSockets();
app.Use(static next => async context =>
2021-05-30 19:16:20 +08:00
{
// 404 页面处理
await next(context);
2022-04-03 13:34:55 +08:00
if (!context.Response.HasStarted
&& !context.Response.ContentLength.HasValue
&& string.IsNullOrEmpty(context.Response.ContentType)
&& context.Response.StatusCode == StatusCodes.Status404NotFound)
{
var originalPath = context.Request.Path;
var originalQueryString = context.Request.QueryString;
try
{
2022-06-04 03:39:33 +08:00
if (originalPath.StartsWithSegments("/ui"))
{
context.Items["webui-spa-path"] = originalPath;
2022-06-04 03:39:33 +08:00
context.Request.Path = "/ui/";
}
else
{
context.Request.Path = PAGE404;
}
context.Request.QueryString = new QueryString();
await next(context);
}
finally
{
context.Request.Path = originalPath;
context.Request.QueryString = originalQueryString;
}
}
});
2022-04-03 13:34:55 +08:00
app.UseRouting();
2022-04-03 13:34:55 +08:00
static void OnPrepareResponse(StaticFileResponseContext context)
{
if (context.Context.Request.Path == PAGE404)
2021-05-30 19:16:20 +08:00
{
context.Context.Response.StatusCode = StatusCodes.Status404NotFound;
context.Context.Response.Headers.CacheControl = "no-cache";
}
else
{
context.Context.Response.Headers.CacheControl = "max-age=86400, must-revalidate";
}
}
var ctp = new FileExtensionContentTypeProvider();
ctp.Mappings[".htm"] = "text/html; charset=utf-8";
ctp.Mappings[".html"] = "text/html; charset=utf-8";
ctp.Mappings[".css"] = "text/css; charset=utf-8";
ctp.Mappings[".js"] = "text/javascript; charset=utf-8";
ctp.Mappings[".mjs"] = "text/javascript; charset=utf-8";
ctp.Mappings[".json"] = "application/json; charset=utf-8";
ctp.Mappings[".mkv"] = "video/x-matroska";
ctp.Mappings[".mk3d"] = "video/x-matroska";
ctp.Mappings[".mka"] = "audio/x-matroska";
ctp.Mappings[".mks"] = "video/x-matroska";
ctp.Mappings[".srt"] = "text/plain";
ctp.Mappings[".ass"] = "text/plain";
ctp.Mappings[".ssa"] = "text/plain";
var compositeFileProvider = app.ApplicationServices.GetRequiredService<CompositeFileProvider>();
2022-04-05 16:05:48 +08:00
var sharedStaticFiles = new SharedOptions()
{
// 在运行的 exe 旁边新建一个 wwwroot 文件夹,会优先使用里面的内容,然后 fallback 到打包的资源文件
FileProvider = compositeFileProvider,
RedirectToAppendTrailingSlash = true,
};
app
2022-04-05 16:05:48 +08:00
.UseDefaultFiles(new DefaultFilesOptions(sharedStaticFiles))
.UseStaticFiles(new StaticFileOptions(sharedStaticFiles)
{
ContentTypeProvider = ctp,
OnPrepareResponse = OnPrepareResponse
})
//.UseDirectoryBrowser(new DirectoryBrowserOptions(shared))
;
2022-04-05 16:05:48 +08:00
try
{
2022-06-08 00:58:08 +08:00
if (app.ApplicationServices.GetService<BililiveRecorderFileExplorerSettings>()?.Enable ?? false)
2022-04-05 16:05:48 +08:00
{
2022-06-08 00:58:08 +08:00
var sharedRecordingFiles = new SharedOptions
{
FileProvider = new PhysicalFileProvider(app.ApplicationServices.GetRequiredService<IRecorder>().Config.Global.WorkDirectory),
RequestPath = "/file",
RedirectToAppendTrailingSlash = true,
};
app
.UseStaticFiles(new StaticFileOptions(sharedRecordingFiles)
{
ContentTypeProvider = ctp,
ServeUnknownFileTypes = true,
})
.UseDirectoryBrowser(new DirectoryBrowserOptions(sharedRecordingFiles)
{
Formatter = new BililiveRecorderDirectoryFormatter(),
});
2022-06-08 00:58:08 +08:00
}
2022-04-05 16:05:48 +08:00
}
catch (Exception) { }
app
.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapGraphQL<RecorderSchema>();
endpoints.MapGraphQLWebSockets<RecorderSchema>();
2022-04-03 13:34:55 +08:00
endpoints.MapSwagger();
2022-06-04 03:39:33 +08:00
endpoints.MapGraphQLPlayground("graphql/playground");
endpoints.MapGraphQLGraphiQL("graphql/graphiql");
endpoints.MapGraphQLAltair("graphql/altair");
endpoints.MapGraphQLVoyager("graphql/voyager");
})
.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("brec/swagger.json", "录播姬 REST API");
})
.Use(static next => static context =>
2021-05-30 19:16:20 +08:00
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
return Task.CompletedTask;
2022-04-03 13:34:55 +08:00
});
}
2021-05-30 19:16:20 +08:00
}
}