Files
ersatztv/ErsatzTV/Services/RunOnce/DatabaseMigratorService.cs
T
timothyandtimothy ba6a4b08aa
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
feat(732): On Now / Next gets a background box, and is on by default (#843)
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be>
2026-08-26 19:28:25 +00:00

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);");
}
}
}