diff options
| author | orut34iop <orut34iop@users.noreply.github.com> | 2026-09-15 11:16:58 -0400 |
|---|---|---|
| committer | Cody Robibero <cody@robibe.ro> | 2026-09-15 11:16:58 -0400 |
| commit | 42245b9b25ef278abc1127c543357aeecf8de6d0 (patch) | |
| tree | 19919deadf4d35d1c5767cbeca2720b22472bf69 | |
| parent | 28be079779d4508b5ab2a2fd4b482f3fd929ced5 (diff) | |
Backport pull request #18007 from jellyfin/release-12.z
Preserve library items when directory enumeration fails
Original-merge: 63def185b199c5afded850b8eb5667276bc864f9
Merged-by: crobibero <cody@robibe.ro>
Backported-by: Cody Robibero <cody@robibe.ro>
| -rw-r--r-- | MediaBrowser.Controller/Entities/Folder.cs | 8 | ||||
| -rw-r--r-- | tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs | 58 |
2 files changed, 64 insertions, 2 deletions
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 25e19c8351..dae4612aa1 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -429,19 +429,23 @@ namespace MediaBrowser.Controller.Entities if (IsFileProtocol) { - IEnumerable<BaseItem> nonCachedChildren = []; + IEnumerable<BaseItem> nonCachedChildren; try { - nonCachedChildren = GetNonCachedChildren(directoryService); + // Finish enumeration before mutating the library. An I/O failure, including + // one partway through a lazy enumeration, must not look like removed files. + nonCachedChildren = GetNonCachedChildren(directoryService).ToArray(); } catch (IOException ex) { Logger.LogError(ex, "Error retrieving children from file system"); + return; } catch (SecurityException ex) { Logger.LogError(ex, "Error retrieving children from file system"); + return; } catch (Exception ex) { diff --git a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs index 588f9d5530..e072bccb82 100644 --- a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs +++ b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs @@ -30,6 +30,43 @@ namespace Jellyfin.Controller.Tests.Entities; [Collection("LibraryManagerTests")] public class BaseItemTests { + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public async Task ValidateChildren_FailedEnumeration_DoesNotReconcileOrDeleteChildren(bool failAfterFirstChild, bool accessDenied) + { + var previousLibrary = BaseItem.LibraryManager; + var previousRepository = BaseItem.ItemRepository; + var previousLogger = BaseItem.Logger; + var library = new Mock<ILibraryManager>(MockBehavior.Strict); + var repository = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>(MockBehavior.Strict); + var directory = new Mock<IDirectoryService>(); + directory.Setup(d => d.IsAccessible(It.IsAny<string>())).Returns(true); + try + { + BaseItem.LibraryManager = library.Object; + BaseItem.ItemRepository = repository.Object; + BaseItem.Logger = Microsoft.Extensions.Logging.Abstractions.NullLogger<BaseItem>.Instance; + var folder = new FailingEnumerationFolder(failAfterFirstChild, accessDenied) + { + Id = Guid.NewGuid(), + Path = "/media/review-folder" + }; + await folder.ValidateChildren(new Progress<double>(), new MetadataRefreshOptions(directory.Object), recursive: false, cancellationToken: TestContext.Current.CancellationToken).ConfigureAwait(true); + Assert.True(folder.EnumerationAttempted); + repository.VerifyNoOtherCalls(); + library.VerifyNoOtherCalls(); + } + finally + { + BaseItem.LibraryManager = previousLibrary; + BaseItem.ItemRepository = previousRepository; + BaseItem.Logger = previousLogger; + } + } + [Fact] public void SetPrimaryVersionId_Null_RestoresTheItemsOwnPresentationKey() { @@ -692,4 +729,25 @@ public class BaseItemTests Assert.Equal([primary.Id, alt1.Id, alt2.Id], ids); } + + private sealed class FailingEnumerationFolder(bool failAfterFirstChild, bool accessDenied) : Folder + { + public bool EnumerationAttempted { get; private set; } + + protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) + { + EnumerationAttempted = true; + if (failAfterFirstChild) + { + yield return new Movie { Id = Guid.NewGuid(), Path = "/media/review-folder/movie.mkv" }; + } + + if (accessDenied) + { + throw new System.Security.SecurityException("Simulated access failure"); + } + + throw new IOException("Simulated directory read failure"); + } + } } |
