aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authororut34iop <orut34iop@users.noreply.github.com>2026-09-15 11:16:58 -0400
committerCody Robibero <cody@robibe.ro>2026-09-15 11:16:58 -0400
commit42245b9b25ef278abc1127c543357aeecf8de6d0 (patch)
tree19919deadf4d35d1c5767cbeca2720b22472bf69
parent28be079779d4508b5ab2a2fd4b482f3fd929ced5 (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.cs8
-rw-r--r--tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs58
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");
+ }
+ }
}