Add user-configurable PostgreSQL backup system
Introduced backup/restore via external pg_dump/pg_restore tools, configurable in database.xml. Added DatabaseBackupOptions, PostgresBackupService, and example/config docs. Backup features are enabled only for local databases; remote DBs require manual management. Logging and documentation clarify external tool usage, error handling, and security. Updated provider logic and assembly files accordingly.
This commit is contained in:
+140
-9
@@ -9,6 +9,7 @@ namespace Jellyfin.Database.Providers.Postgres;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -29,16 +30,24 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
|
||||
{
|
||||
private readonly IApplicationPaths applicationPaths;
|
||||
private readonly ILogger<PostgresDatabaseProvider> logger;
|
||||
private readonly IConfigurationManager? configurationManager;
|
||||
private PostgresBackupService? backupService;
|
||||
private string? currentHost;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PostgresDatabaseProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="applicationPaths">Service to construct the fallback when the old data path configuration is used.</param>
|
||||
/// <param name="logger">A logger.</param>
|
||||
public PostgresDatabaseProvider(IApplicationPaths applicationPaths, ILogger<PostgresDatabaseProvider> logger)
|
||||
/// <param name="configurationManager">The configuration manager (optional, for backup support).</param>
|
||||
public PostgresDatabaseProvider(
|
||||
IApplicationPaths applicationPaths,
|
||||
ILogger<PostgresDatabaseProvider> logger,
|
||||
IConfigurationManager? configurationManager = null)
|
||||
{
|
||||
this.applicationPaths = applicationPaths;
|
||||
this.logger = logger;
|
||||
this.configurationManager = configurationManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -118,6 +127,9 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
|
||||
|
||||
var connectionString = connectionBuilder.ToString();
|
||||
|
||||
// Store the host for backup operations
|
||||
currentHost = connectionBuilder.Host;
|
||||
|
||||
// Log PostgreSQL connection parameters (without password)
|
||||
logger.LogInformation(
|
||||
"PostgreSQL connection: Host={Host}, Port={Port}, Database={Database}, Username={Username}, Pooling={Pooling}, MaxPoolSize={MaxPoolSize}, Multiplexing={Multiplexing}",
|
||||
@@ -129,6 +141,19 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
|
||||
connectionBuilder.MaxPoolSize,
|
||||
connectionBuilder.Multiplexing);
|
||||
|
||||
// Initialize backup service if host is local and configuration manager is available
|
||||
if (IsLocalHost(currentHost) && configurationManager is not null)
|
||||
{
|
||||
var loggerFactory = Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory.Instance;
|
||||
var backupLogger = loggerFactory.CreateLogger<PostgresBackupService>();
|
||||
backupService = new PostgresBackupService(backupLogger, configurationManager);
|
||||
logger.LogInformation("PostgreSQL backup service initialized for local database (will use external pg_dump/pg_restore tools)");
|
||||
}
|
||||
else if (!IsLocalHost(currentHost))
|
||||
{
|
||||
logger.LogInformation("PostgreSQL database is on remote server ({Host}). Backup operations via external pg_dump/pg_restore tools are disabled", currentHost);
|
||||
}
|
||||
|
||||
options
|
||||
.UseNpgsql(
|
||||
connectionString,
|
||||
@@ -394,28 +419,134 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
|
||||
/// <inheritdoc />
|
||||
public async Task<string> MigrationBackupFast(CancellationToken cancellationToken)
|
||||
{
|
||||
if (IsLocalHost(currentHost) && backupService is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var backupDirectory = Path.Combine(applicationPaths.DataPath, "backups");
|
||||
var backupPath = await backupService.CreateBackupAsync(backupDirectory, cancellationToken).ConfigureAwait(false);
|
||||
logger.LogInformation("PostgreSQL local backup created: {BackupPath}", backupPath);
|
||||
return Path.GetFileName(backupPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create local PostgreSQL backup using pg_dump");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
var key = DateTime.UtcNow.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture);
|
||||
|
||||
logger.LogWarning("Fast backup is not implemented for PostgreSQL. Use pg_dump for proper backups.");
|
||||
|
||||
// Return a key for compatibility, but actual backup should be done externally
|
||||
logger.LogWarning("Fast backup is not implemented for PostgreSQL on a remote server. Use pg_dump manually for proper backups.");
|
||||
return await Task.FromResult(key).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RestoreBackupFast(string key, CancellationToken cancellationToken)
|
||||
public async Task RestoreBackupFast(string key, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogWarning("Fast restore is not implemented for PostgreSQL. Use pg_restore for proper restoration.");
|
||||
return Task.CompletedTask;
|
||||
if (IsLocalHost(currentHost) && backupService is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var backupDirectory = Path.Combine(applicationPaths.DataPath, "backups");
|
||||
var backupPath = Path.Combine(backupDirectory, key);
|
||||
|
||||
if (!File.Exists(backupPath))
|
||||
{
|
||||
// Try to find the backup file with common extensions
|
||||
var possibleExtensions = new[] { ".dump", ".sql", ".tar" };
|
||||
foreach (var ext in possibleExtensions)
|
||||
{
|
||||
var pathWithExt = backupPath + ext;
|
||||
if (File.Exists(pathWithExt))
|
||||
{
|
||||
backupPath = pathWithExt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (File.Exists(backupPath))
|
||||
{
|
||||
await backupService.RestoreBackupAsync(backupPath, cancellationToken).ConfigureAwait(false);
|
||||
logger.LogInformation("PostgreSQL local backup restored from: {BackupPath}", backupPath);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogWarning("Backup file not found: {BackupPath}", backupPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to restore local PostgreSQL backup using pg_restore");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogWarning("Fast restore is not implemented for PostgreSQL on a remote server. Use pg_restore manually for proper restoration.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteBackup(string key)
|
||||
{
|
||||
logger.LogWarning("Backup deletion is not implemented for PostgreSQL. Manage backups externally.");
|
||||
if (IsLocalHost(currentHost) && backupService is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var backupDirectory = Path.Combine(applicationPaths.DataPath, "backups");
|
||||
var backupPath = Path.Combine(backupDirectory, key);
|
||||
|
||||
// Try to find and delete the backup file
|
||||
if (File.Exists(backupPath))
|
||||
{
|
||||
File.Delete(backupPath);
|
||||
logger.LogInformation("Deleted local PostgreSQL backup: {BackupPath}", backupPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try with common extensions
|
||||
var possibleExtensions = new[] { ".dump", ".sql", ".tar" };
|
||||
foreach (var ext in possibleExtensions)
|
||||
{
|
||||
var pathWithExt = backupPath + ext;
|
||||
if (File.Exists(pathWithExt))
|
||||
{
|
||||
File.Delete(pathWithExt);
|
||||
logger.LogInformation("Deleted local PostgreSQL backup: {BackupPath}", pathWithExt);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogWarning("Backup file not found for deletion: {BackupPath}", backupPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to delete local PostgreSQL backup: {BackupKey}", key);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
logger.LogWarning("Backup deletion is not implemented for PostgreSQL on a remote server. Manage backups externally.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the given host is localhost.
|
||||
/// </summary>
|
||||
/// <param name="host">The host to check.</param>
|
||||
/// <returns>True if the host is localhost or 127.0.0.1, false otherwise.</returns>
|
||||
private static bool IsLocalHost(string? host)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return host.Equals("localhost", StringComparison.OrdinalIgnoreCase) ||
|
||||
host.Equals("127.0.0.1", StringComparison.Ordinal) ||
|
||||
host.Equals("::1", StringComparison.Ordinal); // IPv6 localhost
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user