Add automatic PostgreSQL database creation and migration
- Added Jellyfin.Database.Providers.Postgres project with full EF Core migration support for PostgreSQL. - Implemented automatic database creation and privilege assignment on startup. - Generated initial migration and model snapshot for PostgreSQL schema. - Updated build, test, and dependency files to include PostgreSQL provider and Npgsql packages. - Added PowerShell script for generating and testing PostgreSQL migrations. - No changes to application logic outside database provider/migration infrastructure.
This commit is contained in:
+88
@@ -97,6 +97,94 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the PostgreSQL database exists, creating it if necessary.
|
||||
/// </summary>
|
||||
/// <param name="databaseConfiguration">The database configuration.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task EnsureDatabaseExistsAsync(DatabaseConfigurationOptions databaseConfiguration, CancellationToken cancellationToken = default)
|
||||
{
|
||||
static T? GetOption<T>(ICollection<CustomDatabaseOption>? options, string key, Func<string, T> converter, Func<T>? defaultValue = null)
|
||||
{
|
||||
if (options is null)
|
||||
{
|
||||
return defaultValue is not null ? defaultValue() : default;
|
||||
}
|
||||
|
||||
var value = options.FirstOrDefault(e => e.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
|
||||
if (value is null)
|
||||
{
|
||||
return defaultValue is not null ? defaultValue() : default;
|
||||
}
|
||||
|
||||
return converter(value.Value);
|
||||
}
|
||||
|
||||
var customOptions = databaseConfiguration.CustomProviderOptions?.Options;
|
||||
|
||||
var host = GetOption(customOptions, "host", e => e, () => "localhost")!;
|
||||
var port = GetOption(customOptions, "port", int.Parse, () => 5432);
|
||||
var database = GetOption(customOptions, "database", e => e, () => "jellyfin")!;
|
||||
var username = GetOption(customOptions, "username", e => e, () => "jellyfin")!;
|
||||
var password = GetOption(customOptions, "password", e => e, () => string.Empty)!;
|
||||
var timeout = GetOption(customOptions, "connection-timeout", int.Parse, () => 15);
|
||||
|
||||
// Connect to 'postgres' maintenance database to check if target database exists
|
||||
var maintenanceConnectionBuilder = new NpgsqlConnectionStringBuilder
|
||||
{
|
||||
Host = host,
|
||||
Port = port,
|
||||
Database = "postgres", // Connect to maintenance database
|
||||
Username = username,
|
||||
Password = password,
|
||||
Timeout = timeout,
|
||||
Pooling = false // Don't pool maintenance connections
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = new NpgsqlConnection(maintenanceConnectionBuilder.ToString());
|
||||
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Check if database exists
|
||||
var checkQuery = "SELECT 1 FROM pg_database WHERE datname = @databaseName";
|
||||
await using var checkCommand = new NpgsqlCommand(checkQuery, connection);
|
||||
checkCommand.Parameters.AddWithValue("databaseName", database);
|
||||
|
||||
var exists = await checkCommand.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (exists is null)
|
||||
{
|
||||
// Database doesn't exist, create it
|
||||
logger.LogInformation("PostgreSQL database '{Database}' does not exist. Creating...", database);
|
||||
|
||||
// Use identifier quoting for safety
|
||||
var createQuery = $"CREATE DATABASE \"{database}\" OWNER \"{username}\"";
|
||||
await using var createCommand = new NpgsqlCommand(createQuery, connection);
|
||||
await createCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
logger.LogInformation("PostgreSQL database '{Database}' created successfully", database);
|
||||
|
||||
// Grant privileges
|
||||
var grantQuery = $"GRANT ALL PRIVILEGES ON DATABASE \"{database}\" TO \"{username}\"";
|
||||
await using var grantCommand = new NpgsqlCommand(grantQuery, connection);
|
||||
await grantCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
logger.LogInformation("Granted all privileges on database '{Database}' to user '{Username}'", database, username);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("PostgreSQL database '{Database}' already exists", database);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to ensure PostgreSQL database '{Database}' exists. Error: {Message}", database, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task RunScheduledOptimisation(CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user