Backport pull request #15209 from jellyfin/release-10.11.z
Improve symlink handling
Original-merge: e5656af1f2
Merged-by: crobibero <cody@robibe.ro>
Backported-by: Bond_009 <bond.009@outlook.com>
This commit is contained in:
@@ -252,47 +252,40 @@ namespace Emby.Server.Implementations.IO
|
|||||||
{
|
{
|
||||||
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
|
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)
|
if (info is FileInfo fileInfo)
|
||||||
{
|
{
|
||||||
result.Length = fileInfo.Length;
|
result.CreationTimeUtc = GetCreationTimeUtc(info);
|
||||||
|
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
|
||||||
// Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes!
|
if (fileInfo.LinkTarget is not null)
|
||||||
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
|
|
||||||
{
|
{
|
||||||
try
|
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)
|
catch (UnauthorizedAccessException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
|
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
|
||||||
}
|
}
|
||||||
catch (IOException ex)
|
}
|
||||||
{
|
else
|
||||||
// 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
|
result.Length = fileInfo.Length;
|
||||||
_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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.CreationTimeUtc = GetCreationTimeUtc(info);
|
|
||||||
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fast path in case the ignore files isn't a symlink and is empty
|
// Fast path in case the ignore files isn't a symlink and is empty
|
||||||
if ((dirIgnoreFile.Attributes & FileAttributes.ReparsePoint) == 0
|
if (dirIgnoreFile.LinkTarget is null && dirIgnoreFile.Length == 0)
|
||||||
&& dirIgnoreFile.Length == 0)
|
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -93,6 +92,12 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
|||||||
|
|
||||||
private static string GetFileContent(FileInfo dirIgnoreFile)
|
private static string GetFileContent(FileInfo dirIgnoreFile)
|
||||||
{
|
{
|
||||||
|
dirIgnoreFile = (FileInfo?)dirIgnoreFile.ResolveLinkTarget(returnFinalTarget: true) ?? dirIgnoreFile;
|
||||||
|
if (!dirIgnoreFile.Exists)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
using (var reader = dirIgnoreFile.OpenText())
|
using (var reader = dirIgnoreFile.OpenText())
|
||||||
{
|
{
|
||||||
return reader.ReadToEnd();
|
return reader.ReadToEnd();
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,15 +16,12 @@ using Jellyfin.Networking.HappyEyeballs;
|
|||||||
using Jellyfin.Server.Extensions;
|
using Jellyfin.Server.Extensions;
|
||||||
using Jellyfin.Server.HealthChecks;
|
using Jellyfin.Server.HealthChecks;
|
||||||
using Jellyfin.Server.Implementations.Extensions;
|
using Jellyfin.Server.Implementations.Extensions;
|
||||||
using Jellyfin.Server.Infrastructure;
|
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Extensions;
|
using MediaBrowser.Controller.Extensions;
|
||||||
using MediaBrowser.XbmcMetadata;
|
using MediaBrowser.XbmcMetadata;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
|
||||||
using Microsoft.AspNetCore.StaticFiles;
|
using Microsoft.AspNetCore.StaticFiles;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -69,8 +66,6 @@ namespace Jellyfin.Server
|
|||||||
options.HttpsPort = _serverApplicationHost.HttpsPort;
|
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.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
|
||||||
services.AddJellyfinDbContext(_serverApplicationHost.ConfigurationManager, _configuration);
|
services.AddJellyfinDbContext(_serverApplicationHost.ConfigurationManager, _configuration);
|
||||||
services.AddJellyfinApiSwagger();
|
services.AddJellyfinApiSwagger();
|
||||||
|
|||||||
Reference in New Issue
Block a user