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/Entities/BaseItem.cs | 12 ++++- MediaBrowser.Controller/IO/FileSystemHelper.cs | 74 ++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) (limited to 'MediaBrowser.Controller') diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 4989f0f3f..3c46d53e5 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -24,6 +24,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Persistence; @@ -1127,6 +1128,15 @@ namespace MediaBrowser.Controller.Entities var protocol = item.PathProtocol; + // Resolve the item path so everywhere we use the media source it will always point to + // the correct path even if symlinks are in use. Calling ResolveLinkTarget on a non-link + // path will return null, so it's safe to check for all paths. + var itemPath = item.Path; + if (protocol is MediaProtocol.File && FileSystemHelper.ResolveLinkTarget(itemPath, returnFinalTarget: true) is { Exists: true } linkInfo) + { + itemPath = linkInfo.FullName; + } + var info = new MediaSourceInfo { Id = item.Id.ToString("N", CultureInfo.InvariantCulture), @@ -1134,7 +1144,7 @@ namespace MediaBrowser.Controller.Entities MediaStreams = MediaSourceManager.GetMediaStreams(item.Id), MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id), Name = GetMediaSourceName(item), - Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path, + Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath, RunTimeTicks = item.RunTimeTicks, Container = item.Container, Size = item.Size, 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