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;
|
2022-04-05 13:42:14 +08:00
|
|
|
|
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;
|
2022-04-03 14:47:54 +08:00
|
|
|
|
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;
|
2022-04-05 13:42:14 +08:00
|
|
|
|
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;
|
2022-04-05 13:42:14 +08:00
|
|
|
|
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)
|
|
|
|
|
{
|
2022-04-05 13:42:14 +08:00
|
|
|
|
// 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);
|
|
|
|
|
|
2022-04-05 13:42:14 +08:00
|
|
|
|
// 如果 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 移动到一个单独的测试项目里
|
|
|
|
|
if (Debugger.IsAttached)
|
|
|
|
|
{
|
|
|
|
|
var configuration = new MapperConfiguration(cfg => cfg.AddProfile<DataMappingProfile>());
|
|
|
|
|
configuration.AssertConfigurationIsValid();
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2021-05-30 19:16:20 +08:00
|
|
|
|
services
|
2022-04-05 13:42:14 +08:00
|
|
|
|
.AddAutoMapper(c => c.AddProfile<DataMappingProfile>())
|
2021-05-30 19:16:20 +08:00
|
|
|
|
.AddCors(o => o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()))
|
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>>();
|
2022-04-05 13:42:14 +08:00
|
|
|
|
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 =>
|
|
|
|
|
{
|
|
|
|
|
c.SwaggerDoc("brec", new OpenApiInfo
|
|
|
|
|
{
|
|
|
|
|
Title = "录播姬 REST API",
|
2022-04-03 14:52:11 +08:00
|
|
|
|
Description = "录播姬网站 [rec.danmuji.org](https://rec.danmuji.org/) \n录播姬 GitHub [Bililive/BililiveRecorder](https://github.com/Bililive/BililiveRecorder) \n\n" +
|
|
|
|
|
"除了 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 =>
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
})
|
2022-04-03 14:47:54 +08:00
|
|
|
|
.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.
|
2022-04-05 13:42:14 +08:00
|
|
|
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
|
|
|
|
{
|
|
|
|
|
const string PAGE404 = "/404.html";
|
|
|
|
|
|
|
|
|
|
app.UseCors().UseWebSockets();
|
|
|
|
|
|
|
|
|
|
app.Use(static next => async context =>
|
2021-05-30 19:16:20 +08:00
|
|
|
|
{
|
2022-04-05 13:42:14 +08:00
|
|
|
|
// 404 页面处理
|
|
|
|
|
await next(context);
|
2022-04-03 13:34:55 +08:00
|
|
|
|
|
2022-04-05 13:42:14 +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.Request.Path = "/ui/";
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
context.Request.Path = PAGE404;
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-05 13:42:14 +08:00
|
|
|
|
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
|
|
|
|
|
2022-04-05 13:42:14 +08:00
|
|
|
|
app.UseRouting();
|
2022-04-03 13:34:55 +08:00
|
|
|
|
|
2022-04-05 13:42:14 +08:00
|
|
|
|
static void OnPrepareResponse(StaticFileResponseContext context)
|
|
|
|
|
{
|
|
|
|
|
if (context.Context.Request.Path == PAGE404)
|
2021-05-30 19:16:20 +08:00
|
|
|
|
{
|
2022-04-05 13:42:14 +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";
|
|
|
|
|
|
2022-04-05 16:05:48 +08:00
|
|
|
|
var sharedStaticFiles = new SharedOptions()
|
2022-04-05 13:42:14 +08:00
|
|
|
|
{
|
|
|
|
|
// 在运行的 exe 旁边新建一个 wwwroot 文件夹,会优先使用里面的内容,然后 fallback 到打包的资源文件
|
|
|
|
|
FileProvider = new CompositeFileProvider(env.WebRootFileProvider, new ManifestEmbeddedFileProvider(typeof(Startup).Assembly)),
|
|
|
|
|
RedirectToAppendTrailingSlash = true,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
app
|
2022-04-05 16:05:48 +08:00
|
|
|
|
.UseDefaultFiles(new DefaultFilesOptions(sharedStaticFiles))
|
|
|
|
|
.UseStaticFiles(new StaticFileOptions(sharedStaticFiles)
|
2022-04-05 13:42:14 +08:00
|
|
|
|
{
|
|
|
|
|
ContentTypeProvider = ctp,
|
|
|
|
|
OnPrepareResponse = OnPrepareResponse
|
|
|
|
|
})
|
|
|
|
|
//.UseDirectoryBrowser(new DirectoryBrowserOptions(shared))
|
|
|
|
|
;
|
|
|
|
|
|
2022-04-05 16:05:48 +08:00
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var sharedRecordingFiles = new SharedOptions
|
|
|
|
|
{
|
|
|
|
|
FileProvider = new PhysicalFileProvider(app.ApplicationServices.GetRequiredService<IRecorder>().Config.Global.WorkDirectory),
|
|
|
|
|
RequestPath = "/file",
|
|
|
|
|
RedirectToAppendTrailingSlash = true,
|
|
|
|
|
};
|
|
|
|
|
app.UseStaticFiles(new StaticFileOptions(sharedRecordingFiles)).UseDirectoryBrowser(new DirectoryBrowserOptions(sharedRecordingFiles));
|
|
|
|
|
}
|
|
|
|
|
catch (Exception) { }
|
|
|
|
|
|
2022-04-05 13:42:14 +08:00
|
|
|
|
app
|
|
|
|
|
.UseEndpoints(endpoints =>
|
|
|
|
|
{
|
|
|
|
|
endpoints.MapControllers();
|
|
|
|
|
|
|
|
|
|
endpoints.MapGraphQL<RecorderSchema>();
|
|
|
|
|
endpoints.MapGraphQLWebSockets<RecorderSchema>();
|
2022-04-03 13:34:55 +08:00
|
|
|
|
|
2022-04-05 13:42:14 +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");
|
2022-04-05 13:42:14 +08:00
|
|
|
|
})
|
|
|
|
|
.UseSwaggerUI(c =>
|
|
|
|
|
{
|
|
|
|
|
c.SwaggerEndpoint("brec/swagger.json", "录播姬 REST API");
|
|
|
|
|
})
|
|
|
|
|
.Use(static next => static context =>
|
2021-05-30 19:16:20 +08:00
|
|
|
|
{
|
2022-04-05 13:42:14 +08:00
|
|
|
|
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
|
|
|
|
return Task.CompletedTask;
|
2022-04-03 13:34:55 +08:00
|
|
|
|
});
|
2022-04-05 13:42:14 +08:00
|
|
|
|
}
|
2021-05-30 19:16:20 +08:00
|
|
|
|
}
|
|
|
|
|
}
|