diff options
Diffstat (limited to 'Jellyfin.Server.Implementations/Users')
3 files changed, 142 insertions, 36 deletions
diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs index 92e2bb4fa7..7c46ef7721 100644 --- a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs +++ b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs @@ -1,3 +1,4 @@ +using System; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data; @@ -9,6 +10,7 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Implementations.Users; @@ -20,6 +22,7 @@ public sealed class DeviceAccessHost : IHostedService private readonly IUserManager _userManager; private readonly IDeviceManager _deviceManager; private readonly ISessionManager _sessionManager; + private readonly ILogger<DeviceAccessHost> _logger; /// <summary> /// Initializes a new instance of the <see cref="DeviceAccessHost"/> class. @@ -27,11 +30,17 @@ public sealed class DeviceAccessHost : IHostedService /// <param name="userManager">The <see cref="IUserManager"/>.</param> /// <param name="deviceManager">The <see cref="IDeviceManager"/>.</param> /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param> - public DeviceAccessHost(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager) + /// <param name="logger">The <see cref="ILogger{TCategoryName}"/>.</param> + public DeviceAccessHost( + IUserManager userManager, + IDeviceManager deviceManager, + ISessionManager sessionManager, + ILogger<DeviceAccessHost> logger) { _userManager = userManager; _deviceManager = deviceManager; _sessionManager = sessionManager; + _logger = logger; } /// <inheritdoc /> @@ -53,9 +62,18 @@ public sealed class DeviceAccessHost : IHostedService private async void OnUserUpdated(object? sender, GenericEventArgs<User> e) { var user = e.Argument; - if (!user.HasPermission(PermissionKind.EnableAllDevices)) + + // This handler is async void, so an escaping exception would terminate the process. + try + { + if (!user.HasPermission(PermissionKind.EnableAllDevices)) + { + await UpdateDeviceAccess(user).ConfigureAwait(false); + } + } + catch (Exception ex) { - await UpdateDeviceAccess(user).ConfigureAwait(false); + _logger.LogError(ex, "Error updating device access for user {UserId}", user.Id); } } diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs index 0e126fe9a0..dd3c63ae32 100644 --- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs +++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs @@ -13,6 +13,8 @@ namespace Jellyfin.Server.Implementations.Users; /// </summary> public sealed class DisplayPreferencesManager : IDisplayPreferencesManager { + private const int MaxSaveAttempts = 3; + private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory; /// <summary> @@ -28,17 +30,31 @@ public sealed class DisplayPreferencesManager : IDisplayPreferencesManager public DisplayPreferences GetDisplayPreferences(Guid userId, Guid itemId, string client) { using var dbContext = _dbContextFactory.CreateDbContext(); - var prefs = dbContext.DisplayPreferences - .Include(pref => pref.HomeSections) - .FirstOrDefault(pref => - pref.UserId.Equals(userId) && pref.Client == client && pref.ItemId.Equals(itemId)); + var prefs = FindDisplayPreferences(dbContext, userId, itemId, client); + if (prefs is not null) + { + return prefs; + } - if (prefs is null) + prefs = new DisplayPreferences(userId, itemId, client); + dbContext.DisplayPreferences.Add(prefs); + try { - prefs = new DisplayPreferences(userId, itemId, client); - dbContext.DisplayPreferences.Add(prefs); dbContext.SaveChanges(); } + catch (DbUpdateException) + { + // Another request may have stored the preferences between the lookup and the insert, and the unique index + // rejected this one. Return the stored preferences; if there are none, the insert failed for another reason. + using var retryContext = _dbContextFactory.CreateDbContext(); + var stored = FindDisplayPreferences(retryContext, userId, itemId, client); + if (stored is null) + { + throw; + } + + return stored; + } return prefs; } @@ -52,7 +68,7 @@ public sealed class DisplayPreferencesManager : IDisplayPreferencesManager if (prefs is null) { - prefs = new ItemDisplayPreferences(userId, Guid.Empty, client); + prefs = new ItemDisplayPreferences(userId, itemId, client); dbContext.ItemDisplayPreferences.Add(prefs); dbContext.SaveChanges(); } @@ -83,19 +99,35 @@ public sealed class DisplayPreferencesManager : IDisplayPreferencesManager /// <inheritdoc /> public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string?> customPreferences) { - using var dbContext = _dbContextFactory.CreateDbContext(); - dbContext.CustomItemDisplayPreferences.Where(prefs => prefs.UserId.Equals(userId) - && prefs.ItemId.Equals(itemId) - && prefs.Client == client) - .ExecuteDelete(); - - foreach (var (key, value) in customPreferences) + // Another request can store one of these keys after this one's delete, and the unique index then rejects the + // insert. Replacing the set again gives the same result, so the replace is repeated. + for (var attempt = 1; ; attempt++) { - dbContext.CustomItemDisplayPreferences - .Add(new CustomItemDisplayPreferences(userId, itemId, client, key, value)); + using var dbContext = _dbContextFactory.CreateDbContext(); + using var transaction = dbContext.Database.BeginTransaction(); + dbContext.CustomItemDisplayPreferences.Where(prefs => prefs.UserId.Equals(userId) + && prefs.ItemId.Equals(itemId) + && prefs.Client == client) + .ExecuteDelete(); + + foreach (var (key, value) in customPreferences) + { + dbContext.CustomItemDisplayPreferences + .Add(new CustomItemDisplayPreferences(userId, itemId, client, key, value)); + } + + try + { + dbContext.SaveChanges(); + } + catch (DbUpdateException) when (attempt < MaxSaveAttempts) + { + continue; + } + + transaction.Commit(); + return; } - - dbContext.SaveChanges(); } /// <inheritdoc/> @@ -113,4 +145,9 @@ public sealed class DisplayPreferencesManager : IDisplayPreferencesManager dbContext.ItemDisplayPreferences.Attach(itemDisplayPreferences).State = EntityState.Modified; dbContext.SaveChanges(); } + + private static DisplayPreferences? FindDisplayPreferences(JellyfinDbContext dbContext, Guid userId, Guid itemId, string client) + => dbContext.DisplayPreferences + .Include(pref => pref.HomeSections) + .FirstOrDefault(pref => pref.UserId.Equals(userId) && pref.Client == client && pref.ItemId.Equals(itemId)); } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 41648268a9..b15f6b98b2 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -225,17 +225,8 @@ namespace Jellyfin.Server.Implementations.Users ?? throw new ResourceNotFoundException(nameof(user.Id)); dbContext.Entry(dbUser).CurrentValues.SetValues(user); - dbUser.Permissions.Clear(); - foreach (var permission in user.Permissions) - { - dbUser.Permissions.Add(new Permission(permission.Kind, permission.Value)); - } - - dbUser.Preferences.Clear(); - foreach (var preference in user.Preferences) - { - dbUser.Preferences.Add(new Preference(preference.Kind, preference.Value)); - } + SyncPermissions(dbUser, user.Permissions); + SyncPreferences(dbUser, user.Preferences); dbUser.AccessSchedules.Clear(); foreach (var accessSchedule in user.AccessSchedules) @@ -269,6 +260,60 @@ namespace Jellyfin.Server.Implementations.Users } } + private static void SyncPermissions(User dbUser, ICollection<Permission> source) + { + var incoming = new Dictionary<PermissionKind, bool>(); + foreach (var permission in source) + { + incoming[permission.Kind] = permission.Value; + } + + foreach (var existing in dbUser.Permissions) + { + if (incoming.Remove(existing.Kind, out var value)) + { + // EF only marks the row modified if the value actually differs, so an update that + // touches nothing but the user row - a session activity stamp - writes no children. + existing.Value = value; + } + else + { + dbUser.Permissions.Remove(existing); + } + } + + foreach (var (kind, value) in incoming) + { + dbUser.Permissions.Add(new Permission(kind, value)); + } + } + + private static void SyncPreferences(User dbUser, ICollection<Preference> source) + { + var incoming = new Dictionary<PreferenceKind, string>(); + foreach (var preference in source) + { + incoming[preference.Kind] = preference.Value; + } + + foreach (var existing in dbUser.Preferences) + { + if (incoming.Remove(existing.Kind, out var value)) + { + existing.Value = value; + } + else + { + dbUser.Preferences.Remove(existing); + } + } + + foreach (var (kind, value) in incoming) + { + dbUser.Preferences.Add(new Preference(kind, value)); + } + } + internal async Task<User> CreateUserInternalAsync(string name, JellyfinDbContext dbContext) { // TODO: Remove after user item data is migrated. @@ -802,14 +847,16 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy) { + User user; using (await _userLock.LockAsync(userId).ConfigureAwait(false)) { var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { - var user = UserQuery(dbContext) + user = await UserQuery(dbContext) .AsTracking() - .FirstOrDefault(u => u.Id.Equals(userId)) + .FirstOrDefaultAsync(u => u.Id.Equals(userId)) + .ConfigureAwait(false) ?? throw new ArgumentException("No user exists with given Id!"); // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0" @@ -874,6 +921,10 @@ namespace Jellyfin.Server.Implementations.Users await dbContext.SaveChangesAsync().ConfigureAwait(false); } } + + var eventArgs = new UserUpdatedEventArgs(user); + await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false); + OnUserUpdated?.Invoke(this, eventArgs); } /// <inheritdoc/> @@ -911,7 +962,7 @@ namespace Jellyfin.Server.Implementations.Users internal static void ThrowIfInvalidUsername(string name) { - if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name)) + if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name) && !string.Equals(name, ".", StringComparison.Ordinal) && !string.Equals(name, "..", StringComparison.Ordinal)) { return; } |
