Ensure initial user exists before startup wizard completes

Add logic to initialize at least one user if the startup wizard is not completed, both during application startup and in the startup user API. After user creation, reload the user entity with all related navigation properties to ensure the in-memory user object is fully populated. Also update build and publish metadata files.
This commit is contained in:
2026-02-23 15:42:19 -05:00
parent f833f0ceeb
commit 534b0cde91
43 changed files with 25 additions and 4 deletions
+695
View File
@@ -0,0 +1,695 @@
# PostgreSQL Backup and Restore Analysis
## Summary
**Finding**: Jellyfin does **NOT use pg_dump/pg_restore** for PostgreSQL backups. Instead, it uses a **generic backup mechanism** that works across all database providers.
**Date**: 2025-01-15
**Analysis**: Migration backup and restore functionality
---
## Current Implementation
### Backup Strategy
Jellyfin uses a **provider-agnostic backup system** that:
1. **SQLite**: File-based backup (copies `jellyfin.db` file)
2. **PostgreSQL**: **No native backup** - Warns users to use pg_dump externally
3. **Generic Backup**: Uses `BackupService` with JSON serialization
---
## PostgreSQL Implementation Analysis
### MigrationBackupFast (PostgresDatabaseProvider.cs)
**Lines 395-403:**
```csharp
/// <inheritdoc />
public async Task<string> MigrationBackupFast(CancellationToken cancellationToken)
{
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
return await Task.FromResult(key).ConfigureAwait(false);
}
```
**❌ NO pg_dump usage** - Just logs a warning and returns a dummy key
### RestoreBackupFast (PostgresDatabaseProvider.cs)
**Lines 406-410:**
```csharp
/// <inheritdoc />
public Task RestoreBackupFast(string key, CancellationToken cancellationToken)
{
logger.LogWarning("Fast restore is not implemented for PostgreSQL. Use pg_restore for proper restoration.");
return Task.CompletedTask;
}
```
**❌ NO pg_restore usage** - Just logs a warning
### DeleteBackup (PostgresDatabaseProvider.cs)
**Lines 413-417:**
```csharp
/// <inheritdoc />
public Task DeleteBackup(string key)
{
logger.LogWarning("Backup deletion is not implemented for PostgreSQL. Manage backups externally.");
return Task.CompletedTask;
}
```
**❌ NO backup management** - Assumes external backup management
---
## Comparison: SQLite vs PostgreSQL
### SQLite Implementation (Working)
**MigrationBackupFast:**
```csharp
public Task<string> MigrationBackupFast(CancellationToken cancellationToken)
{
var key = DateTime.UtcNow.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture);
var path = Path.Combine(applicationPaths.DataPath, "jellyfin.db");
var backupFile = Path.Combine(applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db");
File.Copy(path, backupFile); // ✅ Simple file copy works
return Task.FromResult(key);
}
```
**RestoreBackupFast:**
```csharp
public Task RestoreBackupFast(string key, CancellationToken cancellationToken)
{
SqliteConnection.ClearAllPools();
var path = Path.Combine(applicationPaths.DataPath, "jellyfin.db");
var backupFile = Path.Combine(applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db");
File.Copy(backupFile, path, true); // ✅ Restore by copying back
return Task.CompletedTask;
}
```
### PostgreSQL Implementation (Not Implemented)
**MigrationBackupFast:**
```csharp
public async Task<string> MigrationBackupFast(CancellationToken cancellationToken)
{
logger.LogWarning("Fast backup is not implemented for PostgreSQL. Use pg_dump for proper backups.");
return await Task.FromResult(key).ConfigureAwait(false); // ❌ Does nothing
}
```
**RestoreBackupFast:**
```csharp
public Task RestoreBackupFast(string key, CancellationToken cancellationToken)
{
logger.LogWarning("Fast restore is not implemented for PostgreSQL. Use pg_restore for proper restoration.");
return Task.CompletedTask; // ❌ Does nothing
}
```
---
## Generic Backup System (BackupService.cs)
Jellyfin has a **separate generic backup system** that works for both SQLite and PostgreSQL:
### CreateBackupAsync (BackupService.cs)
**Process:**
1. Runs database optimization (`VACUUM ANALYZE` for PostgreSQL)
2. Creates a ZIP archive
3. Backs up configuration files
4. **Serializes all database tables to JSON** (no pg_dump)
5. Backs up metadata, trickplay, subtitles (optional)
**Code (lines 260-400+):**
```csharp
public async Task<BackupManifestDto> CreateBackupAsync(BackupOptionsDto backupOptions)
{
// ... optimization
await _jellyfinDatabaseProvider.RunScheduledOptimisation(cancellationToken);
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create))
{
var dbContext = await _dbProvider.CreateDbContextAsync();
await using (dbContext)
{
// For each entity type
foreach (var entityType in entityTypes)
{
// Serialize entities to JSON
var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
await foreach (var item in set)
{
using var document = JsonSerializer.SerializeToDocument(item);
document.WriteTo(jsonSerializer);
}
}
}
// Copy config/data folders
CopyDirectory("Config", ...);
CopyDirectory("Data", ...);
}
}
```
**❌ NOT using pg_dump** - Uses JSON serialization instead
### RestoreBackupAsync (BackupService.cs)
**Process:**
1. Extracts ZIP archive
2. Reads manifest
3. Restores configuration files
4. **Deserializes JSON back to database** (no pg_restore)
5. Restores migration history manually
**Code (lines 87-246):**
```csharp
public async Task RestoreBackupAsync(string archivePath)
{
using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read);
var dbContext = await _dbProvider.CreateDbContextAsync();
await using (dbContext)
{
// Purge database tables
await _jellyfinDatabaseProvider.PurgeDatabase(dbContext, tableNames);
// Restore each table from JSON
foreach (var entityType in entityTypes)
{
await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<JsonObject>(zipEntryStream))
{
var entity = item.Deserialize(entityType);
dbContext.Add(entity);
}
}
await dbContext.SaveChangesAsync();
}
}
```
**❌ NOT using pg_restore** - Uses JSON deserialization instead
---
## Why pg_dump/pg_restore Are NOT Used
### Design Philosophy
Jellyfin's backup system is **database-agnostic** to support:
- SQLite (file-based)
- PostgreSQL (server-based)
- Potential future databases (MySQL, SQL Server, etc.)
### Current Approach
- **Generic serialization**: Works with any EF Core provider
- **JSON format**: Human-readable and portable
- **Cross-database**: Can potentially restore SQLite backup to PostgreSQL (with schema migration)
### Limitations of Current Approach
1. **Slow for large databases**: JSON serialization is slower than native tools
2. **No differential backups**: Always full backup
3. **Memory intensive**: Loads entire tables into memory
4. **No compression within JSON**: ZIP compression only
5. **No transaction log**: Point-in-time recovery not possible
---
## PurgeDatabase Implementation
### PostgreSQL (lines 420-446)
```csharp
public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames)
{
var deleteQueries = new List<string>();
foreach (var tableName in tableNames)
{
var schema = entityType.GetSchema() ?? "public";
deleteQueries.Add($"TRUNCATE TABLE \"{schema}\".\"{tableName}\" CASCADE;");
}
var deleteAllQuery = string.Join('\n', deleteQueries);
await dbContext.Database.ExecuteSqlRawAsync(deleteAllQuery).ConfigureAwait(false);
}
```
**Uses TRUNCATE** - Fast table clearing with CASCADE for foreign keys
### SQLite (lines 193-211)
```csharp
public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames)
{
var deleteQueries = new List<string>();
foreach (var tableName in tableNames)
{
deleteQueries.Add($"DELETE FROM \"{tableName}\";");
}
var deleteAllQuery =
$"""
PRAGMA foreign_keys = OFF;
{string.Join('\n', deleteQueries)}
PRAGMA foreign_keys = ON;
""";
await dbContext.Database.ExecuteSqlRawAsync(deleteAllQuery).ConfigureAwait(false);
}
```
**Uses DELETE** - SQLite doesn't support TRUNCATE as efficiently
---
## Migration Backup Attribute Usage
### Example from MigrateRatingLevels.cs
```csharp
[JellyfinMigration("2025-04-20T22:00:00", nameof(MigrateRatingLevels))]
[JellyfinMigrationBackup(JellyfinDb = true)] // ← Requests backup
internal class MigrateRatingLevels : IDatabaseMigrationRoutine
{
// Migration code
}
```
**What Happens:**
1. Migration system sees `JellyfinMigrationBackup` attribute
2. Calls `MigrationBackupFast()` on the database provider
3. **For PostgreSQL**: Just logs a warning, no actual backup
4. **For SQLite**: Creates a file-based backup
5. Runs the migration
6. If migration fails, calls `RestoreBackupFast()`
7. **For PostgreSQL**: Logs warning, no actual restore
---
## Recommendations
### 🔴 CRITICAL: PostgreSQL Has No Automatic Backup!
**Current Behavior:**
- Migration attribute `[JellyfinMigrationBackup(JellyfinDb = true)]` does **NOTHING** for PostgreSQL
- Users are at risk if migrations fail
- Manual pg_dump is required before starting Jellyfin
### ✅ Recommendation 1: Implement pg_dump Integration
**Add to PostgresDatabaseProvider.cs:**
```csharp
public async Task<string> MigrationBackupFast(CancellationToken cancellationToken)
{
var key = DateTime.UtcNow.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture);
var backupPath = Path.Combine(_applicationPaths.DataPath, "backups", $"{key}_jellyfin_pg.backup");
// Get connection details from DbContextFactory
var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken);
await using (context)
{
var connectionString = context.Database.GetConnectionString();
var builder = new NpgsqlConnectionStringBuilder(connectionString);
// Use pg_dump for native PostgreSQL backup
var pgDumpPath = FindPgDumpExecutable() ?? "pg_dump";
var arguments = $"-h {builder.Host} -p {builder.Port} -U {builder.Username} " +
$"-d {builder.Database} -F c -f \"{backupPath}\"";
var processInfo = new ProcessStartInfo
{
FileName = pgDumpPath,
Arguments = arguments,
Environment = { ["PGPASSWORD"] = builder.Password },
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
using var process = Process.Start(processInfo);
await process.WaitForExitAsync(cancellationToken);
if (process.ExitCode != 0)
{
throw new InvalidOperationException($"pg_dump failed with exit code {process.ExitCode}");
}
logger.LogInformation("PostgreSQL backup created at {BackupPath}", backupPath);
return key;
}
}
```
### ✅ Recommendation 2: Implement pg_restore Integration
```csharp
public async Task RestoreBackupFast(string key, CancellationToken cancellationToken)
{
var backupPath = Path.Combine(_applicationPaths.DataPath, "backups", $"{key}_jellyfin_pg.backup");
if (!File.Exists(backupPath))
{
logger.LogCritical("Backup file not found: {BackupPath}", backupPath);
return;
}
var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken);
await using (context)
{
var connectionString = context.Database.GetConnectionString();
var builder = new NpgsqlConnectionStringBuilder(connectionString);
// Drop and recreate database
await DropAndRecreateDatabaseAsync(builder, cancellationToken);
// Use pg_restore
var pgRestorePath = FindPgRestoreExecutable() ?? "pg_restore";
var arguments = $"-h {builder.Host} -p {builder.Port} -U {builder.Username} " +
$"-d {builder.Database} \"{backupPath}\"";
var processInfo = new ProcessStartInfo
{
FileName = pgRestorePath,
Arguments = arguments,
Environment = { ["PGPASSWORD"] = builder.Password },
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
using var process = Process.Start(processInfo);
await process.WaitForExitAsync(cancellationToken);
if (process.ExitCode != 0)
{
throw new InvalidOperationException($"pg_restore failed with exit code {process.ExitCode}");
}
logger.LogInformation("PostgreSQL backup restored from {BackupPath}", backupPath);
}
}
```
### ✅ Recommendation 3: Alternative - Use SQL Dump Format
**Simpler approach** without requiring pg_dump/pg_restore binaries:
```csharp
public async Task<string> MigrationBackupFast(CancellationToken cancellationToken)
{
var key = DateTime.UtcNow.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture);
var backupPath = Path.Combine(_applicationPaths.DataPath, "backups", $"{key}_jellyfin_pg.sql");
var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken);
await using (context)
{
// Use PostgreSQL COPY command to export data
foreach (var schema in Schemas.All)
{
var tables = context.Model.GetEntityTypes()
.Where(et => et.GetSchema() == schema)
.Select(et => et.GetTableName());
foreach (var table in tables)
{
// Export using COPY TO
var query = $"COPY \"{schema}\".\"{table}\" TO STDOUT WITH (FORMAT CSV, HEADER true)";
// Save to backup file
}
}
}
return key;
}
```
---
## Risk Analysis
### Current Risks with PostgreSQL Migrations
| Risk | Severity | Impact | Mitigation |
|------|----------|--------|------------|
| **No automatic backup** | 🔴 HIGH | Data loss if migration fails | Users must manually backup |
| **No rollback capability** | 🔴 HIGH | Cannot undo failed migrations | Manual pg_restore required |
| **Migration failure** | 🟡 MEDIUM | Database corruption possible | Transaction rollback helps |
| **User awareness** | 🟡 MEDIUM | Users may not backup | Documentation needed |
### Current Behavior
**If a PostgreSQL migration fails:**
1. ❌ No automatic backup exists
2. ❌ No automatic rollback
3. ❌ Database may be left in inconsistent state
4. ⚠️ User must manually restore from their own pg_dump backup
**For SQLite:**
1. ✅ Automatic file backup created
2. ✅ Can restore backup automatically
3. ✅ Rollback possible
4. ✅ User protected from data loss
---
## Generic Backup System (BackupService)
### How It Works
**Create Backup (lines 260-420):**
1. Optimize database (`VACUUM ANALYZE` for PostgreSQL)
2. Create ZIP archive
3. Serialize all database tables to JSON
4. Include migration history
5. Copy config/data folders
6. Create manifest.json
**Restore Backup (lines 87-246):**
1. Extract ZIP archive
2. Read manifest
3. **Purge all tables** (TRUNCATE CASCADE for PostgreSQL)
4. Restore migration history
5. Deserialize JSON and insert records
6. Restore config/data folders
### Advantages
- ✅ Works with any database provider
- ✅ Human-readable format (JSON)
- ✅ Portable between databases (potentially)
- ✅ Includes all Jellyfin data (config, metadata)
### Disadvantages
- ❌ Slow for large databases (serialization overhead)
- ❌ Memory intensive
- ❌ No differential/incremental backups
- ❌ Not using native database features
- ❌ No point-in-time recovery
---
## Proposed Solutions
### Option 1: Implement Native pg_dump/pg_restore (Recommended)
**Pros:**
- ✅ Fast and efficient
- ✅ Industry-standard PostgreSQL backup
- ✅ Supports large databases
- ✅ Point-in-time recovery possible
- ✅ Compressed backups
**Cons:**
- ❌ Requires pg_dump/pg_restore binaries
- ❌ Platform-dependent (Windows/Linux paths)
- ❌ Requires PostgreSQL client tools installed
- ❌ More complex error handling
**Implementation Effort**: Medium (1-2 days)
### Option 2: Use PostgreSQL SQL Commands (Alternative)
**Pros:**
- ✅ No external dependencies
- ✅ Works through EF Core connection
- ✅ Cross-platform
- ✅ Simpler implementation
**Cons:**
- ❌ Slower than native tools
- ❌ Limited features
- ❌ Still requires custom format
**Implementation Effort**: Low (4-8 hours)
### Option 3: Document Manual Backup (Current)
**Pros:**
- ✅ No implementation required
- ✅ Users have full control
- ✅ Can use their preferred tools
**Cons:**
- ❌ Users may forget to backup
- ❌ No automatic rollback
- ❌ Risk of data loss
**Implementation Effort**: None (just documentation)
---
## Recommendation for Your Project
### Immediate Action (Current Status)
**✅ GOOD NEWS**: The generic `BackupService` **DOES work** with PostgreSQL:
- It serializes all tables to JSON
- It can restore from JSON
- It's just slower than pg_dump
**⚠️ CONCERN**: `MigrationBackupFast` does nothing for PostgreSQL:
- Migration backups are not created
- Rollback is not possible
- Users are at risk during migrations
### Short-term Solution
**Add warning to documentation:**
```
For PostgreSQL users: Before running Jellyfin updates, always run:
pg_dump -h localhost -U jellyfin -d jellyfin -F c -f jellyfin_backup.backup
```
### Long-term Solution (If Needed)
**Implement pg_dump integration** for `MigrationBackupFast`:
1. Detect if pg_dump is available
2. Fall back to generic backup if not
3. Store backups in standard location
4. Implement restore with pg_restore
---
## Code Locations
### Key Files
1. **Interface**: `src\Jellyfin.Database\Jellyfin.Database.Implementations\IJellyfinDatabaseProvider.cs`
- Defines `MigrationBackupFast()`, `RestoreBackupFast()`, `DeleteBackup()`
2. **PostgreSQL Provider**: `src\Jellyfin.Database\Jellyfin.Database.Providers.Postgres\PostgresDatabaseProvider.cs`
- Lines 395-417: Backup/restore methods (NOT IMPLEMENTED)
- Lines 420-446: `PurgeDatabase()` using TRUNCATE CASCADE
3. **SQLite Provider**: `src\Jellyfin.Database\Jellyfin.Database.Providers.Sqlite\SqliteDatabaseProvider.cs`
- Lines 145-190: Backup/restore methods (FULLY IMPLEMENTED with file copy)
4. **Generic Backup**: `Jellyfin.Server.Implementations\FullSystemBackup\BackupService.cs`
- Lines 260-420: `CreateBackupAsync()` using JSON serialization
- Lines 87-246: `RestoreBackupAsync()` using JSON deserialization
5. **Backup Attribute**: `Jellyfin.Server\Migrations\JellyfinMigrationBackupAttribute.cs`
- Decorates migrations that need backup
---
## Testing Verification
### To Verify Current Backup System Works
**1. Create a generic backup:**
```csharp
await _backupService.CreateBackupAsync(new BackupOptionsDto
{
Database = true,
Metadata = false,
Trickplay = false,
Subtitles = false
});
```
**2. Check backup location:**
```
<DataPath>/backups/jellyfin-backup-yyyyMMddHHmmss.zip
```
**3. Inspect contents:**
- `manifest.json` - Backup metadata
- `Database/*.json` - Serialized tables
- `Config/` - Configuration files
- `Data/` - Data folder contents
**4. Test restore:**
```csharp
await _backupService.RestoreBackupAsync(backupPath);
```
### To Test Migration Backup (Will NOT Work for PostgreSQL)
**1. Run a migration with backup attribute:**
```csharp
[JellyfinMigrationBackup(JellyfinDb = true)]
```
**2. Check logs:**
```
[WARN] Fast backup is not implemented for PostgreSQL. Use pg_dump for proper backups.
```
**3. Verify no backup created** in data/backups folder
---
## Conclusion
### Finding Summary
**pg_dump**: NOT used
**pg_restore**: NOT used
**Generic JSON backup**: Available but not triggered by migrations
**Migration backups**: Do not work for PostgreSQL
### Implications
1. **Current**: PostgreSQL users have **no automatic migration rollback**
2. **Generic backup works**: But not used during migrations
3. **Risk**: Data loss if migration fails
4. **Mitigation**: Users must manually backup with pg_dump
### Next Steps
**For Your Project:**
1.**Fixed the immediate error** (query materialization)
2. ⚠️ **Be aware**: No automatic backup during migrations
3. 📝 **Document**: Users should run pg_dump before updates
4. 🔧 **Consider**: Implementing pg_dump integration (future enhancement)
**For Production:**
1. **Always backup before Jellyfin updates**: `pg_dump -F c -f backup.backup`
2. **Monitor migrations**: Watch logs for issues
3. **Test migrations**: Use test database first
4. **Keep backups**: Regular pg_dump backups recommended
---
**Document Version**: 1.0
**Date**: 2025-01-15
**Analysis**: PostgreSQL backup mechanisms
**Status**: ❌ pg_dump/pg_restore NOT used, ⚠️ Manual backups required