Browse Source

Backport pull request #15263 from jellyfin/release-10.11.z

Resolve symlinks for static media source infos

Original-merge: 3b2d64995aab63ebaa6832c059a3cc0bdebe90dc

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
revam 3 days ago
parent
commit
5ea3910af9

+ 2 - 1
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Security;
 using Jellyfin.Extensions;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Model.IO;
 using Microsoft.Extensions.Logging;
 
@@ -260,7 +261,7 @@ namespace Emby.Server.Implementations.IO
                     {
                         try
                         {
-                            var targetFileInfo = (FileInfo?)fileInfo.ResolveLinkTarget(returnFinalTarget: true);
+                            var targetFileInfo = FileSystemHelper.ResolveLinkTarget(fileInfo, returnFinalTarget: true);
                             if (targetFileInfo is not null)
                             {
                                 result.Exists = targetFileInfo.Exists;

+ 2 - 1
Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs

@@ -1,6 +1,7 @@
 using System;
 using System.IO;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.IO;
 
@@ -92,7 +93,7 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
 
     private static string GetFileContent(FileInfo dirIgnoreFile)
     {
-        dirIgnoreFile = (FileInfo?)dirIgnoreFile.ResolveLinkTarget(returnFinalTarget: true) ?? dirIgnoreFile;
+        dirIgnoreFile = FileSystemHelper.ResolveLinkTarget(dirIgnoreFile, returnFinalTarget: true) ?? dirIgnoreFile;
         if (!dirIgnoreFile.Exists)
         {
             return string.Empty;

+ 2 - 2
Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs

@@ -254,10 +254,10 @@ public class TrickplayManager : ITrickplayManager
                 }
 
                 // We support video backdrops, but we should not generate trickplay images for them
-                var parentDirectory = Directory.GetParent(mediaPath);
+                var parentDirectory = Directory.GetParent(video.Path);
                 if (parentDirectory is not null && string.Equals(parentDirectory.Name, "backdrops", StringComparison.OrdinalIgnoreCase))
                 {
-                    _logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", mediaPath, video.Id);
+                    _logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", video.Path, video.Id);
                     return;
                 }
 

+ 11 - 1
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,

+ 74 - 0
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
             }
         }
     }
+
+    /// <summary>
+    /// Gets the target of the specified file link.
+    /// </summary>
+    /// <remarks>
+    /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128.
+    /// </remarks>
+    /// <param name="linkPath">The path of the file link.</param>
+    /// <param name="returnFinalTarget">true to follow links to the final target; false to return the immediate next link.</param>
+    /// <returns>
+    /// A <see cref="FileInfo"/> if the <paramref name="linkPath"/> is a link, regardless of if the target exists; otherwise, <c>null</c>.
+    /// </returns>
+    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<string>(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;
+    }
+
+    /// <summary>
+    /// Gets the target of the specified file link.
+    /// </summary>
+    /// <remarks>
+    /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128.
+    /// </remarks>
+    /// <param name="fileInfo">The file info of the file link.</param>
+    /// <param name="returnFinalTarget">true to follow links to the final target; false to return the immediate next link.</param>
+    /// <returns>
+    /// A <see cref="FileInfo"/> if the <paramref name="fileInfo"/> is a link, regardless of if the target exists; otherwise, <c>null</c>.
+    /// </returns>
+    public static FileInfo? ResolveLinkTarget(FileInfo fileInfo, bool returnFinalTarget = false)
+    {
+        ArgumentNullException.ThrowIfNull(fileInfo);
+
+        return ResolveLinkTarget(fileInfo.FullName, returnFinalTarget);
+    }
 }