mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2025-05-10 17:34:13 +02:00
Add PostgreSQL database support via Entity Framework
This commit is contained in:
parent
23a4cf69dd
commit
57073b1bd0
.config
.run
AddMigration.batAddMigration.shPhantomPanel.slnServer
Phantom.Server.Database.Postgres
Phantom.Server.Database
Phantom.Server.Web.Components/Utils
Phantom.Server.Web
Phantom.Server
12
.config/dotnet-tools.json
Normal file
12
.config/dotnet-tools.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "7.0.0-rc.1.22426.7",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,11 @@
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<envs>
|
||||
<env name="ASPNETCORE_ENVIRONMENT" value="Development" />
|
||||
<env name="PG_DATABASE" value="postgres" />
|
||||
<env name="PG_HOST" value="localhost" />
|
||||
<env name="PG_PASS" value="development" />
|
||||
<env name="PG_PORT" value="9402" />
|
||||
<env name="PG_USER" value="postgres" />
|
||||
<env name="RPC_SERVER_HOST" value="localhost" />
|
||||
<env name="WEB_SERVER_HOST" value="localhost" />
|
||||
</envs>
|
||||
|
8
AddMigration.bat
Normal file
8
AddMigration.bat
Normal file
@ -0,0 +1,8 @@
|
||||
@echo off
|
||||
|
||||
if [%1]==[] (
|
||||
echo "Usage: AddMigration.bat <migration-name>"
|
||||
exit
|
||||
)
|
||||
|
||||
dotnet ef migrations add %~1 --project Server/Phantom.Server.Database.Postgres
|
6
AddMigration.sh
Normal file
6
AddMigration.sh
Normal file
@ -0,0 +1,6 @@
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: AddMigration.sh <migration-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dotnet ef migrations add "$1" --project Server/Phantom.Server.Database.Postgres
|
@ -24,6 +24,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Messages", "
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Server", "Server\Phantom.Server\Phantom.Server.csproj", "{A0F1C595-96B6-4DBF-8C16-6B99223F8F35}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Server.Database", "Server\Phantom.Server.Database\Phantom.Server.Database.csproj", "{E3AD566F-384A-489A-A3BB-EA3BA400C18C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Server.Database.Postgres", "Server\Phantom.Server.Database.Postgres\Phantom.Server.Database.Postgres.csproj", "{81625B4A-3DB6-48BD-A739-D23DA02107D1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Server.Rpc", "Server\Phantom.Server.Rpc\Phantom.Server.Rpc.csproj", "{79312D72-44E0-431D-96A4-4C0A066B9671}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Server.Services", "Server\Phantom.Server.Services\Phantom.Server.Services.csproj", "{90F0F1B1-EB0A-49C9-8DF0-1153A87F77C9}"
|
||||
@ -78,6 +82,14 @@ Global
|
||||
{A0F1C595-96B6-4DBF-8C16-6B99223F8F35}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A0F1C595-96B6-4DBF-8C16-6B99223F8F35}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A0F1C595-96B6-4DBF-8C16-6B99223F8F35}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E3AD566F-384A-489A-A3BB-EA3BA400C18C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E3AD566F-384A-489A-A3BB-EA3BA400C18C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E3AD566F-384A-489A-A3BB-EA3BA400C18C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E3AD566F-384A-489A-A3BB-EA3BA400C18C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{81625B4A-3DB6-48BD-A739-D23DA02107D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{81625B4A-3DB6-48BD-A739-D23DA02107D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{81625B4A-3DB6-48BD-A739-D23DA02107D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{81625B4A-3DB6-48BD-A739-D23DA02107D1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{79312D72-44E0-431D-96A4-4C0A066B9671}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{79312D72-44E0-431D-96A4-4C0A066B9671}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{79312D72-44E0-431D-96A4-4C0A066B9671}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@ -123,6 +135,8 @@ Global
|
||||
{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18}
|
||||
{95B55357-F8F0-48C2-A1C2-5EA997651783} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18}
|
||||
{A0F1C595-96B6-4DBF-8C16-6B99223F8F35} = {8AC8FB6C-033A-4626-820F-ED0F908756B2}
|
||||
{E3AD566F-384A-489A-A3BB-EA3BA400C18C} = {8AC8FB6C-033A-4626-820F-ED0F908756B2}
|
||||
{81625B4A-3DB6-48BD-A739-D23DA02107D1} = {8AC8FB6C-033A-4626-820F-ED0F908756B2}
|
||||
{79312D72-44E0-431D-96A4-4C0A066B9671} = {8AC8FB6C-033A-4626-820F-ED0F908756B2}
|
||||
{90F0F1B1-EB0A-49C9-8DF0-1153A87F77C9} = {8AC8FB6C-033A-4626-820F-ED0F908756B2}
|
||||
{7CA2E5FE-E507-4DC6-930C-E18711A9F856} = {8AC8FB6C-033A-4626-820F-ED0F908756B2}
|
||||
|
@ -0,0 +1,15 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace Phantom.Server.Database.Postgres;
|
||||
|
||||
public sealed class ApplicationDbContextDesignFactory : IDesignTimeDbContextFactory<ApplicationDbContext> {
|
||||
public ApplicationDbContext CreateDbContext(string[] args) {
|
||||
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||
.UseNpgsql(static options => options.MigrationsAssembly(Assembly.GetExecutingAssembly().FullName))
|
||||
.Options;
|
||||
|
||||
return new ApplicationDbContext(options);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0-rc.1.22426.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.0-rc.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Phantom.Server.Database\Phantom.Server.Database.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
7
Server/Phantom.Server.Database/ApplicationDbContext.cs
Normal file
7
Server/Phantom.Server.Database/ApplicationDbContext.cs
Normal file
@ -0,0 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Phantom.Server.Database;
|
||||
|
||||
public class ApplicationDbContext : DbContext {
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) {}
|
||||
}
|
30
Server/Phantom.Server.Database/DatabaseProvider.cs
Normal file
30
Server/Phantom.Server.Database/DatabaseProvider.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Phantom.Server.Database;
|
||||
|
||||
public sealed class DatabaseProvider {
|
||||
private readonly IServiceScopeFactory serviceScopeFactory;
|
||||
|
||||
public DatabaseProvider(IServiceScopeFactory serviceScopeFactory) {
|
||||
this.serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
public Scope CreateScope() {
|
||||
return new Scope(serviceScopeFactory.CreateScope());
|
||||
}
|
||||
|
||||
public readonly struct Scope : IDisposable {
|
||||
private readonly IServiceScope scope;
|
||||
|
||||
public ApplicationDbContext Ctx { get; }
|
||||
|
||||
internal Scope(IServiceScope scope) {
|
||||
this.scope = scope;
|
||||
this.Ctx = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
scope.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Phantom.Server.Database.Factories;
|
||||
|
||||
public abstract class AbstractUpsertHelper<T> where T : class {
|
||||
private protected readonly ApplicationDbContext Ctx;
|
||||
|
||||
internal AbstractUpsertHelper(ApplicationDbContext ctx) {
|
||||
this.Ctx = ctx;
|
||||
}
|
||||
|
||||
private protected abstract DbSet<T> Set { get; }
|
||||
|
||||
private protected abstract T Construct(Guid guid);
|
||||
|
||||
public T Fetch(Guid guid) {
|
||||
DbSet<T> set = Set;
|
||||
T? entity = set.Find(guid);
|
||||
|
||||
if (entity == null) {
|
||||
entity = Construct(guid);
|
||||
set.Add(entity);
|
||||
}
|
||||
else {
|
||||
set.Update(entity);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.0-rc.1.22427.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0-rc.1.22426.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Data\Phantom.Common.Data.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
31
Server/Phantom.Server.Web.Components/Utils/Throttler.cs
Normal file
31
Server/Phantom.Server.Web.Components/Utils/Throttler.cs
Normal file
@ -0,0 +1,31 @@
|
||||
namespace Phantom.Server.Web.Components.Utils;
|
||||
|
||||
public sealed class Throttler {
|
||||
private readonly TimeSpan interval;
|
||||
private DateTime lastInvocation;
|
||||
|
||||
public Throttler(TimeSpan interval) {
|
||||
this.interval = interval;
|
||||
this.lastInvocation = DateTime.Now;
|
||||
}
|
||||
|
||||
public bool Check() {
|
||||
var now = DateTime.Now;
|
||||
if (now - lastInvocation >= interval) {
|
||||
lastInvocation = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task Wait() {
|
||||
var now = DateTime.Now;
|
||||
var waitTime = lastInvocation + interval - now;
|
||||
if (waitTime > TimeSpan.Zero) {
|
||||
await Task.Delay(waitTime);
|
||||
}
|
||||
|
||||
lastInvocation = DateTime.Now;
|
||||
}
|
||||
}
|
@ -1,9 +1,12 @@
|
||||
using Serilog;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Server.Database;
|
||||
using Phantom.Server.Web.Components.Utils;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Server.Web;
|
||||
|
||||
public static class Launcher {
|
||||
public static WebApplication CreateApplication(Configuration config, IConfigurator configurator) {
|
||||
public static async Task<WebApplication> CreateApplication(Configuration config, IConfigurator configurator, Action<DbContextOptionsBuilder> dbOptionsBuilder) {
|
||||
var builder = WebApplication.CreateBuilder(new WebApplicationOptions {
|
||||
ApplicationName = typeof(Launcher).Assembly.GetName().Name
|
||||
});
|
||||
@ -21,10 +24,18 @@ public static class Launcher {
|
||||
|
||||
builder.Services.AddSingleton<IHostLifetime>(new NullLifetime());
|
||||
|
||||
builder.Services.AddDbContextPool<ApplicationDbContext>(dbOptionsBuilder, poolSize: 64);
|
||||
builder.Services.AddSingleton<DatabaseProvider>();
|
||||
|
||||
builder.Services.AddRazorPages(static options => options.RootDirectory = "/Layout");
|
||||
builder.Services.AddServerSideBlazor();
|
||||
|
||||
return builder.Build();
|
||||
var application = builder.Build();
|
||||
|
||||
await MigrateDatabase(config, application.Services.GetRequiredService<DatabaseProvider>());
|
||||
await configurator.LoadFromDatabase(application.Services);
|
||||
|
||||
return application;
|
||||
}
|
||||
|
||||
public static async Task Launch(Configuration config, WebApplication application) {
|
||||
@ -59,7 +70,26 @@ public static class Launcher {
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task MigrateDatabase(Configuration config, DatabaseProvider databaseProvider) {
|
||||
var logger = config.Logger;
|
||||
|
||||
using var scope = databaseProvider.CreateScope();
|
||||
var database = scope.Ctx.Database;
|
||||
|
||||
logger.Information("Connecting to database...");
|
||||
|
||||
var retryConnection = new Throttler(TimeSpan.FromSeconds(10));
|
||||
while (!await database.CanConnectAsync(config.CancellationToken)) {
|
||||
logger.Warning("Cannot connect to database, retrying...");
|
||||
await retryConnection.Wait();
|
||||
}
|
||||
|
||||
logger.Information("Running database migrations...");
|
||||
await database.MigrateAsync(); // Do not allow cancellation.
|
||||
}
|
||||
|
||||
public interface IConfigurator {
|
||||
void ConfigureServices(IServiceCollection services);
|
||||
Task LoadFromDatabase(IServiceProvider serviceProvider);
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Phantom.Server.Database\Phantom.Server.Database.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Server.Web.Components\Phantom.Server.Web.Components.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj" />
|
||||
<ProjectReference Include="..\..\Utils\Phantom.Utils.IO\Phantom.Utils.IO.csproj" />
|
||||
<ProjectReference Include="..\..\Utils\Phantom.Utils.Runtime\Phantom.Utils.Runtime.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Server.Database.Postgres\Phantom.Server.Database.Postgres.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Server.Rpc\Phantom.Server.Rpc.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Server.Services\Phantom.Server.Services.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Server.Web\Phantom.Server.Web.csproj" />
|
||||
|
@ -1,6 +1,8 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Server;
|
||||
using Phantom.Server.Database.Postgres;
|
||||
using Phantom.Server.Rpc;
|
||||
using Phantom.Server.Services.Rpc;
|
||||
using Phantom.Utils.IO;
|
||||
@ -18,7 +20,7 @@ PosixSignals.RegisterCancellation(cancellationTokenSource, static () => {
|
||||
try {
|
||||
PhantomLogger.Root.InformationHeading("Initializing Phantom Panel server...");
|
||||
|
||||
var (webServerHost, webServerPort, rpcServerHost, rpcServerPort) = Variables.LoadOrExit();
|
||||
var (webServerHost, webServerPort, rpcServerHost, rpcServerPort, sqlConnectionString) = Variables.LoadOrExit();
|
||||
|
||||
string secretsPath = Path.GetFullPath("./secrets");
|
||||
if (!Directory.Exists(secretsPath)) {
|
||||
@ -46,7 +48,9 @@ try {
|
||||
PhantomLogger.Root.InformationHeading("Launching Phantom Panel server...");
|
||||
|
||||
var webConfigurator = new WebConfigurator(agentToken, cancellationTokenSource.Token);
|
||||
var webApplication = WebLauncher.CreateApplication(webConfiguration, webConfigurator);
|
||||
var webApplication = await WebLauncher.CreateApplication(webConfiguration, webConfigurator, options => options.UseNpgsql(sqlConnectionString, static options => {
|
||||
options.CommandTimeout(10).MigrationsAssembly(typeof(ApplicationDbContextDesignFactory).Assembly.FullName);
|
||||
}));
|
||||
|
||||
await Task.WhenAll(
|
||||
RpcLauncher.Launch(rpcConfiguration, webApplication.Services.GetRequiredService<MessageToServerListenerFactory>().CreateListener),
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Phantom.Common.Logging;
|
||||
using Npgsql;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Utils.Runtime;
|
||||
|
||||
namespace Phantom.Server;
|
||||
@ -7,14 +8,24 @@ sealed record Variables(
|
||||
string WebServerHost,
|
||||
ushort WebServerPort,
|
||||
string RpcServerHost,
|
||||
ushort RpcServerPort
|
||||
ushort RpcServerPort,
|
||||
string SqlConnectionString
|
||||
) {
|
||||
private static Variables LoadOrThrow() {
|
||||
var connectionStringBuilder = new NpgsqlConnectionStringBuilder {
|
||||
Host = EnvironmentVariables.GetString("PG_HOST").OrThrow,
|
||||
Port = EnvironmentVariables.GetPortNumber("PG_PORT").OrThrow,
|
||||
Username = EnvironmentVariables.GetString("PG_USER").OrThrow,
|
||||
Password = EnvironmentVariables.GetString("PG_PASS").OrThrow,
|
||||
Database = EnvironmentVariables.GetString("PG_DATABASE").OrThrow
|
||||
};
|
||||
|
||||
return new Variables(
|
||||
EnvironmentVariables.GetString("WEB_SERVER_HOST").OrDefault("0.0.0.0"),
|
||||
EnvironmentVariables.GetPortNumber("WEB_SERVER_PORT").OrDefault(9400),
|
||||
EnvironmentVariables.GetString("RPC_SERVER_HOST").OrDefault("0.0.0.0"),
|
||||
EnvironmentVariables.GetPortNumber("RPC_SERVER_PORT").OrDefault(9401)
|
||||
EnvironmentVariables.GetPortNumber("RPC_SERVER_PORT").OrDefault(9401),
|
||||
connectionStringBuilder.ToString()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -22,4 +22,8 @@ sealed class WebConfigurator : WebLauncher.IConfigurator {
|
||||
services.AddSingleton<AgentManager>();
|
||||
services.AddSingleton<MessageToServerListenerFactory>();
|
||||
}
|
||||
|
||||
public Task LoadFromDatabase(IServiceProvider serviceProvider) {
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user