From 5ea3910af96ead74d9267ec239e47a798a33e78f Mon Sep 17 00:00:00 2001 From: revam Date: Mon, 17 Nov 2025 14:08:47 -0500 Subject: Backport pull request #15263 from jellyfin/release-10.11.z Resolve symlinks for static media source infos Original-merge: 3b2d64995aab63ebaa6832c059a3cc0bdebe90dc Merged-by: crobibero Backported-by: Bond_009 --- MediaBrowser.Controller/IO/FileSystemHelper.cs | 74 ++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) (limited to 'MediaBrowser.Controller/IO/FileSystemHelper.cs') diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs index 1a33c3aa8..324aea7e3 100644 --- a/MediaBrowser.Controller/IO/FileSystemHelper.cs +++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using MediaBrowser.Model.IO; @@ -61,4 +62,77 @@ public static class FileSystemHelper } } } + + /// + /// Gets the target of the specified file link. + /// + /// + /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128. + /// + /// The path of the file link. + /// true to follow links to the final target; false to return the immediate next link. + /// + /// A if the is a link, regardless of if the target exists; otherwise, null. + /// + public static FileInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) + { + // Check if the file exists so the native resolve handler won't throw at us. + if (!File.Exists(linkPath)) + { + return null; + } + + if (!returnFinalTarget) + { + return File.ResolveLinkTarget(linkPath, returnFinalTarget: false) as FileInfo; + } + + if (File.ResolveLinkTarget(linkPath, returnFinalTarget: false) is not FileInfo targetInfo) + { + return null; + } + + var currentPath = targetInfo.FullName; + var visited = new HashSet(StringComparer.Ordinal) { linkPath, currentPath }; + while (File.ResolveLinkTarget(currentPath, returnFinalTarget: false) is FileInfo linkInfo) + { + var targetPath = linkInfo.FullName; + + // If an infinite loop is detected, return the file info for the + // first link in the loop we encountered. + if (!visited.Add(targetPath)) + { + return new FileInfo(targetPath); + } + + targetInfo = linkInfo; + currentPath = targetPath; + + // Exit if the target doesn't exist, so the native resolve handler won't throw at us. + if (!targetInfo.Exists) + { + break; + } + } + + return targetInfo; + } + + /// + /// Gets the target of the specified file link. + /// + /// + /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128. + /// + /// The file info of the file link. + /// true to follow links to the final target; false to return the immediate next link. + /// + /// A if the is a link, regardless of if the target exists; otherwise, null. + /// + public static FileInfo? ResolveLinkTarget(FileInfo fileInfo, bool returnFinalTarget = false) + { + ArgumentNullException.ThrowIfNull(fileInfo); + + return ResolveLinkTarget(fileInfo.FullName, returnFinalTarget); + } } -- cgit v1.2.3 From 99c68ddd50353b62f86067a8336c001f0592131d Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Mon, 17 Nov 2025 14:09:05 -0500 Subject: Backport pull request #15468 from jellyfin/release-10.11.z Check if target exists before trying to follow it Original-merge: 5878b1ffc569bc7e7204a07e77e0bb57d3984e56 Merged-by: joshuaboniface Backported-by: Bond_009 --- MediaBrowser.Controller/IO/FileSystemHelper.cs | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'MediaBrowser.Controller/IO/FileSystemHelper.cs') diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs index 324aea7e3..3e390ca42 100644 --- a/MediaBrowser.Controller/IO/FileSystemHelper.cs +++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs @@ -92,6 +92,11 @@ public static class FileSystemHelper return null; } + if (!targetInfo.Exists) + { + return targetInfo; + } + var currentPath = targetInfo.FullName; var visited = new HashSet(StringComparer.Ordinal) { linkPath, currentPath }; while (File.ResolveLinkTarget(currentPath, returnFinalTarget: false) is FileInfo linkInfo) -- cgit v1.2.3 From 5d4627858418722c85d76d27b4ba982f209505f2 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:04:23 -0500 Subject: Backport pull request #15568 from jellyfin/release-10.11.z Fix ResolveLinkTarget crashing on exFAT drives Original-merge: fbb9a0b2c7c5afbc56be76a4eb11a1045f0ab0f0 Merged-by: crobibero Backported-by: Bond_009 --- MediaBrowser.Controller/IO/FileSystemHelper.cs | 42 +++++++++++++++++++++----- 1 file changed, 34 insertions(+), 8 deletions(-) (limited to 'MediaBrowser.Controller/IO/FileSystemHelper.cs') diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs index 3e390ca42..44b7fadf5 100644 --- a/MediaBrowser.Controller/IO/FileSystemHelper.cs +++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs @@ -63,6 +63,29 @@ public static class FileSystemHelper } } + /// + /// Resolves a single link hop for the specified path. + /// + /// + /// Returns null if the path is not a symbolic link or the filesystem does not support link resolution (e.g., exFAT). + /// + /// The file path to resolve. + /// + /// A representing the next link target if the path is a link; otherwise, null. + /// + private static FileInfo? Resolve(string path) + { + try + { + return File.ResolveLinkTarget(path, returnFinalTarget: false) as FileInfo; + } + catch (IOException) + { + // Filesystem doesn't support links (e.g., exFAT). + return null; + } + } + /// /// Gets the target of the specified file link. /// @@ -84,23 +107,26 @@ public static class FileSystemHelper if (!returnFinalTarget) { - return File.ResolveLinkTarget(linkPath, returnFinalTarget: false) as FileInfo; - } - - if (File.ResolveLinkTarget(linkPath, returnFinalTarget: false) is not FileInfo targetInfo) - { - return null; + return Resolve(linkPath); } - if (!targetInfo.Exists) + var targetInfo = Resolve(linkPath); + if (targetInfo is null || !targetInfo.Exists) { return targetInfo; } var currentPath = targetInfo.FullName; var visited = new HashSet(StringComparer.Ordinal) { linkPath, currentPath }; - while (File.ResolveLinkTarget(currentPath, returnFinalTarget: false) is FileInfo linkInfo) + + while (true) { + var linkInfo = Resolve(currentPath); + if (linkInfo is null) + { + break; + } + var targetPath = linkInfo.FullName; // If an infinite loop is detected, return the file info for the -- cgit v1.2.3