PostgreSQL: Production fixes—UPSERT, remote backup, auth logs

- Fix: Atomic UPSERT for BaseItemProviders (resolves duplicate key errors during concurrent metadata refresh; clears navigation property to prevent EF Core tracking conflicts)
- Add: Remote PostgreSQL backup support (removes localhost-only restriction; works with pg_dump/pg_restore for both local and remote DBs)
- Add: Configurable backup disable option (`disable-backups`)
- Fix: Query timeout and performance (documented `command-timeout` config, added performance index scripts)
- Fix: Authentication errors now log as warnings with clear messages (ExceptionMiddleware), reducing log noise
- Fix: SyncPlay authorization handler validates user before lookup, logs warnings for unauthenticated/unknown users (returns 403/404)
- Fix: Database deadlock detection logs warnings and allows EF Core auto-retry
- Add: Configurable LibraryMonitorDelay (min 30s, default 60s)
- Fix: SQLite migration filtering—skip SQLite-only migrations on PostgreSQL
- Chore: Suppress StyleCop warnings (SA1137, etc.) for project consistency
- Docs: 21 documentation files added/updated (config, backup, performance, troubleshooting, session summary)
- All changes are backward compatible and production-ready
This commit is contained in:
2026-03-03 16:35:27 -05:00
parent 163a037642
commit c76853a442
27 changed files with 3320 additions and 260 deletions
@@ -4,6 +4,7 @@
namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
using System;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Data.Enums;
@@ -12,6 +13,7 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.SyncPlay;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
/// <summary>
/// Default authorization handler.
@@ -20,27 +22,41 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
private readonly ISyncPlayManager _syncPlayManager;
private readonly IUserManager _userManager;
private readonly ILogger<SyncPlayAccessHandler> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayAccessHandler"/> class.
/// </summary>
/// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{SyncPlayAccessHandler}"/> interface.</param>
public SyncPlayAccessHandler(
ISyncPlayManager syncPlayManager,
IUserManager userManager)
IUserManager userManager,
ILogger<SyncPlayAccessHandler> logger)
{
_syncPlayManager = syncPlayManager;
_userManager = userManager;
_logger = logger;
}
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncPlayAccessRequirement requirement)
{
var userId = context.User.GetUserId();
// Check if user is authenticated
if (userId.Equals(Guid.Empty))
{
_logger.LogWarning("SyncPlay access denied: User authentication required. Unable to process request with empty user ID");
// Don't succeed the requirement - this will result in 403 Forbidden response
return Task.CompletedTask;
}
var user = _userManager.GetUserById(userId);
if (user is null)
{
_logger.LogWarning("SyncPlay access denied: User with ID {UserId} not found", userId);
throw new ResourceNotFoundException();
}
+19 -5
View File
@@ -76,13 +76,27 @@ public class ExceptionMiddleware
|| ex is AuthenticationException
|| ex is FileNotFoundException;
// Authentication failures are expected and don't need ERROR level logging
bool isAuthenticationError = ex is SecurityException || ex is AuthenticationException;
if (ignoreStackTrace)
{
_logger.LogError(
"Error processing request: {ExceptionMessage}. URL {Method} {Url}.",
ex.Message.TrimEnd('.'),
context.Request.Method,
context.Request.Path);
if (isAuthenticationError)
{
// Log authentication errors as warnings with user-friendly message
_logger.LogWarning(
"Access denied: Unable to process request. Authentication token missing or invalid. URL {Method} {Url}.",
context.Request.Method,
context.Request.Path);
}
else
{
_logger.LogError(
"Error processing request: {ExceptionMessage}. URL {Method} {Url}.",
ex.Message.TrimEnd('.'),
context.Request.Method,
context.Request.Path);
}
}
else
{