mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-05-17 15:22:26 +00:00
Add support for migrations that need the database context
This commit is contained in:
parent
6d0673fcb8
commit
64f65ba574
15 changed files with 656 additions and 481 deletions
|
@ -3,11 +3,13 @@
|
|||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes>
|
||||
<Path>.config/dotnet-tools.json</Path>
|
||||
<Path>.github</Path>
|
||||
<Path>.gitignore</Path>
|
||||
<Path>.idea</Path>
|
||||
<Path>CONTRIBUTING.md</Path>
|
||||
<Path>DatabaseMigrations</Path>
|
||||
<Path>LICENSE</Path>
|
||||
<Path>ProjectLighthouse.sln.DotSettings</Path>
|
||||
<Path>ProjectLighthouse.sln.DotSettings.user</Path>
|
||||
<Path>README.md</Path>
|
||||
|
|
25
ProjectLighthouse/Administration/CompletedMigration.cs
Normal file
25
ProjectLighthouse/Administration/CompletedMigration.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using LBPUnion.ProjectLighthouse.Administration.Maintenance;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Administration;
|
||||
|
||||
/// <summary>
|
||||
/// A record of the completion of a <see cref="IMigrationTask"/>.
|
||||
/// </summary>
|
||||
public class CompletedMigration
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the migration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Do not use the user-friendly name when setting this.
|
||||
/// </remarks>
|
||||
[Key]
|
||||
public string MigrationName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The moment the migration was ran.
|
||||
/// </summary>
|
||||
public DateTime RanAt { get; set; }
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Administration.Maintenance;
|
||||
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
|
||||
public interface IMaintenanceJob
|
||||
{
|
||||
public Task Run();
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Administration.Maintenance;
|
||||
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
|
||||
public interface IMigrationTask
|
||||
{
|
||||
/// <summary>
|
||||
/// The user-friendly name of a migration.
|
||||
/// </summary>
|
||||
public string Name();
|
||||
|
||||
/// <summary>
|
||||
/// Performs the migration.
|
||||
/// </summary>
|
||||
/// <param name="database">The Lighthouse database.</param>
|
||||
/// <returns>True if successful, false if not.</returns>
|
||||
internal Task<bool> Run(Database database);
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using LBPUnion.ProjectLighthouse.Extensions;
|
||||
using LBPUnion.ProjectLighthouse.Logging;
|
||||
using LBPUnion.ProjectLighthouse.Logging.Loggers;
|
||||
|
||||
|
@ -11,24 +13,16 @@ namespace LBPUnion.ProjectLighthouse.Administration.Maintenance;
|
|||
|
||||
public static class MaintenanceHelper
|
||||
{
|
||||
|
||||
static MaintenanceHelper()
|
||||
{
|
||||
Commands = getListOfInterfaceObjects<ICommand>();
|
||||
MaintenanceJobs = getListOfInterfaceObjects<IMaintenanceJob>();
|
||||
MigrationTasks = getListOfInterfaceObjects<IMigrationTask>();
|
||||
}
|
||||
|
||||
public static List<ICommand> Commands { get; }
|
||||
|
||||
public static List<IMaintenanceJob> MaintenanceJobs { get; }
|
||||
|
||||
private static List<T> getListOfInterfaceObjects<T>() where T : class
|
||||
{
|
||||
return Assembly.GetExecutingAssembly()
|
||||
.GetTypes()
|
||||
.Where(t => t.GetInterfaces().Contains(typeof(T)) && t.GetConstructor(Type.EmptyTypes) != null)
|
||||
.Select(t => Activator.CreateInstance(t) as T)
|
||||
.ToList()!;
|
||||
}
|
||||
public static List<IMigrationTask> MigrationTasks { get; }
|
||||
|
||||
public static async Task<List<LogLine>> RunCommand(string[] args)
|
||||
{
|
||||
|
@ -66,11 +60,57 @@ public static class MaintenanceHelper
|
|||
IMaintenanceJob? job = MaintenanceJobs.FirstOrDefault(j => j.GetType().Name == jobName);
|
||||
if (job == null) throw new ArgumentNullException();
|
||||
|
||||
await RunMaintenanceJob(job);
|
||||
}
|
||||
|
||||
public static async Task RunMaintenanceJob(IMaintenanceJob job)
|
||||
{
|
||||
await job.Run();
|
||||
}
|
||||
|
||||
public static async Task RunMigration(IMigrationTask migrationTask, Database? database = null)
|
||||
{
|
||||
database ??= new Database();
|
||||
|
||||
// Migrations should never be run twice.
|
||||
Debug.Assert(!await database.CompletedMigrations.Has(m => m.MigrationName == migrationTask.GetType().Name));
|
||||
|
||||
Logger.Info($"Running migration task {migrationTask.Name()}", LogArea.Database);
|
||||
|
||||
bool success;
|
||||
Exception? exception = null;
|
||||
|
||||
try
|
||||
{
|
||||
success = await migrationTask.Run(database);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
success = false;
|
||||
exception = e;
|
||||
}
|
||||
|
||||
if(!success)
|
||||
{
|
||||
Logger.Error($"Could not run migration {migrationTask.Name()}", LogArea.Database);
|
||||
if (exception != null) Logger.Error(exception.ToDetailedException(), LogArea.Database);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Success($"Successfully completed migration {migrationTask.Name()}", LogArea.Database);
|
||||
|
||||
CompletedMigration completedMigration = new()
|
||||
{
|
||||
MigrationName = migrationTask.GetType().Name,
|
||||
RanAt = DateTime.Now,
|
||||
};
|
||||
|
||||
database.CompletedMigrations.Add(completedMigration);
|
||||
await database.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static List<T> getListOfInterfaceObjects<T>() where T : class
|
||||
{
|
||||
return Assembly.GetExecutingAssembly()
|
||||
.GetTypes()
|
||||
.Where(t => t.GetInterfaces().Contains(typeof(T)) && t.GetConstructor(Type.EmptyTypes) != null)
|
||||
.Select(t => Activator.CreateInstance(t) as T)
|
||||
.ToList()!;
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ public class CleanupBrokenPhotosMaintenanceJob : IMaintenanceJob
|
|||
bool largeHashIsInvalidFile = false;
|
||||
bool tooManyPhotoSubjects = false;
|
||||
bool duplicatePhotoSubjects = false;
|
||||
bool takenInTheFuture = true;
|
||||
bool takenInTheFuture = false;
|
||||
|
||||
// Checks should generally be ordered in least computationally expensive to most.
|
||||
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
using LBPUnion.ProjectLighthouse.Administration.Reports;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
using LBPUnion.ProjectLighthouse.Levels;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData.Reviews;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.MaintenanceJobs;
|
||||
|
||||
public class CleanupXmlInjection : IMaintenanceJob
|
||||
{
|
||||
private readonly Database database = new();
|
||||
public string Name() => "Sanitize user content";
|
||||
public string Description() => "Sanitizes all user-generated strings in levels, reviews, comments, users, and scores to prevent XML injection. Only needs to be run once.";
|
||||
|
||||
public async Task Run()
|
||||
{
|
||||
foreach (Slot slot in this.database.Slots) SanitizationHelper.SanitizeStringsInClass(slot);
|
||||
|
||||
foreach (Review review in this.database.Reviews) SanitizationHelper.SanitizeStringsInClass(review);
|
||||
|
||||
foreach (Comment comment in this.database.Comments) SanitizationHelper.SanitizeStringsInClass(comment);
|
||||
|
||||
foreach (Score score in this.database.Scores) SanitizationHelper.SanitizeStringsInClass(score);
|
||||
|
||||
foreach (User user in this.database.Users) SanitizationHelper.SanitizeStringsInClass(user);
|
||||
|
||||
foreach (Photo photo in this.database.Photos) SanitizationHelper.SanitizeStringsInClass(photo);
|
||||
|
||||
foreach (GriefReport report in this.database.Reports) SanitizationHelper.SanitizeStringsInClass(report);
|
||||
|
||||
await this.database.SaveChangesAsync();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.MigrationTasks;
|
||||
|
||||
public class CleanupXmlInjectionMigration : IMigrationTask
|
||||
{
|
||||
public string Name() => "Cleanup XML injections";
|
||||
|
||||
// Weird, but required. Thanks, hejlsberg.
|
||||
async Task<bool> IMigrationTask.Run(Database database)
|
||||
{
|
||||
List<object> objsToBeSanitized = new();
|
||||
|
||||
// Store all the objects we need to sanitize in a list.
|
||||
// The alternative here is to loop through every table, but thats a ton of code...
|
||||
objsToBeSanitized.AddRange(database.Slots);
|
||||
objsToBeSanitized.AddRange(database.Reviews);
|
||||
objsToBeSanitized.AddRange(database.Comments);
|
||||
objsToBeSanitized.AddRange(database.Scores);
|
||||
objsToBeSanitized.AddRange(database.Users);
|
||||
objsToBeSanitized.AddRange(database.Photos);
|
||||
objsToBeSanitized.AddRange(database.Reports);
|
||||
|
||||
foreach (object obj in objsToBeSanitized) SanitizationHelper.SanitizeStringsInClass(obj);
|
||||
|
||||
await database.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using LBPUnion.ProjectLighthouse.Administration;
|
||||
using LBPUnion.ProjectLighthouse.Administration.Reports;
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
|
@ -21,6 +22,7 @@ namespace LBPUnion.ProjectLighthouse;
|
|||
|
||||
public class Database : DbContext
|
||||
{
|
||||
public DbSet<CompletedMigration> CompletedMigrations { get; set; }
|
||||
public DbSet<User> Users { get; set; }
|
||||
public DbSet<Location> Locations { get; set; }
|
||||
public DbSet<Slot> Slots { get; set; }
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using LBPUnion.ProjectLighthouse.Levels;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData.Reviews;
|
||||
|
@ -52,4 +55,7 @@ public static class DatabaseExtensions
|
|||
|
||||
return query;
|
||||
}
|
||||
|
||||
public static async Task<bool> Has<T>(this IQueryable<T> queryable, Expression<Func<T, bool>> predicate) =>
|
||||
await queryable.FirstOrDefaultAsync(predicate) != null;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
using LBPUnion.ProjectLighthouse;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ProjectLighthouse.Migrations
|
||||
{
|
||||
[DbContext(typeof(Database))]
|
||||
[Migration("20220610061641_AddCompletedMigrations")]
|
||||
public class AddCompletedMigrations : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CompletedMigrations",
|
||||
columns: table => new
|
||||
{
|
||||
MigrationName = table.Column<string>(type: "varchar(255)", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
RanAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CompletedMigrations", x => x.MigrationName);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "CompletedMigrations");
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -50,6 +50,10 @@
|
|||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Administration\Maintenance\MaintenanceJobs\CleanupXMLInjection.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
||||
<Exec Command="git describe --long --always --dirty --exclude=\* --abbrev=8 > "$(ProjectDir)/gitVersion.txt"" />
|
||||
<Exec Command="git branch --show-current > "$(ProjectDir)/gitBranch.txt"" />
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using LBPUnion.ProjectLighthouse.Administration;
|
||||
using LBPUnion.ProjectLighthouse.Administration.Maintenance;
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using LBPUnion.ProjectLighthouse.Extensions;
|
||||
|
@ -112,14 +114,35 @@ public static class StartupTasks
|
|||
Logger.Success($"Ready! Startup took {stopwatch.ElapsedMilliseconds}ms. Passing off control to ASP.NET...", LogArea.Startup);
|
||||
}
|
||||
|
||||
private static void migrateDatabase(DbContext database)
|
||||
private static void migrateDatabase(Database database)
|
||||
{
|
||||
Stopwatch totalStopwatch = new();
|
||||
Stopwatch stopwatch = new();
|
||||
totalStopwatch.Start();
|
||||
stopwatch.Start();
|
||||
|
||||
database.Database.MigrateAsync().Wait();
|
||||
stopwatch.Stop();
|
||||
Logger.Success($"Structure migration took {stopwatch.ElapsedMilliseconds}ms.", LogArea.Database);
|
||||
|
||||
stopwatch.Reset();
|
||||
stopwatch.Start();
|
||||
|
||||
List<CompletedMigration> completedMigrations = database.CompletedMigrations.ToList();
|
||||
List<IMigrationTask> migrationsToRun = MaintenanceHelper.MigrationTasks
|
||||
.Where(migrationTask => !completedMigrations
|
||||
.Select(m => m.MigrationName)
|
||||
.Contains(migrationTask.GetType().Name)
|
||||
).ToList();
|
||||
|
||||
foreach (IMigrationTask migrationTask in migrationsToRun)
|
||||
{
|
||||
MaintenanceHelper.RunMigration(migrationTask, database).Wait();
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
Logger.Success($"Migration took {stopwatch.ElapsedMilliseconds}ms.", LogArea.Database);
|
||||
totalStopwatch.Stop();
|
||||
Logger.Success($"Extra migration tasks took {stopwatch.ElapsedMilliseconds}ms.", LogArea.Database);
|
||||
Logger.Success($"Total migration took {totalStopwatch.ElapsedMilliseconds}ms.", LogArea.Database);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,9 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Developer script to create EntityFramework database migrations
|
||||
#
|
||||
# $1: Name of the migration, e.g. SwitchToPermissionLevels
|
||||
# Invoked manually
|
||||
|
||||
export LIGHTHOUSE_DB_CONNECTION_STRING='server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse'
|
||||
dotnet ef migrations add "$1" --project ProjectLighthouse
|
Loading…
Add table
Add a link
Reference in a new issue