Platform config templates & Postgres migration recovery

- Added Linux/Windows startup config templates and README
- Updated default paths to FHS-compliant locations
- Improved publish profiles for platform targeting
- Enhanced Postgres migration: auto-recover on 42P07 errors
- Added RecordMigrationAsAppliedAsync for manual migration history
- Improved migration logging and error handling
- Added detailed documentation for config and migration fixes
This commit is contained in:
2026-02-26 13:44:03 -05:00
parent 24ae417a39
commit 5bdb7d53c3
11 changed files with 947 additions and 10 deletions
@@ -11,6 +11,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
@@ -337,6 +338,14 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
// Ensure the migrations history table exists
await context.Database.EnsureCreatedAsync(cancellationToken).ConfigureAwait(false);
// Get applied migrations
var appliedMigrations = await context.Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false);
var appliedMigrationsList = appliedMigrations.ToList();
logger.LogDebug("Found {Count} applied migrations", appliedMigrationsList.Count);
// Get pending migrations
var pendingMigrations = await context.Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false);
var pendingMigrationsList = pendingMigrations.ToList();
@@ -352,6 +361,24 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
await context.Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
logger.LogInformation("Successfully applied {Count} migrations", pendingMigrationsList.Count);
}
catch (Npgsql.PostgresException pgEx) when (pgEx.SqlState == "42P07") // relation already exists
{
// Table already exists - this can happen if migrations were partially applied
logger.LogWarning("Migration attempted to create existing table. This may indicate a previous failed migration. Error: {Message}", pgEx.Message);
logger.LogWarning("Attempting to mark migration as applied in history table...");
// Try to manually record the migration as applied
try
{
await RecordMigrationAsAppliedAsync(context, pendingMigrationsList, cancellationToken).ConfigureAwait(false);
logger.LogInformation("Successfully recovered from partial migration state");
}
catch (Exception recordEx)
{
logger.LogError(recordEx, "Failed to recover from partial migration. Manual intervention may be required.");
throw;
}
}
catch (InvalidOperationException ex) when (ex.Message.Contains("pending changes", StringComparison.OrdinalIgnoreCase))
{
// This can happen if the model has changed but migrations haven't been generated
@@ -424,6 +451,69 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
}
}
/// <summary>
/// Manually records migrations as applied in the migrations history table.
/// This is used to recover from situations where tables were created but the history wasn't updated.
/// </summary>
/// <param name="context">The database context.</param>
/// <param name="migrationIds">List of migration IDs to record.</param>
/// <param name="cancellationToken">Cancellation token.</param>
private async Task RecordMigrationAsAppliedAsync(JellyfinDbContext context, IList<string> migrationIds, CancellationToken cancellationToken)
{
var connection = context.Database.GetDbConnection();
var wasOpened = false;
try
{
if (connection.State != System.Data.ConnectionState.Open)
{
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
wasOpened = true;
}
// Get the current product version from the assembly
var productVersion = typeof(JellyfinDbContext).Assembly
.GetCustomAttribute<System.Reflection.AssemblyInformationalVersionAttribute>()?
.InformationalVersion ?? "Unknown";
foreach (var migrationId in migrationIds)
{
logger.LogInformation("Recording migration '{MigrationId}' as applied", migrationId);
var insertQuery = @"
INSERT INTO ""__EFMigrationsHistory"" (""MigrationId"", ""ProductVersion"")
VALUES (@migrationId, @productVersion)
ON CONFLICT (""MigrationId"") DO NOTHING";
await using (var command = connection.CreateCommand())
{
command.CommandText = insertQuery;
var migrationParam = command.CreateParameter();
migrationParam.ParameterName = "@migrationId";
migrationParam.Value = migrationId;
command.Parameters.Add(migrationParam);
var versionParam = command.CreateParameter();
versionParam.ParameterName = "@productVersion";
versionParam.Value = productVersion;
command.Parameters.Add(versionParam);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
}
logger.LogInformation("Successfully recorded {Count} migrations in history table", migrationIds.Count);
}
finally
{
if (wasOpened)
{
await connection.CloseAsync().ConfigureAwait(false);
}
}
}
/// <inheritdoc/>
public async Task RunScheduledOptimisation(CancellationToken cancellationToken)
{