Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 25s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m40s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m18s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m12s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m13s
probe742/combined-newest SECOND
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be>
123 lines
4.8 KiB
C#
123 lines
4.8 KiB
C#
using System.Reflection;
|
|
using Dapper;
|
|
using ErsatzTV.Core;
|
|
using ErsatzTV.Infrastructure.Data;
|
|
using ErsatzTV.Infrastructure.Streaming.Graphics;
|
|
using Microsoft.Data.Sqlite;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ErsatzTV.Services.RunOnce;
|
|
|
|
public class DatabaseMigratorService : BackgroundService
|
|
{
|
|
private readonly ILogger<DatabaseMigratorService> _logger;
|
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
|
private readonly SystemStartup _systemStartup;
|
|
|
|
public DatabaseMigratorService(
|
|
IServiceScopeFactory serviceScopeFactory,
|
|
SystemStartup systemStartup,
|
|
ILogger<DatabaseMigratorService> logger)
|
|
{
|
|
_serviceScopeFactory = serviceScopeFactory;
|
|
_systemStartup = systemStartup;
|
|
_logger = logger;
|
|
}
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
await Task.Yield();
|
|
|
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
|
await using TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
|
|
|
// extract empty database to speed up initial startup
|
|
if (TvContext.IsSqlite && !File.Exists(FileSystemLayout.DatabasePath))
|
|
{
|
|
_logger.LogInformation("Extracting empty database to {DatabasePath}", FileSystemLayout.DatabasePath);
|
|
await using Stream resource = typeof(ResourceExtractorService).GetTypeInfo().Assembly
|
|
.GetManifestResourceStream("ErsatzTV.Resources.empty.sqlite3");
|
|
if (resource != null)
|
|
{
|
|
await using FileStream fs = File.Create(FileSystemLayout.DatabasePath);
|
|
await resource.CopyToAsync(fs, stoppingToken);
|
|
}
|
|
}
|
|
|
|
_logger.LogInformation("Applying database migrations");
|
|
|
|
if (TvContext.IsSqlite)
|
|
{
|
|
int count = await dbContext.Connection.ExecuteScalarAsync<int>(
|
|
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='__EFMigrationsLock'");
|
|
if (count > 0)
|
|
{
|
|
count = await dbContext.Connection.ExecuteScalarAsync<int>("SELECT count(*) FROM `__EFMigrationsLock`");
|
|
if (count > 0)
|
|
{
|
|
_logger.LogWarning(
|
|
"Cleaning database migrations lock; this is needed when ETV is terminated during a database migration.");
|
|
|
|
// sqlite migrations lock is always stale since mutex ensures single instance of etv
|
|
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM `__EFMigrationsLock`", stoppingToken);
|
|
}
|
|
}
|
|
}
|
|
|
|
List<string> pendingMigrations = await dbContext.Database
|
|
.GetPendingMigrationsAsync(stoppingToken)
|
|
.Map(l => l.ToList());
|
|
|
|
if (pendingMigrations.Any(m => m.Contains("Add_MediaFilePathHash", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
await dbContext.Database.MigrateAsync("Add_MediaFilePathHash", stoppingToken);
|
|
}
|
|
|
|
List<string> appliedMigrations = await dbContext.Database
|
|
.GetAppliedMigrationsAsync(stoppingToken)
|
|
.Map(l => l.ToList());
|
|
|
|
if (appliedMigrations.Count > 0)
|
|
{
|
|
// this can't be part of a migration, so we have to stop here and run some sql
|
|
await PopulatePathHashes(dbContext);
|
|
}
|
|
|
|
// then continue migrating
|
|
await dbContext.Database.MigrateAsync(stoppingToken);
|
|
|
|
_logger.LogInformation("Initializing database");
|
|
await DbInitializer.Initialize(dbContext, stoppingToken);
|
|
|
|
var fileSystem = scope.ServiceProvider.GetRequiredService<System.IO.Abstractions.IFileSystem>();
|
|
await GraphicsElementSeeder.SeedOnNowNext(dbContext, fileSystem, _logger, stoppingToken);
|
|
await GraphicsElementSeeder.AttachOnNowNextByDefault(dbContext, stoppingToken);
|
|
|
|
_systemStartup.DatabaseIsReady();
|
|
|
|
_logger.LogInformation("Done applying database migrations");
|
|
}
|
|
|
|
private async Task PopulatePathHashes(TvContext dbContext)
|
|
{
|
|
if (await dbContext.Connection.ExecuteScalarAsync<int>(
|
|
"SELECT COUNT(*) FROM `MediaFile` WHERE `PathHash` IS NULL OR `PathHash` = ''") == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_logger.LogInformation("Populating database path hashes");
|
|
|
|
if (dbContext.Connection is SqliteConnection sqliteConnection)
|
|
{
|
|
sqliteConnection.CreateFunction("HASH_SHA256", (string text) => PathUtils.GetPathHash(text));
|
|
await dbContext.Connection.ExecuteAsync("UPDATE `MediaFile` SET `PathHash` = HASH_SHA256(`Path`);");
|
|
}
|
|
else
|
|
{
|
|
// mysql
|
|
await dbContext.Connection.ExecuteAsync("UPDATE `MediaFile` SET `PathHash` = sha2(`Path`, 256);");
|
|
}
|
|
}
|
|
}
|