Browse Source

Improve symlink handling (#15209)

Tim Eisele 1 day ago
parent
commit
e5656af1f2

+ 20 - 27
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -252,47 +252,40 @@ namespace Emby.Server.Implementations.IO
             {
                 result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
 
-                // if (!result.IsDirectory)
-                // {
-                //    result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
-                // }
-
                 if (info is FileInfo fileInfo)
                 {
-                    result.Length = fileInfo.Length;
-
-                    // Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes!
-                    if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
+                    result.CreationTimeUtc = GetCreationTimeUtc(info);
+                    result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
+                    if (fileInfo.LinkTarget is not null)
                     {
                         try
                         {
-                            using (var fileHandle = File.OpenHandle(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+                            var targetFileInfo = (FileInfo?)fileInfo.ResolveLinkTarget(returnFinalTarget: true);
+                            if (targetFileInfo is not null)
                             {
-                                result.Length = RandomAccess.GetLength(fileHandle);
+                                result.Exists = targetFileInfo.Exists;
+                                if (result.Exists)
+                                {
+                                    result.Length = targetFileInfo.Length;
+                                    result.CreationTimeUtc = GetCreationTimeUtc(targetFileInfo);
+                                    result.LastWriteTimeUtc = GetLastWriteTimeUtc(targetFileInfo);
+                                }
+                            }
+                            else
+                            {
+                                result.Exists = false;
                             }
-                        }
-                        catch (FileNotFoundException ex)
-                        {
-                            // Dangling symlinks cannot be detected before opening the file unfortunately...
-                            _logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
-                            result.Exists = false;
                         }
                         catch (UnauthorizedAccessException ex)
                         {
                             _logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
                         }
-                        catch (IOException ex)
-                        {
-                            // IOException generally means the file is not accessible due to filesystem issues
-                            // Catch this exception and mark the file as not exist to ignore it
-                            _logger.LogError(ex, "Reading the file at {Path} failed due to an IO Exception. Marking the file as not existing", fileInfo.FullName);
-                            result.Exists = false;
-                        }
+                    }
+                    else
+                    {
+                        result.Length = fileInfo.Length;
                     }
                 }
-
-                result.CreationTimeUtc = GetCreationTimeUtc(info);
-                result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
             }
             else
             {

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

@@ -51,8 +51,7 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
             }
 
             // Fast path in case the ignore files isn't a symlink and is empty
-            if ((dirIgnoreFile.Attributes & FileAttributes.ReparsePoint) == 0
-                && dirIgnoreFile.Length == 0)
+            if (dirIgnoreFile.LinkTarget is null && dirIgnoreFile.Length == 0)
             {
                 return true;
             }
@@ -93,6 +92,12 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
 
     private static string GetFileContent(FileInfo dirIgnoreFile)
     {
+        dirIgnoreFile = (FileInfo?)dirIgnoreFile.ResolveLinkTarget(returnFinalTarget: true) ?? dirIgnoreFile;
+        if (!dirIgnoreFile.Exists)
+        {
+            return string.Empty;
+        }
+
         using (var reader = dirIgnoreFile.OpenText())
         {
             return reader.ReadToEnd();

+ 0 - 151
Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs

@@ -1,151 +0,0 @@
-// The MIT License (MIT)
-//
-// Copyright (c) .NET Foundation and Contributors
-//
-// All rights reserved.
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.Infrastructure;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Jellyfin.Server.Infrastructure
-{
-    /// <inheritdoc />
-    public class SymlinkFollowingPhysicalFileResultExecutor : PhysicalFileResultExecutor
-    {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SymlinkFollowingPhysicalFileResultExecutor"/> class.
-        /// </summary>
-        /// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
-        public SymlinkFollowingPhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory)
-        {
-        }
-
-        /// <inheritdoc />
-        protected override FileMetadata GetFileInfo(string path)
-        {
-            var fileInfo = new FileInfo(path);
-            var length = fileInfo.Length;
-            // This may or may not be fixed in .NET 6, but looks like it will not https://github.com/dotnet/aspnetcore/issues/34371
-            if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
-            {
-                using var fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
-                length = RandomAccess.GetLength(fileHandle);
-            }
-
-            return new FileMetadata
-            {
-                Exists = fileInfo.Exists,
-                Length = length,
-                LastModified = fileInfo.LastWriteTimeUtc
-            };
-        }
-
-        /// <inheritdoc />
-        protected override async Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength)
-        {
-            ArgumentNullException.ThrowIfNull(context);
-            ArgumentNullException.ThrowIfNull(result);
-
-            if (range is not null && rangeLength == 0)
-            {
-                return;
-            }
-
-            // It's a bit of wasted IO to perform this check again, but non-symlinks shouldn't use this code
-            if (!IsSymLink(result.FileName))
-            {
-                await base.WriteFileAsync(context, result, range, rangeLength).ConfigureAwait(false);
-                return;
-            }
-
-            var response = context.HttpContext.Response;
-
-            if (range is not null)
-            {
-                await SendFileAsync(
-                    result.FileName,
-                    response,
-                    offset: range.From ?? 0L,
-                    count: rangeLength).ConfigureAwait(false);
-                return;
-            }
-
-            await SendFileAsync(
-                result.FileName,
-                response,
-                offset: 0,
-                count: null).ConfigureAwait(false);
-        }
-
-        private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count, CancellationToken cancellationToken = default)
-        {
-            var fileInfo = GetFileInfo(filePath);
-            if (offset < 0 || offset > fileInfo.Length)
-            {
-                throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
-            }
-
-            if (count.HasValue
-                && (count.Value < 0 || count.Value > fileInfo.Length - offset))
-            {
-                throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
-            }
-
-            // Copied from SendFileFallback.SendFileAsync
-            const int BufferSize = 1024 * 16;
-
-            var useRequestAborted = !cancellationToken.CanBeCanceled;
-            var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken;
-
-            var fileStream = new FileStream(
-                filePath,
-                FileMode.Open,
-                FileAccess.Read,
-                FileShare.ReadWrite,
-                bufferSize: BufferSize,
-                options: FileOptions.Asynchronous | FileOptions.SequentialScan);
-            await using (fileStream.ConfigureAwait(false))
-            {
-                try
-                {
-                    localCancel.ThrowIfCancellationRequested();
-                    fileStream.Seek(offset, SeekOrigin.Begin);
-                    await StreamCopyOperation
-                        .CopyToAsync(fileStream, response.Body, count, BufferSize, localCancel)
-                        .ConfigureAwait(true);
-                }
-                catch (OperationCanceledException) when (useRequestAborted)
-                {
-                }
-            }
-        }
-
-        private static bool IsSymLink(string path) => (File.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
-    }
-}

+ 0 - 5
Jellyfin.Server/Startup.cs

@@ -16,15 +16,12 @@ using Jellyfin.Networking.HappyEyeballs;
 using Jellyfin.Server.Extensions;
 using Jellyfin.Server.HealthChecks;
 using Jellyfin.Server.Implementations.Extensions;
-using Jellyfin.Server.Infrastructure;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.XbmcMetadata;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.Infrastructure;
 using Microsoft.AspNetCore.StaticFiles;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
@@ -69,8 +66,6 @@ namespace Jellyfin.Server
                 options.HttpsPort = _serverApplicationHost.HttpsPort;
             });
 
-            // TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
-            services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
             services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
             services.AddJellyfinDbContext(_serverApplicationHost.ConfigurationManager, _configuration);
             services.AddJellyfinApiSwagger();