aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShadowghost <Shadowghost@users.noreply.github.com>2025-10-27 15:43:27 -0400
committerBond_009 <bond.009@outlook.com>2025-10-27 15:43:27 -0400
commit423c2654c087e9c23fafe6766ccc7168b6b2dd3a (patch)
tree87cbcc5c3bf28632c649c698397a34acf6251a35
parent4dc826644d213ee47f4181009be3c928448b0879 (diff)
Backport pull request #15209 from jellyfin/release-10.11.z
Improve symlink handling Original-merge: e5656af1f2e740c6e4f78f613d47d37567940ed8 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com>
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs47
-rw-r--r--Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs9
-rw-r--r--Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs151
-rw-r--r--Jellyfin.Server/Startup.cs5
4 files changed, 27 insertions, 185 deletions
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 1510e537d..97e89ca3d 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/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
{
diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
index bafe3ad43..959acd475 100644
--- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
+++ b/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();
diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
deleted file mode 100644
index 910b5c467..000000000
--- a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
+++ /dev/null
@@ -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;
- }
-}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index aa8f6dd1c..5032b2aec 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/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();