From 1c28df61dfca79880c760762a1b27e9ad8b71e6c Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 5 Mar 2026 07:24:28 -0500 Subject: [PATCH] fix(ha): PostgreSQL migration backup no-op + URI connection string support - MigrationBackupFast/RestoreBackupFast/DeleteBackup return no-ops for PostgreSQL; pre-migration backups are handled by jellyfin-pg-backup CronJob, not the automated backup path that throws NotSupportedException - ServiceCollectionExtensions: detect postgresql:// / postgres:// URI format in POSTGRES_CONNECTION_STRING and convert to ADO.NET key=value format before passing to NpgsqlDataSourceBuilder (which requires it) Closes startup crash: 'Automated migration backups are not supported for PostgreSQL' on first boot with a fresh database. --- .../Extensions/ServiceCollectionExtensions.cs | 17 +++++++++++++++++ .../PostgreSqlDatabaseProvider.cs | 16 +++++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index aed695c35..abdd5ec83 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -149,6 +149,23 @@ public static class ServiceCollectionExtensions "No PostgreSQL connection string found. Set the POSTGRES_CONNECTION_STRING environment variable, " + "or provide it via CustomProviderOptions.Options[\"ConnectionString\"] or CustomProviderOptions.ConnectionString."); + // Support postgresql:// / postgres:// URI format (e.g. DATABASE_URL convention). + // NpgsqlDataSourceBuilder requires ADO.NET key=value format; convert if needed. + if (connectionString.StartsWith("postgresql://", StringComparison.OrdinalIgnoreCase) + || connectionString.StartsWith("postgres://", StringComparison.OrdinalIgnoreCase)) + { + var uri = new Uri(connectionString); + var userInfoParts = uri.UserInfo.Split(':', 2); + connectionString = new NpgsqlConnectionStringBuilder + { + Host = uri.Host, + Port = uri.Port > 0 ? uri.Port : 5432, + Database = uri.AbsolutePath.TrimStart('/'), + Username = userInfoParts.Length > 0 ? Uri.UnescapeDataString(userInfoParts[0]) : null, + Password = userInfoParts.Length > 1 ? Uri.UnescapeDataString(userInfoParts[1]) : null, + }.ToString(); + } + var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); dataSourceBuilder.ConnectionStringBuilder.MinPoolSize = GetPoolOption(options, "MinPoolSize", 2); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/PostgreSqlDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/PostgreSqlDatabaseProvider.cs index 9b0367685..8fac80484 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/PostgreSqlDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/PostgreSqlDatabaseProvider.cs @@ -15,8 +15,9 @@ namespace Jellyfin.Database.Providers.PostgreSQL; [JellyfinDatabaseProviderKey("Jellyfin-PostgreSQL")] public sealed class PostgreSqlDatabaseProvider : IJellyfinDatabaseProvider { - private const string BackupNotSupportedMessage = - "Automated migration backups are not supported for PostgreSQL. Use the jellyfin-pg-backup CronJob for nightly S3 backups."; + // Sentinel returned by MigrationBackupFast to signal that no file backup was + // created (PostgreSQL backups are handled externally by jellyfin-pg-backup CronJob). + private const string NoAutomatedBackupKey = "postgresql-no-automated-backup"; private readonly NpgsqlDataSource _dataSource; @@ -69,19 +70,24 @@ public sealed class PostgreSqlDatabaseProvider : IJellyfinDatabaseProvider /// public Task MigrationBackupFast(CancellationToken cancellationToken) { - throw new NotSupportedException(BackupNotSupportedMessage); + // PostgreSQL pre-migration backups are handled externally by the + // jellyfin-pg-backup CronJob. Return a sentinel so callers know no + // file backup was created and the migration can proceed safely. + return Task.FromResult(NoAutomatedBackupKey); } /// public Task RestoreBackupFast(string key, CancellationToken cancellationToken) { - throw new NotSupportedException(BackupNotSupportedMessage); + // No automated backup was taken; nothing to restore. + return Task.CompletedTask; } /// public Task DeleteBackup(string key) { - throw new NotSupportedException(BackupNotSupportedMessage); + // No automated backup was taken; nothing to delete. + return Task.CompletedTask; } ///