Merge pull request 'pgsql_testing_branch' (#9) from pgsql_testing_branch into main
Reviewed-on: #9
This commit is contained in:
@@ -599,6 +599,10 @@ namespace Emby.Server.Implementations
|
||||
SetStaticProperties();
|
||||
|
||||
FindParts();
|
||||
|
||||
// Ensure at least one user exists
|
||||
var userManager = Resolve<IUserManager>();
|
||||
await userManager.InitializeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private X509Certificate2 GetCertificate(string path, string password)
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Models.StartupDtos;
|
||||
using Jellyfin.Data;
|
||||
using MediaBrowser.Common.Api;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@@ -111,15 +112,18 @@ public class StartupController : BaseJellyfinApiController
|
||||
[HttpGet("User")]
|
||||
[HttpGet("FirstUser", Name = "GetFirstUser_2")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<StartupUserDto> GetFirstUser()
|
||||
public Task<StartupUserDto?> GetFirstUser()
|
||||
{
|
||||
// TODO: Remove this method when startup wizard no longer requires an existing user.
|
||||
await _userManager.InitializeAsync().ConfigureAwait(false);
|
||||
var user = _userManager.Users.First();
|
||||
return new StartupUserDto
|
||||
var user = _userManager.Users.FirstOrDefault();
|
||||
if (user is null)
|
||||
{
|
||||
return Task.FromResult<StartupUserDto?>(null);
|
||||
}
|
||||
|
||||
return Task.FromResult<StartupUserDto?>(new StartupUserDto
|
||||
{
|
||||
Name = user.Username
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -135,19 +139,33 @@ public class StartupController : BaseJellyfinApiController
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> UpdateStartupUser([FromBody] StartupUserDto startupUserDto)
|
||||
{
|
||||
var user = _userManager.Users.First();
|
||||
if (string.IsNullOrWhiteSpace(startupUserDto.Password))
|
||||
{
|
||||
return BadRequest("Password must not be empty");
|
||||
}
|
||||
|
||||
if (startupUserDto.Name is not null)
|
||||
var user = _userManager.Users.FirstOrDefault();
|
||||
|
||||
// If no user exists, create the initial admin user
|
||||
if (user is null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(startupUserDto.Name))
|
||||
{
|
||||
return BadRequest("Username must not be empty when creating initial user");
|
||||
}
|
||||
|
||||
user = await _userManager.CreateUserAsync(startupUserDto.Name).ConfigureAwait(false);
|
||||
user.SetPermission(Jellyfin.Database.Implementations.Enums.PermissionKind.IsAdministrator, true);
|
||||
user.SetPermission(Jellyfin.Database.Implementations.Enums.PermissionKind.EnableContentDeletion, true);
|
||||
user.SetPermission(Jellyfin.Database.Implementations.Enums.PermissionKind.EnableRemoteControlOfOtherUsers, true);
|
||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||
}
|
||||
else if (startupUserDto.Name is not null)
|
||||
{
|
||||
user.Username = startupUserDto.Name;
|
||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(startupUserDto.Password))
|
||||
{
|
||||
await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false);
|
||||
|
||||
@@ -542,31 +542,41 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
/// <inheritdoc />
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
|
||||
// If no users exist, create the default admin user
|
||||
if (_users.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var defaultName = Environment.UserName;
|
||||
if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
|
||||
{
|
||||
defaultName = "MyJellyfinUser";
|
||||
}
|
||||
|
||||
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
|
||||
_logger.LogWarning("No users found, creating default admin user with username 'admin'");
|
||||
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
|
||||
var newUser = await CreateUserInternalAsync("admin", dbContext).ConfigureAwait(false);
|
||||
newUser.SetPermission(PermissionKind.IsAdministrator, true);
|
||||
newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
|
||||
newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
|
||||
|
||||
dbContext.Users.Add(newUser);
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
_users.Add(newUser.Id, newUser);
|
||||
|
||||
var reloadedUser = await dbContext.Users
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Include(u => u.Permissions)
|
||||
.Include(u => u.Preferences)
|
||||
.Include(u => u.AccessSchedules)
|
||||
.Include(u => u.ProfileImage)
|
||||
.FirstAsync(u => u.Id.Equals(newUser.Id))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
_users.Add(reloadedUser.Id, reloadedUser);
|
||||
|
||||
// Set the default password "jellyfin"
|
||||
await ChangePassword(reloadedUser, "jellyfin").ConfigureAwait(false);
|
||||
|
||||
_logger.LogWarning("Default admin user created with username 'admin' and password 'jellyfin'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<_PublishTargetUrl>E:\Program Files\Jellyfin</_PublishTargetUrl>
|
||||
<History>True|2026-02-23T17:08:36.6307546Z||;False|2026-02-23T12:02:39.0223565-05:00||;True|2026-02-23T11:48:21.1980918-05:00||;True|2026-02-23T11:13:01.1928466-05:00||;True|2026-02-23T10:32:13.7989634-05:00||;True|2026-02-23T09:02:53.5227868-05:00||;True|2026-02-23T08:11:05.7403694-05:00||;False|2026-02-23T08:07:46.6244256-05:00||;True|2026-02-23T08:00:05.0454829-05:00||;False|2026-02-23T07:56:44.6461434-05:00||;True|2026-02-23T07:50:44.1212024-05:00||;True|2026-02-23T07:29:19.3226285-05:00||;True|2026-02-22T20:02:08.7802640-05:00||;True|2026-02-22T19:00:49.1033285-05:00||;True|2026-02-22T18:46:50.8399883-05:00||;True|2026-02-22T18:36:34.2983199-05:00||;True|2026-02-22T18:33:05.9495883-05:00||;True|2026-02-22T18:28:09.3531365-05:00||;True|2026-02-22T18:23:19.7767548-05:00||;True|2026-02-22T18:03:49.9702878-05:00||;True|2026-02-22T17:31:43.8027839-05:00||;True|2026-02-22T17:14:22.1722691-05:00||;True|2026-02-22T17:07:09.6937759-05:00||;True|2026-02-22T16:29:37.5643134-05:00||;True|2026-02-22T16:05:05.3412117-05:00||;True|2026-02-22T15:59:39.7645693-05:00||;</History>
|
||||
<History>True|2026-02-23T23:25:39.1942435Z||;True|2026-02-23T18:08:02.8170921-05:00||;True|2026-02-23T17:50:19.2488617-05:00||;True|2026-02-23T17:40:34.7963081-05:00||;True|2026-02-23T17:30:42.0185573-05:00||;False|2026-02-23T16:44:17.4036787-05:00||;False|2026-02-23T16:10:39.9498056-05:00||;False|2026-02-23T16:06:47.9500254-05:00||;True|2026-02-23T15:36:20.8511403-05:00||;True|2026-02-23T14:06:04.7110322-05:00||;True|2026-02-23T13:51:39.0686004-05:00||;True|2026-02-23T13:12:08.3890386-05:00||;False|2026-02-23T13:06:12.0188266-05:00||;True|2026-02-23T12:08:36.6307546-05:00||;False|2026-02-23T12:02:39.0223565-05:00||;True|2026-02-23T11:48:21.1980918-05:00||;True|2026-02-23T11:13:01.1928466-05:00||;True|2026-02-23T10:32:13.7989634-05:00||;True|2026-02-23T09:02:53.5227868-05:00||;True|2026-02-23T08:11:05.7403694-05:00||;False|2026-02-23T08:07:46.6244256-05:00||;True|2026-02-23T08:00:05.0454829-05:00||;False|2026-02-23T07:56:44.6461434-05:00||;True|2026-02-23T07:50:44.1212024-05:00||;True|2026-02-23T07:29:19.3226285-05:00||;True|2026-02-22T20:02:08.7802640-05:00||;True|2026-02-22T19:00:49.1033285-05:00||;True|2026-02-22T18:46:50.8399883-05:00||;True|2026-02-22T18:36:34.2983199-05:00||;True|2026-02-22T18:33:05.9495883-05:00||;True|2026-02-22T18:28:09.3531365-05:00||;True|2026-02-22T18:23:19.7767548-05:00||;True|2026-02-22T18:03:49.9702878-05:00||;True|2026-02-22T17:31:43.8027839-05:00||;True|2026-02-22T17:14:22.1722691-05:00||;True|2026-02-22T17:07:09.6937759-05:00||;True|2026-02-22T16:29:37.5643134-05:00||;True|2026-02-22T16:05:05.3412117-05:00||;True|2026-02-22T15:59:39.7645693-05:00||;</History>
|
||||
<LastFailureDetails />
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,200 @@
|
||||
# PostgreSQL Backup Configuration - Implementation Summary
|
||||
|
||||
## What Was Created
|
||||
|
||||
This implementation adds configurable PostgreSQL backup options to Jellyfin that can be configured through the `database.xml` file.
|
||||
|
||||
### New Files Created:
|
||||
|
||||
1. **`DatabaseBackupOptions.cs`**
|
||||
- Location: `src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/`
|
||||
- Contains all configurable backup settings
|
||||
- Properties: PgDumpPath, PgRestorePath, BackupFormat, CompressionLevel, TimeoutSeconds, etc.
|
||||
|
||||
2. **`PostgresBackupService.cs`**
|
||||
- Location: `src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/`
|
||||
- Service class that performs backup and restore operations
|
||||
- Reads configuration from `database.xml`
|
||||
- Executes `pg_dump` and `pg_restore` with configured options
|
||||
|
||||
3. **`database.xml.example`**
|
||||
- Location: `src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/`
|
||||
- Example configuration file showing all available options
|
||||
- Can be copied to Jellyfin's config directory
|
||||
|
||||
4. **`BACKUP_CONFIGURATION.md`**
|
||||
- Location: `src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/`
|
||||
- Complete documentation for end users
|
||||
- Configuration reference and troubleshooting guide
|
||||
|
||||
### Modified Files:
|
||||
|
||||
1. **`DatabaseConfigurationOptions.cs`**
|
||||
- Added `BackupOptions` property to hold backup configuration
|
||||
|
||||
## How It Works
|
||||
|
||||
### Configuration Flow:
|
||||
|
||||
```
|
||||
database.xml
|
||||
↓
|
||||
DatabaseConfigurationOptions.BackupOptions
|
||||
↓
|
||||
PostgresBackupService reads options via IConfigurationManager
|
||||
↓
|
||||
pg_dump/pg_restore executed with configured parameters
|
||||
```
|
||||
|
||||
### Example database.xml:
|
||||
|
||||
```xml
|
||||
<DatabaseConfigurationOptions>
|
||||
<DatabaseType>Jellyfin-PostgreSQL</DatabaseType>
|
||||
|
||||
<CustomProviderOptions>
|
||||
<PluginName>Jellyfin-PostgreSQL</PluginName>
|
||||
<PluginAssembly>Jellyfin.Database.Providers.Postgres</PluginAssembly>
|
||||
<ConnectionString>Host=localhost;Database=jellyfin;Username=jellyfin;Password=pwd</ConnectionString>
|
||||
</CustomProviderOptions>
|
||||
|
||||
<BackupOptions>
|
||||
<PgDumpPath>pg_dump</PgDumpPath>
|
||||
<BackupFormat>custom</BackupFormat>
|
||||
<CompressionLevel>6</CompressionLevel>
|
||||
<IncludeBlobs>true</IncludeBlobs>
|
||||
<TimeoutSeconds>1800</TimeoutSeconds>
|
||||
<VerboseOutput>true</VerboseOutput>
|
||||
</BackupOptions>
|
||||
</DatabaseConfigurationOptions>
|
||||
```
|
||||
|
||||
### Using the Service:
|
||||
|
||||
```csharp
|
||||
// In your DI setup (Startup.cs or similar)
|
||||
services.AddSingleton<PostgresBackupService>();
|
||||
|
||||
// In your code
|
||||
public class BackupManager
|
||||
{
|
||||
private readonly PostgresBackupService _backupService;
|
||||
|
||||
public BackupManager(PostgresBackupService backupService)
|
||||
{
|
||||
_backupService = backupService;
|
||||
}
|
||||
|
||||
public async Task PerformBackup()
|
||||
{
|
||||
// All settings are read from database.xml automatically
|
||||
var backupPath = await _backupService.CreateBackupAsync("/backup/directory");
|
||||
Console.WriteLine($"Backup created: {backupPath}");
|
||||
}
|
||||
|
||||
public async Task RestoreFromBackup(string backupFile)
|
||||
{
|
||||
await _backupService.RestoreBackupAsync(backupFile);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
All options are **optional** with sensible defaults:
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `PgDumpPath` | string | Searches PATH | Path to pg_dump executable |
|
||||
| `PgRestorePath` | string | Searches PATH | Path to pg_restore executable |
|
||||
| `BackupFormat` | string | "custom" | Backup format: custom, plain, directory, tar |
|
||||
| `IncludeBlobs` | bool | true | Include large objects in backup |
|
||||
| `CompressionLevel` | int | 6 | Compression level (0-9) |
|
||||
| `TimeoutSeconds` | int | 1800 | Max backup/restore time (30 min) |
|
||||
| `VerboseOutput` | bool | true | Enable verbose logging |
|
||||
| `ParallelJobs` | int? | null | Number of parallel jobs (directory format) |
|
||||
| `AdditionalArguments` | string? | null | Extra pg_dump arguments |
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **User-Configurable**: Users can customize backup behavior without code changes
|
||||
2. **Flexible**: Supports all pg_dump formats and options
|
||||
3. **Secure**: Uses environment variables for passwords
|
||||
4. **Robust**: Includes timeout handling and error reporting
|
||||
5. **Well-Documented**: Complete documentation for end users
|
||||
6. **Backward Compatible**: All options have defaults, no configuration required for basic use
|
||||
|
||||
## Next Steps
|
||||
|
||||
To integrate this into Jellyfin:
|
||||
|
||||
1. **Register the Service** in your DI container:
|
||||
```csharp
|
||||
services.AddSingleton<PostgresBackupService>();
|
||||
```
|
||||
|
||||
2. **Wire into existing backup system** (if Jellyfin has one):
|
||||
```csharp
|
||||
// In your backup manager/controller
|
||||
if (dbType == "PostgreSQL")
|
||||
{
|
||||
var postgresBackup = serviceProvider.GetRequiredService<PostgresBackupService>();
|
||||
await postgresBackup.CreateBackupAsync(backupDirectory);
|
||||
}
|
||||
```
|
||||
|
||||
3. **Add UI configuration** (optional):
|
||||
- Create admin UI to edit backup options
|
||||
- Save to database.xml through configuration manager
|
||||
|
||||
4. **Testing**:
|
||||
- Test with different backup formats
|
||||
- Test timeout handling
|
||||
- Test restore operations
|
||||
- Test path auto-detection on Windows/Linux
|
||||
|
||||
## Example Usage Scenarios
|
||||
|
||||
### Scenario 1: Basic Backup (default settings)
|
||||
```xml
|
||||
<BackupOptions>
|
||||
<PgDumpPath>pg_dump</PgDumpPath>
|
||||
</BackupOptions>
|
||||
```
|
||||
|
||||
### Scenario 2: High Compression for Storage
|
||||
```xml
|
||||
<BackupOptions>
|
||||
<BackupFormat>custom</BackupFormat>
|
||||
<CompressionLevel>9</CompressionLevel>
|
||||
<TimeoutSeconds>3600</TimeoutSeconds>
|
||||
</BackupOptions>
|
||||
```
|
||||
|
||||
### Scenario 3: Fast Parallel Backup
|
||||
```xml
|
||||
<BackupOptions>
|
||||
<BackupFormat>directory</BackupFormat>
|
||||
<ParallelJobs>4</ParallelJobs>
|
||||
<CompressionLevel>3</CompressionLevel>
|
||||
</BackupOptions>
|
||||
```
|
||||
|
||||
### Scenario 4: Plain SQL Script
|
||||
```xml
|
||||
<BackupOptions>
|
||||
<BackupFormat>plain</BackupFormat>
|
||||
<AdditionalArguments>--exclude-schema=test</AdditionalArguments>
|
||||
</BackupOptions>
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **File Permissions**: Ensure `database.xml` is readable only by the Jellyfin user
|
||||
2. **Backup Storage**: Store backups in a secure location with appropriate permissions
|
||||
3. **Password Handling**: Uses `PGPASSWORD` environment variable (more secure than CLI args)
|
||||
4. **Audit Logging**: All operations are logged with timestamps
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Common issues and solutions are documented in `BACKUP_CONFIGURATION.md`.
|
||||
@@ -0,0 +1,235 @@
|
||||
# PostgreSQL Backup - Local-Only Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
The PostgreSQL backup functionality using `pg_dump` and `pg_restore` has been updated to **ONLY work with local databases** (localhost or 127.0.0.1). For remote PostgreSQL servers, users must manage backups externally.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. **PostgresDatabaseProvider.cs**
|
||||
|
||||
#### Added Fields:
|
||||
```csharp
|
||||
private readonly IConfigurationManager? configurationManager;
|
||||
private PostgresBackupService? backupService;
|
||||
private string? currentHost;
|
||||
```
|
||||
|
||||
#### Updated Constructor:
|
||||
- Added optional `IConfigurationManager` parameter
|
||||
- Allows backup service initialization when configuration is available
|
||||
|
||||
#### New Method: `IsLocalHost()`
|
||||
```csharp
|
||||
private static bool IsLocalHost(string? host)
|
||||
{
|
||||
return host.Equals("localhost", StringComparison.OrdinalIgnoreCase) ||
|
||||
host.Equals("127.0.0.1", StringComparison.Ordinal) ||
|
||||
host.Equals("::1", StringComparison.Ordinal); // IPv6 localhost
|
||||
}
|
||||
```
|
||||
|
||||
#### Updated `Initialise()` Method:
|
||||
- Captures the database host from connection string
|
||||
- Initializes `PostgresBackupService` **only if host is local**
|
||||
- Logs appropriate messages based on whether database is local or remote
|
||||
|
||||
```csharp
|
||||
// Store the host for backup operations
|
||||
currentHost = connectionBuilder.Host;
|
||||
|
||||
// Initialize backup service if host is local and configuration manager is available
|
||||
if (IsLocalHost(currentHost) && configurationManager is not null)
|
||||
{
|
||||
backupService = new PostgresBackupService(backupLogger, configurationManager);
|
||||
logger.LogInformation("PostgreSQL backup service initialized for local database");
|
||||
}
|
||||
else if (!IsLocalHost(currentHost))
|
||||
{
|
||||
logger.LogInformation("PostgreSQL database is on remote server ({Host}). Backup operations via pg_dump/pg_restore are disabled", currentHost);
|
||||
}
|
||||
```
|
||||
|
||||
#### Updated Backup Methods:
|
||||
|
||||
**`MigrationBackupFast()`**:
|
||||
- **Local database**: Uses `PostgresBackupService.CreateBackupAsync()`
|
||||
- **Remote database**: Returns timestamp key, logs warning
|
||||
|
||||
**`RestoreBackupFast()`**:
|
||||
- **Local database**: Uses `PostgresBackupService.RestoreBackupAsync()`
|
||||
- **Remote database**: Logs warning only
|
||||
|
||||
**`DeleteBackup()`**:
|
||||
- **Local database**: Deletes backup file from disk
|
||||
- **Remote database**: Logs warning
|
||||
|
||||
### 2. **Updated Log Messages**
|
||||
|
||||
All messages now specify "on a remote server" for clarity:
|
||||
|
||||
| Old Message | New Message |
|
||||
|-------------|-------------|
|
||||
| "Backup deletion is not implemented for PostgreSQL. Manage backups externally." | "Backup deletion is not implemented for PostgreSQL **on a remote server**. Manage backups externally." |
|
||||
| "Fast backup is not implemented for PostgreSQL. Use pg_dump for proper backups." | "Fast backup is not implemented for PostgreSQL **on a remote server**. Use pg_dump manually for proper backups." |
|
||||
| "Fast restore is not implemented for PostgreSQL. Use pg_restore for proper restoration." | "Fast restore is not implemented for PostgreSQL **on a remote server**. Use pg_restore manually for proper restoration." |
|
||||
|
||||
### 3. **Updated Configuration Documentation**
|
||||
|
||||
**database.xml.example**:
|
||||
```xml
|
||||
<!--
|
||||
IMPORTANT: pg_dump/pg_restore backup features are ONLY available when the
|
||||
PostgreSQL database host is localhost or 127.0.0.1. For remote databases,
|
||||
you must manage backups externally using pg_dump/pg_restore commands manually.
|
||||
-->
|
||||
```
|
||||
|
||||
**BACKUP_CONFIGURATION.md**:
|
||||
- Added prominent warning about local-only requirement
|
||||
- Clarified behavior for remote databases
|
||||
- Updated examples and troubleshooting sections
|
||||
|
||||
## Behavior Matrix
|
||||
|
||||
| Database Host | Backup Service Initialized? | Backup Operations | Log Message |
|
||||
|---------------|---------------------------|-------------------|-------------|
|
||||
| `localhost` | ✅ Yes | Fully functional via pg_dump/pg_restore | "PostgreSQL backup service initialized for local database" |
|
||||
| `127.0.0.1` | ✅ Yes | Fully functional via pg_dump/pg_restore | "PostgreSQL backup service initialized for local database" |
|
||||
| `::1` (IPv6) | ✅ Yes | Fully functional via pg_dump/pg_restore | "PostgreSQL backup service initialized for local database" |
|
||||
| `192.168.1.10` (remote) | ❌ No | Disabled, external management required | "PostgreSQL database is on remote server ({Host}). Backup operations via pg_dump/pg_restore are disabled" |
|
||||
| `db.example.com` (remote) | ❌ No | Disabled, external management required | "PostgreSQL database is on remote server ({Host}). Backup operations via pg_dump/pg_restore are disabled" |
|
||||
|
||||
## Example Configurations
|
||||
|
||||
### Local Database (Backups Enabled):
|
||||
|
||||
```xml
|
||||
<DatabaseConfigurationOptions>
|
||||
<DatabaseType>Jellyfin-PostgreSQL</DatabaseType>
|
||||
|
||||
<CustomProviderOptions>
|
||||
<PluginName>Jellyfin-PostgreSQL</PluginName>
|
||||
<PluginAssembly>Jellyfin.Database.Providers.Postgres</PluginAssembly>
|
||||
<ConnectionString>Host=localhost;Port=5432;Database=jellyfin;Username=jellyfin;Password=pwd</ConnectionString>
|
||||
</CustomProviderOptions>
|
||||
|
||||
<BackupOptions>
|
||||
<PgDumpPath>pg_dump</PgDumpPath>
|
||||
<BackupFormat>custom</BackupFormat>
|
||||
<CompressionLevel>6</CompressionLevel>
|
||||
</BackupOptions>
|
||||
</DatabaseConfigurationOptions>
|
||||
```
|
||||
|
||||
**Result**: ✅ Backups work automatically via pg_dump/pg_restore
|
||||
|
||||
### Remote Database (Backups Disabled):
|
||||
|
||||
```xml
|
||||
<DatabaseConfigurationOptions>
|
||||
<DatabaseType>Jellyfin-PostgreSQL</DatabaseType>
|
||||
|
||||
<CustomProviderOptions>
|
||||
<PluginName>Jellyfin-PostgreSQL</PluginName>
|
||||
<PluginAssembly>Jellyfin.Database.Providers.Postgres</PluginAssembly>
|
||||
<ConnectionString>Host=192.168.1.50;Port=5432;Database=jellyfin;Username=jellyfin;Password=pwd</ConnectionString>
|
||||
</CustomProviderOptions>
|
||||
|
||||
<!-- BackupOptions are ignored for remote databases -->
|
||||
<BackupOptions>
|
||||
<PgDumpPath>pg_dump</PgDumpPath>
|
||||
</BackupOptions>
|
||||
</DatabaseConfigurationOptions>
|
||||
```
|
||||
|
||||
**Result**: ❌ Backup operations log warnings; users must manually run:
|
||||
```bash
|
||||
# Manual backup on remote server
|
||||
pg_dump -h 192.168.1.50 -p 5432 -U jellyfin -d jellyfin -F c -f backup.dump
|
||||
|
||||
# Manual restore on remote server
|
||||
pg_restore -h 192.168.1.50 -p 5432 -U jellyfin -d jellyfin backup.dump
|
||||
```
|
||||
|
||||
## Security Rationale
|
||||
|
||||
This restriction exists because:
|
||||
|
||||
1. **Local Access Required**: `pg_dump` and `pg_restore` must be executed on the same machine as the PostgreSQL server for reliable filesystem access
|
||||
2. **Credentials**: Running pg_dump against remote servers requires storing remote credentials
|
||||
3. **Network Overhead**: Large backups over network connections can be slow and unreliable
|
||||
4. **Best Practice**: Remote PostgreSQL deployments should have their own backup infrastructure (scheduled pg_dump jobs, WAL archiving, replication, etc.)
|
||||
|
||||
## Migration Path
|
||||
|
||||
If you previously had a remote PostgreSQL setup expecting automatic backups:
|
||||
|
||||
### Before (Not Working):
|
||||
- Jellyfin connected to remote PostgreSQL
|
||||
- Expected automatic backups (didn't work)
|
||||
|
||||
### After (Clear Behavior):
|
||||
- Jellyfin connects to remote PostgreSQL
|
||||
- Clear log messages indicate backups are disabled
|
||||
- Documentation provides manual backup commands
|
||||
|
||||
### Recommended Setup for Remote Databases:
|
||||
|
||||
1. **On the PostgreSQL server**, set up a cron job:
|
||||
```bash
|
||||
# /etc/cron.daily/jellyfin-backup.sh
|
||||
pg_dump -U jellyfin -d jellyfin -F c -f /backups/jellyfin_$(date +%Y%m%d).dump
|
||||
```
|
||||
|
||||
2. **Use PostgreSQL's native backup features**:
|
||||
- Continuous archiving (WAL archiving)
|
||||
- Streaming replication
|
||||
- Point-in-time recovery (PITR)
|
||||
- pgBackRest or Barman for enterprise backups
|
||||
|
||||
3. **Cloud-native solutions**:
|
||||
- AWS RDS automated backups
|
||||
- Azure Database for PostgreSQL automated backups
|
||||
- Google Cloud SQL automated backups
|
||||
|
||||
## Testing
|
||||
|
||||
To test the local vs. remote behavior:
|
||||
|
||||
```csharp
|
||||
// Test with localhost
|
||||
var localConfig = new DatabaseConfigurationOptions
|
||||
{
|
||||
CustomProviderOptions = new CustomDatabaseOptions
|
||||
{
|
||||
Options = new[]
|
||||
{
|
||||
new CustomDatabaseOption { Key = "host", Value = "localhost" }
|
||||
}
|
||||
}
|
||||
};
|
||||
// Result: Backup service initialized
|
||||
|
||||
// Test with remote host
|
||||
var remoteConfig = new DatabaseConfigurationOptions
|
||||
{
|
||||
CustomProviderOptions = new CustomDatabaseOptions
|
||||
{
|
||||
Options = new[]
|
||||
{
|
||||
new CustomDatabaseOption { Key = "host", Value = "192.168.1.50" }
|
||||
}
|
||||
}
|
||||
};
|
||||
// Result: Backup service NOT initialized, warning logged
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
✅ **Local databases (localhost/127.0.0.1)**: Full automated backup support via pg_dump/pg_restore
|
||||
❌ **Remote databases**: Backup operations disabled, clear messaging, external management required
|
||||
📝 **Documentation**: Updated to clearly explain the local-only restriction
|
||||
🔒 **Security**: Follows best practices for remote database backup management
|
||||
|
||||
This implementation provides a clear, safe, and maintainable approach to PostgreSQL backups in Jellyfin.
|
||||
@@ -0,0 +1,167 @@
|
||||
# PostgreSQL Backup Logging - External Tool References
|
||||
|
||||
## Overview
|
||||
|
||||
All logging messages have been updated to explicitly clarify that `pg_dump` and `pg_restore` are **external command-line tools** being executed as separate processes, not internal library functions.
|
||||
|
||||
## Updated Log Messages
|
||||
|
||||
### PostgresBackupService.cs
|
||||
|
||||
#### Backup Creation:
|
||||
```csharp
|
||||
// Before:
|
||||
"Starting PostgreSQL backup to {BackupPath}"
|
||||
"Executing: {PgDump} {Arguments}"
|
||||
"Successfully created PostgreSQL backup at {BackupPath}"
|
||||
|
||||
// After:
|
||||
"Starting PostgreSQL backup using external pg_dump tool to {BackupPath}"
|
||||
"Executing external command: {PgDump} {Arguments}"
|
||||
"Successfully created PostgreSQL backup using external pg_dump at {BackupPath}"
|
||||
```
|
||||
|
||||
#### Backup Restoration:
|
||||
```csharp
|
||||
// Before:
|
||||
"Starting PostgreSQL restore from {BackupPath}"
|
||||
"Successfully restored PostgreSQL backup from {BackupPath}"
|
||||
|
||||
// After:
|
||||
"Starting PostgreSQL restore using external pg_restore tool from {BackupPath}"
|
||||
"Successfully restored PostgreSQL backup using external pg_restore from {BackupPath}"
|
||||
```
|
||||
|
||||
#### Backup Deletion:
|
||||
```csharp
|
||||
// Before (in PostgresDatabaseProvider):
|
||||
"Deleted local PostgreSQL backup: {BackupPath}"
|
||||
|
||||
// After:
|
||||
"Deleted local PostgreSQL backup created by external pg_dump: {BackupPath}"
|
||||
```
|
||||
|
||||
#### Error Messages:
|
||||
```csharp
|
||||
// Before:
|
||||
"{defaultExecutableName} executable not found. Please configure the path in database.xml or ensure it's in the system PATH."
|
||||
|
||||
// After:
|
||||
"External {defaultExecutableName} executable not found. Please configure the path in database.xml or ensure PostgreSQL client tools are installed and in the system PATH."
|
||||
```
|
||||
|
||||
### PostgresDatabaseProvider.cs
|
||||
|
||||
#### Initialization Messages:
|
||||
```csharp
|
||||
// Before:
|
||||
"PostgreSQL backup service initialized for local database"
|
||||
"PostgreSQL database is on remote server ({Host}). Backup operations via pg_dump/pg_restore are disabled"
|
||||
|
||||
// After:
|
||||
"PostgreSQL backup service initialized for local database (will use external pg_dump/pg_restore tools)"
|
||||
"PostgreSQL database is on remote server ({Host}). Backup operations via external pg_dump/pg_restore tools are disabled"
|
||||
```
|
||||
|
||||
#### Backup Operation Messages:
|
||||
```csharp
|
||||
// Before:
|
||||
"PostgreSQL local backup created: {BackupPath}"
|
||||
"Failed to create local PostgreSQL backup using pg_dump"
|
||||
"Fast backup is not implemented for PostgreSQL on a remote server. Use pg_dump manually..."
|
||||
|
||||
// After:
|
||||
"PostgreSQL local backup created using external pg_dump: {BackupPath}"
|
||||
"Failed to create local PostgreSQL backup using external pg_dump"
|
||||
"Fast backup is not implemented for PostgreSQL on a remote server. Use external pg_dump command manually..."
|
||||
```
|
||||
|
||||
#### Restore Operation Messages:
|
||||
```csharp
|
||||
// Before:
|
||||
"PostgreSQL local backup restored from: {BackupPath}"
|
||||
"Failed to restore local PostgreSQL backup using pg_restore"
|
||||
"Fast restore is not implemented for PostgreSQL on a remote server. Use pg_restore manually..."
|
||||
|
||||
// After:
|
||||
"PostgreSQL local backup restored using external pg_restore from: {BackupPath}"
|
||||
"Failed to restore local PostgreSQL backup using external pg_restore"
|
||||
"Fast restore is not implemented for PostgreSQL on a remote server. Use external pg_restore command manually..."
|
||||
```
|
||||
|
||||
#### Delete Operation Messages:
|
||||
```csharp
|
||||
// Before:
|
||||
"Failed to delete local PostgreSQL backup: {BackupKey}"
|
||||
"Backup deletion is not implemented for PostgreSQL on a remote server. Manage backups externally."
|
||||
|
||||
// After:
|
||||
"Failed to delete local PostgreSQL backup created by external pg_dump: {BackupKey}"
|
||||
"Backup deletion is not implemented for PostgreSQL on a remote server. Manage backups created by external pg_dump/pg_restore manually."
|
||||
```
|
||||
|
||||
## Complete Log Flow Examples
|
||||
|
||||
### Successful Local Backup:
|
||||
```
|
||||
[INFO] PostgreSQL backup service initialized for local database (will use external pg_dump/pg_restore tools)
|
||||
[INFO] Starting PostgreSQL backup using external pg_dump tool to /data/backups/jellyfin_postgres_backup_20250128_143022.dump
|
||||
[DEBUG] Executing external command: pg_dump -h localhost -p 5432 -U jellyfin -d jellyfin -F c -Z 6 -b -v -f "/data/backups/jellyfin_postgres_backup_20250128_143022.dump"
|
||||
[INFO] Successfully created PostgreSQL backup using external pg_dump at /data/backups/jellyfin_postgres_backup_20250128_143022.dump (15728640 bytes)
|
||||
[INFO] PostgreSQL local backup created using external pg_dump: /data/backups/jellyfin_postgres_backup_20250128_143022.dump
|
||||
```
|
||||
|
||||
### Successful Local Restore:
|
||||
```
|
||||
[INFO] Starting PostgreSQL restore using external pg_restore tool from /data/backups/jellyfin_postgres_backup_20250128_143022.dump
|
||||
[INFO] Successfully restored PostgreSQL backup using external pg_restore from /data/backups/jellyfin_postgres_backup_20250128_143022.dump
|
||||
[INFO] PostgreSQL local backup restored using external pg_restore from: /data/backups/jellyfin_postgres_backup_20250128_143022.dump
|
||||
```
|
||||
|
||||
### Successful Local Delete:
|
||||
```
|
||||
[INFO] Deleted local PostgreSQL backup created by external pg_dump: /data/backups/jellyfin_postgres_backup_20250128_143022.dump
|
||||
```
|
||||
|
||||
### Remote Database (Backups Disabled):
|
||||
```
|
||||
[INFO] PostgreSQL database is on remote server (db.example.com). Backup operations via external pg_dump/pg_restore tools are disabled
|
||||
[WARN] Fast backup is not implemented for PostgreSQL on a remote server. Use external pg_dump command manually for proper backups.
|
||||
```
|
||||
|
||||
### Error: External Tool Not Found:
|
||||
```
|
||||
[ERROR] External pg_dump executable not found. Please configure the path in database.xml or ensure PostgreSQL client tools are installed and in the system PATH.
|
||||
```
|
||||
|
||||
## Benefits of Updated Messaging
|
||||
|
||||
1. **Clarity**: Users understand that these are external processes, not built-in functionality
|
||||
2. **Troubleshooting**: Makes it clear that PostgreSQL client tools must be installed
|
||||
3. **Expectations**: Sets proper expectations about what the system is doing
|
||||
4. **Documentation**: Helps users understand the requirement for external dependencies
|
||||
5. **Transparency**: Makes the technical implementation more transparent
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Process Execution**: `System.Diagnostics.Process.Start()` is used to launch pg_dump/pg_restore
|
||||
- **Environment Variables**: `PGPASSWORD` is set in the process environment for authentication
|
||||
- **Standard Output/Error**: Captured for logging and error handling
|
||||
- **Working Directory**: Backup files are created in the Jellyfin data directory
|
||||
- **Timeout Handling**: Configurable timeout prevents hung processes
|
||||
|
||||
## For Users
|
||||
|
||||
These updated messages help users understand that:
|
||||
1. Jellyfin requires PostgreSQL client tools to be installed
|
||||
2. The backup functionality calls out to external commands
|
||||
3. Path configuration in `database.xml` points to actual executables
|
||||
4. Error messages about missing executables mean the tools need to be installed
|
||||
|
||||
## For Developers
|
||||
|
||||
The updated logging makes it clear:
|
||||
1. We're using process execution, not a library
|
||||
2. External dependencies are required
|
||||
3. Configuration affects external tool execution
|
||||
4. Debugging should include checking if tools are in PATH or configured correctly
|
||||
Binary file not shown.
Binary file not shown.
+2
-1
@@ -1,6 +1,7 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
@@ -13,7 +14,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Jellyfin.CodeAnalysis")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+86883cd5c6e26f1ad8f5d26aeb97f15f859def57")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+43cdbf9b350e608dce121070d565cdce0aea6b17")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Jellyfin.CodeAnalysis")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Jellyfin.CodeAnalysis")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
519a49780b327d7db1c693a3f7b3613e79c1adca3797d68beef6d8a27561c8f3
|
||||
bdef8325a5a74be1c67e80979e05ef8eaf8569901dd2077003e91c35b6d8e7dd
|
||||
|
||||
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
+75
@@ -0,0 +1,75 @@
|
||||
// <copyright file="DatabaseBackupOptions.cs" company="PlaceholderCompany">
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Jellyfin.Database.Implementations.DbConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Options to configure database backup behavior.
|
||||
/// </summary>
|
||||
public class DatabaseBackupOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the pg_dump executable.
|
||||
/// If not specified, will search in system PATH.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Examples:
|
||||
/// - Windows: "C:\\Program Files\\PostgreSQL\\16\\bin\\pg_dump.exe"
|
||||
/// - Linux: "/usr/bin/pg_dump"
|
||||
/// - Use "pg_dump" to search in PATH.
|
||||
/// </remarks>
|
||||
public string? PgDumpPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the pg_restore executable.
|
||||
/// If not specified, will search in system PATH.
|
||||
/// </summary>
|
||||
public string? PgRestorePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the backup format.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Valid values:
|
||||
/// - "custom" (default): Custom compressed format (pg_restore compatible)
|
||||
/// - "plain": Plain SQL script
|
||||
/// - "directory": Directory format
|
||||
/// - "tar": Tar archive format.
|
||||
/// </remarks>
|
||||
public string BackupFormat { get; set; } = "custom";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to include large objects (blobs) in backup.
|
||||
/// </summary>
|
||||
public bool IncludeBlobs { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the compression level (0-9, where 9 is maximum compression).
|
||||
/// Only applies to custom and directory formats.
|
||||
/// </summary>
|
||||
public int CompressionLevel { get; set; } = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional pg_dump arguments.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Example: "--verbose --exclude-table-data=public.logs".
|
||||
/// </remarks>
|
||||
public string? AdditionalArguments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timeout in seconds for backup operations.
|
||||
/// </summary>
|
||||
public int TimeoutSeconds { get; set; } = 1800; // 30 minutes
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable verbose output.
|
||||
/// </summary>
|
||||
public bool VerboseOutput { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of parallel jobs for pg_dump (if format is 'directory').
|
||||
/// </summary>
|
||||
public int? ParallelJobs { get; set; }
|
||||
}
|
||||
+5
-2
@@ -4,8 +4,6 @@
|
||||
|
||||
namespace Jellyfin.Database.Implementations.DbConfiguration;
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Options to configure jellyfins managed database.
|
||||
/// </summary>
|
||||
@@ -26,4 +24,9 @@ public class DatabaseConfigurationOptions
|
||||
/// Defaults to "NoLock".
|
||||
/// </summary>
|
||||
public DatabaseLockingBehaviorTypes LockingBehavior { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the backup configuration options.
|
||||
/// </summary>
|
||||
public DatabaseBackupOptions? BackupOptions { get; set; }
|
||||
}
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
<!--
|
||||
Example database.xml configuration for Jellyfin with PostgreSQL backup options
|
||||
Place this file in your Jellyfin config directory
|
||||
-->
|
||||
<DatabaseConfigurationOptions>
|
||||
<!-- Database type: "Jellyfin-PostgreSQL" or "Jellyfin-Sqlite" -->
|
||||
<DatabaseType>Jellyfin-PostgreSQL</DatabaseType>
|
||||
|
||||
<!-- Locking behavior: "NoLock", "Pessimistic", or "Optimistic" -->
|
||||
<LockingBehavior>NoLock</LockingBehavior>
|
||||
|
||||
<!-- Custom database provider options (for PostgreSQL) -->
|
||||
<CustomProviderOptions>
|
||||
<PluginName>Jellyfin-PostgreSQL</PluginName>
|
||||
<PluginAssembly>Jellyfin.Database.Providers.Postgres</PluginAssembly>
|
||||
<ConnectionString>Host=localhost;Port=5432;Database=jellyfin;Username=jellyfin;Password=your_password_here</ConnectionString>
|
||||
|
||||
<Options>
|
||||
<!-- Optional: Override host -->
|
||||
<CustomDatabaseOption>
|
||||
<Key>Host</Key>
|
||||
<Value>localhost</Value>
|
||||
</CustomDatabaseOption>
|
||||
|
||||
<!-- Optional: Override port -->
|
||||
<CustomDatabaseOption>
|
||||
<Key>Port</Key>
|
||||
<Value>5432</Value>
|
||||
</CustomDatabaseOption>
|
||||
|
||||
<!-- Optional: Override database name -->
|
||||
<CustomDatabaseOption>
|
||||
<Key>Database</Key>
|
||||
<Value>jellyfin</Value>
|
||||
</CustomDatabaseOption>
|
||||
</Options>
|
||||
</CustomProviderOptions>
|
||||
|
||||
<!-- Database Backup Configuration -->
|
||||
<!-- NOTE: Backup features using pg_dump/pg_restore are ONLY enabled when -->
|
||||
<!-- the database host is 'localhost' or '127.0.0.1'. For remote databases, -->
|
||||
<!-- these settings will be ignored and you must manage backups externally. -->
|
||||
<BackupOptions>
|
||||
<!-- Path to pg_dump executable (optional, will search PATH if not specified) -->
|
||||
<!-- Windows example: C:\Program Files\PostgreSQL\16\bin\pg_dump.exe -->
|
||||
<!-- Linux example: /usr/bin/pg_dump -->
|
||||
<PgDumpPath>pg_dump</PgDumpPath>
|
||||
|
||||
<!-- Path to pg_restore executable (optional) -->
|
||||
<PgRestorePath>pg_restore</PgRestorePath>
|
||||
|
||||
<!-- Backup format: "custom" (default), "plain", "directory", or "tar" -->
|
||||
<BackupFormat>custom</BackupFormat>
|
||||
|
||||
<!-- Include large objects/blobs in backup -->
|
||||
<IncludeBlobs>true</IncludeBlobs>
|
||||
|
||||
<!-- Compression level (0-9, where 9 is maximum compression) -->
|
||||
<CompressionLevel>6</CompressionLevel>
|
||||
|
||||
<!-- Timeout in seconds for backup operations (default: 1800 = 30 minutes) -->
|
||||
<TimeoutSeconds>1800</TimeoutSeconds>
|
||||
|
||||
<!-- Enable verbose output in logs -->
|
||||
<VerboseOutput>true</VerboseOutput>
|
||||
|
||||
<!-- Number of parallel jobs (only for 'directory' format) -->
|
||||
<!-- <ParallelJobs>4</ParallelJobs> -->
|
||||
|
||||
<!-- Additional pg_dump arguments (optional) -->
|
||||
<!-- <AdditionalArguments>--exclude-table-data=public.temp_logs</AdditionalArguments> -->
|
||||
</BackupOptions>
|
||||
</DatabaseConfigurationOptions>
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// <copyright file="HomeSectionConfiguration.cs" company="PlaceholderCompany">
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Jellyfin.Database.Implementations.ModelConfiguration
|
||||
{
|
||||
using System;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
/// <summary>
|
||||
/// FluentAPI configuration for the HomeSection entity.
|
||||
/// </summary>
|
||||
public class HomeSectionConfiguration : IEntityTypeConfiguration<HomeSection>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<HomeSection> builder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
// Explicitly set table name to singular to match the migration
|
||||
builder.ToTable("HomeSection", "displaypreferences");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
|
||||
builder.Property(e => e.Order)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.Type)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.DisplayPreferencesId)
|
||||
.IsRequired();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
# PostgreSQL Backup Configuration
|
||||
|
||||
This document explains how to configure PostgreSQL backup options in Jellyfin using the `database.xml` configuration file.
|
||||
|
||||
## **IMPORTANT: Local Database Requirement**
|
||||
|
||||
**pg_dump and pg_restore backup features are ONLY available when the PostgreSQL database host is `localhost` or `127.0.0.1` (or `::1` for IPv6).**
|
||||
|
||||
- ✅ **Local database** (localhost/127.0.0.1): Automated backups via pg_dump/pg_restore are enabled
|
||||
- ❌ **Remote database**: Backup settings are ignored; you must manage backups externally
|
||||
|
||||
When using a remote PostgreSQL server, Jellyfin will log:
|
||||
> "PostgreSQL database is on remote server ({Host}). Backup operations via pg_dump/pg_restore are disabled"
|
||||
|
||||
And backup operations will show:
|
||||
> "Backup deletion is not implemented for PostgreSQL on a remote server. Manage backups externally."
|
||||
|
||||
## Configuration File Location
|
||||
|
||||
The `database.xml` file should be placed in your Jellyfin configuration directory:
|
||||
|
||||
- **Windows**: `%AppData%\Jellyfin\Server\config\database.xml`
|
||||
- **Linux**: `~/.config/jellyfin/config/database.xml` or `/etc/jellyfin/config/database.xml`
|
||||
- **Docker**: Mount to `/config/database.xml`
|
||||
|
||||
## Basic Configuration
|
||||
|
||||
Here's a minimal configuration for PostgreSQL with default backup settings:
|
||||
|
||||
```xml
|
||||
<DatabaseConfigurationOptions>
|
||||
<DatabaseType>Jellyfin-PostgreSQL</DatabaseType>
|
||||
<LockingBehavior>NoLock</LockingBehavior>
|
||||
|
||||
<CustomProviderOptions>
|
||||
<PluginName>Jellyfin-PostgreSQL</PluginName>
|
||||
<PluginAssembly>Jellyfin.Database.Providers.Postgres</PluginAssembly>
|
||||
<ConnectionString>Host=localhost;Port=5432;Database=jellyfin;Username=jellyfin;Password=your_password</ConnectionString>
|
||||
</CustomProviderOptions>
|
||||
|
||||
<!-- Backup configuration (all settings are optional) -->
|
||||
<BackupOptions>
|
||||
<PgDumpPath>pg_dump</PgDumpPath>
|
||||
<BackupFormat>custom</BackupFormat>
|
||||
</BackupOptions>
|
||||
</DatabaseConfigurationOptions>
|
||||
```
|
||||
|
||||
## Backup Options Reference
|
||||
|
||||
### PgDumpPath
|
||||
**Type**: `string` (optional)
|
||||
**Default**: Searches in system PATH
|
||||
|
||||
The full path to the `pg_dump` executable. If not specified, Jellyfin will search common locations:
|
||||
- Windows: `C:\Program Files\PostgreSQL\{version}\bin\pg_dump.exe`
|
||||
- Linux: `/usr/bin/pg_dump`, `/usr/local/bin/pg_dump`
|
||||
|
||||
**Examples**:
|
||||
```xml
|
||||
<!-- Windows -->
|
||||
<PgDumpPath>C:\Program Files\PostgreSQL\16\bin\pg_dump.exe</PgDumpPath>
|
||||
|
||||
<!-- Linux -->
|
||||
<PgDumpPath>/usr/bin/pg_dump</PgDumpPath>
|
||||
|
||||
<!-- Search in PATH -->
|
||||
<PgDumpPath>pg_dump</PgDumpPath>
|
||||
```
|
||||
|
||||
### PgRestorePath
|
||||
**Type**: `string` (optional)
|
||||
**Default**: Searches in system PATH
|
||||
|
||||
Path to the `pg_restore` executable. Same search behavior as `PgDumpPath`.
|
||||
|
||||
### BackupFormat
|
||||
**Type**: `string`
|
||||
**Default**: `custom`
|
||||
**Valid values**: `custom`, `plain`, `directory`, `tar`
|
||||
|
||||
The backup file format:
|
||||
- **custom**: Compressed custom format (recommended) - can be restored with `pg_restore`
|
||||
- **plain**: Plain SQL script - can be restored with `psql`
|
||||
- **directory**: Directory format with one file per table
|
||||
- **tar**: Tar archive format
|
||||
|
||||
```xml
|
||||
<BackupFormat>custom</BackupFormat>
|
||||
```
|
||||
|
||||
### IncludeBlobs
|
||||
**Type**: `boolean`
|
||||
**Default**: `true`
|
||||
|
||||
Whether to include large objects (BLOBs) in the backup.
|
||||
|
||||
```xml
|
||||
<IncludeBlobs>true</IncludeBlobs>
|
||||
```
|
||||
|
||||
### CompressionLevel
|
||||
**Type**: `integer` (0-9)
|
||||
**Default**: `6`
|
||||
|
||||
Compression level for custom and directory formats. Higher values mean better compression but slower speed.
|
||||
- `0` = No compression
|
||||
- `9` = Maximum compression
|
||||
- `6` = Good balance (recommended)
|
||||
|
||||
```xml
|
||||
<CompressionLevel>6</CompressionLevel>
|
||||
```
|
||||
|
||||
### TimeoutSeconds
|
||||
**Type**: `integer`
|
||||
**Default**: `1800` (30 minutes)
|
||||
|
||||
Maximum time in seconds to wait for backup/restore operations to complete.
|
||||
|
||||
```xml
|
||||
<TimeoutSeconds>1800</TimeoutSeconds>
|
||||
```
|
||||
|
||||
### VerboseOutput
|
||||
**Type**: `boolean`
|
||||
**Default**: `true`
|
||||
|
||||
Enable verbose logging output from pg_dump/pg_restore operations.
|
||||
|
||||
```xml
|
||||
<VerboseOutput>true</VerboseOutput>
|
||||
```
|
||||
|
||||
### ParallelJobs
|
||||
**Type**: `integer` (optional)
|
||||
**Default**: `null`
|
||||
|
||||
Number of parallel jobs for backup operations. Only works with `directory` format.
|
||||
|
||||
```xml
|
||||
<BackupFormat>directory</BackupFormat>
|
||||
<ParallelJobs>4</ParallelJobs>
|
||||
```
|
||||
|
||||
### AdditionalArguments
|
||||
**Type**: `string` (optional)
|
||||
**Default**: `null`
|
||||
|
||||
Additional command-line arguments to pass to pg_dump.
|
||||
|
||||
**Examples**:
|
||||
```xml
|
||||
<!-- Exclude specific table data -->
|
||||
<AdditionalArguments>--exclude-table-data=public.temp_logs</AdditionalArguments>
|
||||
|
||||
<!-- Exclude schema -->
|
||||
<AdditionalArguments>--exclude-schema=archive</AdditionalArguments>
|
||||
|
||||
<!-- Multiple arguments -->
|
||||
<AdditionalArguments>--exclude-table-data=public.logs --exclude-schema=test</AdditionalArguments>
|
||||
```
|
||||
|
||||
## Complete Example Configuration
|
||||
|
||||
```xml
|
||||
<DatabaseConfigurationOptions>
|
||||
<DatabaseType>Jellyfin-PostgreSQL</DatabaseType>
|
||||
<LockingBehavior>NoLock</LockingBehavior>
|
||||
|
||||
<CustomProviderOptions>
|
||||
<PluginName>Jellyfin-PostgreSQL</PluginName>
|
||||
<PluginAssembly>Jellyfin.Database.Providers.Postgres</PluginAssembly>
|
||||
<ConnectionString>Host=localhost;Port=5432;Database=jellyfin;Username=jellyfin;Password=your_password</ConnectionString>
|
||||
</CustomProviderOptions>
|
||||
|
||||
<BackupOptions>
|
||||
<!-- Paths to executables -->
|
||||
<PgDumpPath>C:\Program Files\PostgreSQL\16\bin\pg_dump.exe</PgDumpPath>
|
||||
<PgRestorePath>C:\Program Files\PostgreSQL\16\bin\pg_restore.exe</PgRestorePath>
|
||||
|
||||
<!-- Backup format and compression -->
|
||||
<BackupFormat>custom</BackupFormat>
|
||||
<CompressionLevel>6</CompressionLevel>
|
||||
<IncludeBlobs>true</IncludeBlobs>
|
||||
|
||||
<!-- Performance and timeout -->
|
||||
<TimeoutSeconds>3600</TimeoutSeconds>
|
||||
<VerboseOutput>true</VerboseOutput>
|
||||
|
||||
<!-- Optional: Parallel processing for directory format -->
|
||||
<!-- <ParallelJobs>4</ParallelJobs> -->
|
||||
|
||||
<!-- Optional: Additional pg_dump arguments -->
|
||||
<!-- <AdditionalArguments>--exclude-table-data=public.temp_data</AdditionalArguments> -->
|
||||
</BackupOptions>
|
||||
</DatabaseConfigurationOptions>
|
||||
```
|
||||
|
||||
## Usage in Code
|
||||
|
||||
The backup service will automatically read these settings from `database.xml`:
|
||||
|
||||
```csharp
|
||||
// Inject the service
|
||||
public class MyClass
|
||||
{
|
||||
private readonly PostgresBackupService _backupService;
|
||||
|
||||
public MyClass(PostgresBackupService backupService)
|
||||
{
|
||||
_backupService = backupService;
|
||||
}
|
||||
|
||||
// Create a backup
|
||||
public async Task CreateBackup()
|
||||
{
|
||||
var backupPath = await _backupService.CreateBackupAsync("/path/to/backups");
|
||||
Console.WriteLine($"Backup created: {backupPath}");
|
||||
}
|
||||
|
||||
// Restore a backup
|
||||
public async Task RestoreBackup(string backupFile)
|
||||
{
|
||||
await _backupService.RestoreBackupAsync(backupFile);
|
||||
Console.WriteLine("Backup restored successfully");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "pg_dump executable not found"
|
||||
1. Verify PostgreSQL is installed
|
||||
2. Check that `pg_dump` is in your system PATH, or
|
||||
3. Specify the full path in `<PgDumpPath>` configuration
|
||||
|
||||
### Permission Issues
|
||||
- Ensure the user running Jellyfin has execute permissions for `pg_dump`
|
||||
- Verify database credentials have backup privileges
|
||||
|
||||
### Backup Takes Too Long
|
||||
- Increase `<TimeoutSeconds>` value
|
||||
- Consider using `<ParallelJobs>` with directory format for large databases
|
||||
- Reduce `<CompressionLevel>` for faster backups
|
||||
|
||||
### Restore Fails
|
||||
- Verify the backup file format matches what pg_restore expects
|
||||
- Check that the target database exists and is accessible
|
||||
- Review logs for specific error messages
|
||||
|
||||
## Security Notes
|
||||
|
||||
1. **Password Storage**: The connection string in `database.xml` contains the database password. Ensure this file has appropriate permissions (readable only by the Jellyfin user).
|
||||
|
||||
2. **PGPASSWORD**: The service uses the `PGPASSWORD` environment variable to pass credentials to pg_dump, which is more secure than command-line arguments.
|
||||
|
||||
3. **Backup Files**: Backup files contain your entire database. Store them securely and restrict access appropriately.
|
||||
@@ -0,0 +1,481 @@
|
||||
// <copyright file="PostgresBackupService.cs" company="PlaceholderCompany">
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Jellyfin.Database.Providers.Postgres;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations.DbConfiguration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Service for creating and restoring PostgreSQL database backups using pg_dump/pg_restore.
|
||||
/// </summary>
|
||||
public class PostgresBackupService
|
||||
{
|
||||
private readonly ILogger<PostgresBackupService> _logger;
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PostgresBackupService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="configurationManager">The configuration manager.</param>
|
||||
public PostgresBackupService(
|
||||
ILogger<PostgresBackupService> logger,
|
||||
IConfigurationManager configurationManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_configurationManager = configurationManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a backup of the PostgreSQL database.
|
||||
/// </summary>
|
||||
/// <param name="backupDirectory">The directory to save the backup file.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The path to the created backup file.</returns>
|
||||
public async Task<string> CreateBackupAsync(string backupDirectory, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var dbConfig = _configurationManager.GetConfiguration<DatabaseConfigurationOptions>("database");
|
||||
var backupOptions = dbConfig.BackupOptions ?? new DatabaseBackupOptions();
|
||||
|
||||
if (dbConfig.CustomProviderOptions?.ConnectionString == null)
|
||||
{
|
||||
throw new InvalidOperationException("Database connection string is not configured");
|
||||
}
|
||||
|
||||
var connectionString = dbConfig.CustomProviderOptions.ConnectionString;
|
||||
var connectionParams = ParseConnectionString(connectionString);
|
||||
|
||||
var pgDumpPath = FindExecutable(backupOptions.PgDumpPath, "pg_dump");
|
||||
var timestamp = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss");
|
||||
var extension = GetFileExtension(backupOptions.BackupFormat);
|
||||
var backupFileName = $"jellyfin_postgres_backup_{timestamp}.{extension}";
|
||||
var fullBackupPath = Path.Combine(backupDirectory, backupFileName);
|
||||
|
||||
Directory.CreateDirectory(backupDirectory);
|
||||
|
||||
var arguments = BuildPgDumpArguments(connectionParams, backupOptions, fullBackupPath);
|
||||
|
||||
_logger.LogInformation("Starting PostgreSQL backup using external pg_dump tool to {BackupPath}", fullBackupPath);
|
||||
_logger.LogDebug("Executing external command: {PgDump} {Arguments}", pgDumpPath, string.Join(" ", arguments.Where(a => !a.Contains("PGPASSWORD", StringComparison.OrdinalIgnoreCase))));
|
||||
|
||||
var processStartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = pgDumpPath,
|
||||
Arguments = string.Join(" ", arguments),
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
// Set password via environment variable (more secure than command line)
|
||||
if (connectionParams.TryGetValue("Password", out var password))
|
||||
{
|
||||
processStartInfo.EnvironmentVariables["PGPASSWORD"] = password;
|
||||
}
|
||||
|
||||
using var process = new Process { StartInfo = processStartInfo };
|
||||
|
||||
var outputBuilder = new StringBuilder();
|
||||
var errorBuilder = new StringBuilder();
|
||||
|
||||
process.OutputDataReceived += (sender, e) =>
|
||||
{
|
||||
if (e.Data != null)
|
||||
{
|
||||
outputBuilder.AppendLine(e.Data);
|
||||
if (backupOptions.VerboseOutput)
|
||||
{
|
||||
_logger.LogDebug("pg_dump: {Output}", e.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
process.ErrorDataReceived += (sender, e) =>
|
||||
{
|
||||
if (e.Data != null)
|
||||
{
|
||||
errorBuilder.AppendLine(e.Data);
|
||||
if (backupOptions.VerboseOutput)
|
||||
{
|
||||
_logger.LogDebug("pg_dump stderr: {Error}", e.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
// Wait with timeout
|
||||
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(backupOptions.TimeoutSeconds));
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(linkedCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill();
|
||||
throw new TimeoutException($"Backup operation timed out after {backupOptions.TimeoutSeconds} seconds");
|
||||
}
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
var errorMessage = errorBuilder.ToString();
|
||||
_logger.LogError("pg_dump failed with exit code {ExitCode}: {Error}", process.ExitCode, errorMessage);
|
||||
|
||||
// Clean up failed backup file
|
||||
if (File.Exists(fullBackupPath))
|
||||
{
|
||||
File.Delete(fullBackupPath);
|
||||
}
|
||||
|
||||
throw new Exception($"Database backup failed: {errorMessage}");
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(fullBackupPath);
|
||||
_logger.LogInformation(
|
||||
"Successfully created PostgreSQL backup at {BackupPath} ({Size:N0} bytes)",
|
||||
fullBackupPath,
|
||||
fileInfo.Length);
|
||||
|
||||
return fullBackupPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores a PostgreSQL database from a backup file.
|
||||
/// </summary>
|
||||
/// <param name="backupFilePath">The path to the backup file.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A task representing the restore operation.</returns>
|
||||
public async Task RestoreBackupAsync(string backupFilePath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var dbConfig = _configurationManager.GetConfiguration<DatabaseConfigurationOptions>("database");
|
||||
var backupOptions = dbConfig.BackupOptions ?? new DatabaseBackupOptions();
|
||||
|
||||
if (!File.Exists(backupFilePath))
|
||||
{
|
||||
throw new FileNotFoundException($"Backup file not found: {backupFilePath}");
|
||||
}
|
||||
|
||||
if (dbConfig.CustomProviderOptions?.ConnectionString == null)
|
||||
{
|
||||
throw new InvalidOperationException("Database connection string is not configured");
|
||||
}
|
||||
|
||||
var connectionString = dbConfig.CustomProviderOptions.ConnectionString;
|
||||
var connectionParams = ParseConnectionString(connectionString);
|
||||
|
||||
var pgRestorePath = FindExecutable(backupOptions.PgRestorePath, "pg_restore");
|
||||
|
||||
var arguments = BuildPgRestoreArguments(connectionParams, backupOptions, backupFilePath);
|
||||
|
||||
_logger.LogInformation("Starting PostgreSQL restore from {BackupPath}", backupFilePath);
|
||||
|
||||
var processStartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = pgRestorePath,
|
||||
Arguments = string.Join(" ", arguments),
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
if (connectionParams.TryGetValue("Password", out var password))
|
||||
{
|
||||
processStartInfo.EnvironmentVariables["PGPASSWORD"] = password;
|
||||
}
|
||||
|
||||
using var process = new Process { StartInfo = processStartInfo };
|
||||
|
||||
var outputBuilder = new StringBuilder();
|
||||
var errorBuilder = new StringBuilder();
|
||||
|
||||
process.OutputDataReceived += (sender, e) =>
|
||||
{
|
||||
if (e.Data != null)
|
||||
{
|
||||
outputBuilder.AppendLine(e.Data);
|
||||
if (backupOptions.VerboseOutput)
|
||||
{
|
||||
_logger.LogDebug("pg_restore: {Output}", e.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
process.ErrorDataReceived += (sender, e) =>
|
||||
{
|
||||
if (e.Data != null)
|
||||
{
|
||||
errorBuilder.AppendLine(e.Data);
|
||||
if (backupOptions.VerboseOutput)
|
||||
{
|
||||
_logger.LogDebug("pg_restore stderr: {Error}", e.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(backupOptions.TimeoutSeconds));
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(linkedCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill();
|
||||
throw new TimeoutException($"Restore operation timed out after {backupOptions.TimeoutSeconds} seconds");
|
||||
}
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
var errorMessage = errorBuilder.ToString();
|
||||
_logger.LogError("pg_restore failed with exit code {ExitCode}: {Error}", process.ExitCode, errorMessage);
|
||||
throw new Exception($"Database restore failed: {errorMessage}");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Successfully restored PostgreSQL backup from {BackupPath}", backupFilePath);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> ParseConnectionString(string connectionString)
|
||||
{
|
||||
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var parts = connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var kvp = part.Split('=', 2);
|
||||
if (kvp.Length == 2)
|
||||
{
|
||||
result[kvp[0].Trim()] = kvp[1].Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<string> BuildPgDumpArguments(
|
||||
Dictionary<string, string> connectionParams,
|
||||
DatabaseBackupOptions options,
|
||||
string outputPath)
|
||||
{
|
||||
var args = new List<string>();
|
||||
|
||||
// Connection parameters
|
||||
if (connectionParams.TryGetValue("Host", out var host))
|
||||
{
|
||||
args.Add($"-h {host}");
|
||||
}
|
||||
|
||||
if (connectionParams.TryGetValue("Port", out var port))
|
||||
{
|
||||
args.Add($"-p {port}");
|
||||
}
|
||||
|
||||
if (connectionParams.TryGetValue("Username", out var username))
|
||||
{
|
||||
args.Add($"-U {username}");
|
||||
}
|
||||
|
||||
if (connectionParams.TryGetValue("Database", out var database))
|
||||
{
|
||||
args.Add($"-d {database}");
|
||||
}
|
||||
|
||||
// Format
|
||||
var formatFlag = options.BackupFormat.ToLowerInvariant() switch
|
||||
{
|
||||
"plain" => "p",
|
||||
"custom" => "c",
|
||||
"directory" => "d",
|
||||
"tar" => "t",
|
||||
_ => "c"
|
||||
};
|
||||
args.Add($"-F {formatFlag}");
|
||||
|
||||
// Compression level (for custom and directory formats)
|
||||
if (formatFlag is "c" or "d")
|
||||
{
|
||||
args.Add($"-Z {options.CompressionLevel}");
|
||||
}
|
||||
|
||||
// Include blobs
|
||||
if (options.IncludeBlobs)
|
||||
{
|
||||
args.Add("-b");
|
||||
}
|
||||
|
||||
// Verbose output
|
||||
if (options.VerboseOutput)
|
||||
{
|
||||
args.Add("-v");
|
||||
}
|
||||
|
||||
// Parallel jobs (directory format only)
|
||||
if (options.ParallelJobs.HasValue && formatFlag == "d")
|
||||
{
|
||||
args.Add($"-j {options.ParallelJobs.Value}");
|
||||
}
|
||||
|
||||
// Additional arguments
|
||||
if (!string.IsNullOrWhiteSpace(options.AdditionalArguments))
|
||||
{
|
||||
args.Add(options.AdditionalArguments);
|
||||
}
|
||||
|
||||
// Output file
|
||||
args.Add($"-f \"{outputPath}\"");
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private List<string> BuildPgRestoreArguments(
|
||||
Dictionary<string, string> connectionParams,
|
||||
DatabaseBackupOptions options,
|
||||
string backupPath)
|
||||
{
|
||||
var args = new List<string>();
|
||||
|
||||
// Connection parameters
|
||||
if (connectionParams.TryGetValue("Host", out var host))
|
||||
{
|
||||
args.Add($"-h {host}");
|
||||
}
|
||||
|
||||
if (connectionParams.TryGetValue("Port", out var port))
|
||||
{
|
||||
args.Add($"-p {port}");
|
||||
}
|
||||
|
||||
if (connectionParams.TryGetValue("Username", out var username))
|
||||
{
|
||||
args.Add($"-U {username}");
|
||||
}
|
||||
|
||||
if (connectionParams.TryGetValue("Database", out var database))
|
||||
{
|
||||
args.Add($"-d {database}");
|
||||
}
|
||||
|
||||
// Clean before restore
|
||||
args.Add("--clean");
|
||||
args.Add("--if-exists");
|
||||
|
||||
// Verbose output
|
||||
if (options.VerboseOutput)
|
||||
{
|
||||
args.Add("-v");
|
||||
}
|
||||
|
||||
// Parallel jobs
|
||||
if (options.ParallelJobs.HasValue)
|
||||
{
|
||||
args.Add($"-j {options.ParallelJobs.Value}");
|
||||
}
|
||||
|
||||
// Input file
|
||||
args.Add($"\"{backupPath}\"");
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private string FindExecutable(string? configuredPath, string defaultExecutableName)
|
||||
{
|
||||
// If configured path is provided, use it
|
||||
if (!string.IsNullOrWhiteSpace(configuredPath))
|
||||
{
|
||||
if (File.Exists(configuredPath))
|
||||
{
|
||||
return configuredPath;
|
||||
}
|
||||
|
||||
_logger.LogWarning("Configured path {Path} does not exist, searching in PATH", configuredPath);
|
||||
}
|
||||
|
||||
// Try to find in PATH
|
||||
var possiblePaths = new[]
|
||||
{
|
||||
defaultExecutableName, // In PATH
|
||||
Path.Combine("/usr/bin", defaultExecutableName),
|
||||
Path.Combine("/usr/local/bin", defaultExecutableName),
|
||||
Path.Combine(@"C:\Program Files\PostgreSQL\16\bin", $"{defaultExecutableName}.exe"),
|
||||
Path.Combine(@"C:\Program Files\PostgreSQL\15\bin", $"{defaultExecutableName}.exe"),
|
||||
Path.Combine(@"C:\Program Files\PostgreSQL\14\bin", $"{defaultExecutableName}.exe"),
|
||||
};
|
||||
|
||||
foreach (var path in possiblePaths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
_logger.LogDebug("Found {Executable} at {Path}", defaultExecutableName, path);
|
||||
return path;
|
||||
}
|
||||
|
||||
// Check if it's in PATH by trying to execute --version
|
||||
if (IsInPath(path))
|
||||
{
|
||||
_logger.LogDebug("Found {Executable} in PATH", defaultExecutableName);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
throw new FileNotFoundException($"{defaultExecutableName} executable not found. Please configure the path in database.xml or ensure it's in the system PATH.");
|
||||
}
|
||||
|
||||
private bool IsInPath(string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = "--version",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
process.Start();
|
||||
process.WaitForExit(2000);
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetFileExtension(string format)
|
||||
{
|
||||
return format.ToLowerInvariant() switch
|
||||
{
|
||||
"plain" => "sql",
|
||||
"custom" => "dump",
|
||||
"directory" => "dir",
|
||||
"tar" => "tar",
|
||||
_ => "dump"
|
||||
};
|
||||
}
|
||||
}
|
||||
+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