From 6a2df35b37dbae830508b9291247b3c1ca514c4d Mon Sep 17 00:00:00 2001 From: cvium Date: Sat, 11 Sep 2021 12:31:29 +0200 Subject: [PATCH 1/8] Read file length for symlinks, supersedes #5775 and #5824 --- ...linkFollowingPhysicalFileResultExecutor.cs | 123 ++++++++++++++++++ Jellyfin.Server/Startup.cs | 6 + 2 files changed, 129 insertions(+) create mode 100644 Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs new file mode 100644 index 0000000000..4f1ce57fc8 --- /dev/null +++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs @@ -0,0 +1,123 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; +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 +{ + /// + public class SymlinkFollowingPhysicalFileResultExecutor : PhysicalFileResultExecutor + { + /// + /// Initializes a new instance of the class. + /// + /// + public SymlinkFollowingPhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory) + { + } + + /// + 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 Stream thisFileStream = AsyncFile.OpenRead(path); + length = thisFileStream.Length; + } + + return new FileMetadata + { + Exists = fileInfo.Exists, + Length = length, + LastModified = fileInfo.LastWriteTimeUtc + }; + } + + /// + protected override Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue range, long rangeLength) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + if (range != null && rangeLength == 0) + { + return Task.CompletedTask; + } + + // It's a bit of wasted IO to perform this check again, but non-symlinks shouldn't use this code + if (!IsSymLink(result.FileName)) + { + return base.WriteFileAsync(context, result, range, rangeLength); + } + + var response = context.HttpContext.Response; + + if (range != null) + { + return SendFileAsync(result.FileName, + response, + offset: range.From ?? 0L, + count: rangeLength); + } + + return SendFileAsync(result.FileName, + response, + offset: 0, + count: null); + } + + private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count) + { + 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; + + await using var fileStream = new FileStream( + filePath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + bufferSize: bufferSize, + options: (AsyncFile.UseAsyncIO ? FileOptions.Asynchronous : FileOptions.None) | FileOptions.SequentialScan); + + fileStream.Seek(offset, SeekOrigin.Begin); + await StreamCopyOperation + .CopyToAsync(fileStream, response.Body, count, bufferSize, CancellationToken.None) + .ConfigureAwait(true); + } + + private static bool IsSymLink(string path) + { + var fileInfo = new FileInfo(path); + return (fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint; + } + } +} diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 60cdc2f6fe..8085c26308 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -7,6 +7,7 @@ using System.Text; using Jellyfin.Networking.Configuration; using Jellyfin.Server.Extensions; using Jellyfin.Server.Implementations; +using Jellyfin.Server.Infrastructure; using Jellyfin.Server.Middleware; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -14,6 +15,8 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Extensions; 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; @@ -56,6 +59,9 @@ namespace Jellyfin.Server { options.HttpsPort = _serverApplicationHost.HttpsPort; }); + + // TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371 + services.AddSingleton, SymlinkFollowingPhysicalFileResultExecutor>(); services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration()); services.AddJellyfinApiSwagger(); From 30152c8d967500f867feed521ed9f6539f8b0712 Mon Sep 17 00:00:00 2001 From: cvium Date: Sat, 11 Sep 2021 12:39:52 +0200 Subject: [PATCH 2/8] Include the MIT license --- ...linkFollowingPhysicalFileResultExecutor.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs index 4f1ce57fc8..c64b8074b8 100644 --- a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs +++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs @@ -1,4 +1,28 @@ -using System; +// 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; From 3bc9f388339bd1266a6a31e93dac81817da1b312 Mon Sep 17 00:00:00 2001 From: cvium Date: Sat, 11 Sep 2021 12:47:01 +0200 Subject: [PATCH 3/8] Fix SA1614 and SA1116 --- .../SymlinkFollowingPhysicalFileResultExecutor.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs index c64b8074b8..401b3bb841 100644 --- a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs +++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs @@ -42,7 +42,7 @@ namespace Jellyfin.Server.Infrastructure /// /// Initializes a new instance of the class. /// - /// + /// An instance of the interface. public SymlinkFollowingPhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory) { } @@ -95,13 +95,15 @@ namespace Jellyfin.Server.Infrastructure if (range != null) { - return SendFileAsync(result.FileName, + return SendFileAsync( + result.FileName, response, offset: range.From ?? 0L, count: rangeLength); } - return SendFileAsync(result.FileName, + return SendFileAsync( + result.FileName, response, offset: 0, count: null); From 794b73c62d7f9e5a962dc6031e7772c1a644aae9 Mon Sep 17 00:00:00 2001 From: cvium Date: Sat, 11 Sep 2021 13:32:59 +0200 Subject: [PATCH 4/8] Use File.GetAttributes instead of creating a new FileInfo --- .../SymlinkFollowingPhysicalFileResultExecutor.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs index 401b3bb841..93bd2c1ba5 100644 --- a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs +++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs @@ -140,10 +140,6 @@ namespace Jellyfin.Server.Infrastructure .ConfigureAwait(true); } - private static bool IsSymLink(string path) - { - var fileInfo = new FileInfo(path); - return (fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint; - } + private static bool IsSymLink(string path) => (File.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint; } } From e34aa22dc10188e32ad29e9b29a77d46473f9932 Mon Sep 17 00:00:00 2001 From: cvium Date: Sat, 11 Sep 2021 14:09:22 +0200 Subject: [PATCH 5/8] Remove AsyncFile.OpenRead --- .../SymlinkFollowingPhysicalFileResultExecutor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs index 93bd2c1ba5..917384cc38 100644 --- a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs +++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs @@ -55,7 +55,7 @@ namespace Jellyfin.Server.Infrastructure // 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 Stream thisFileStream = AsyncFile.OpenRead(path); + using Stream thisFileStream = File.OpenRead(path); length = thisFileStream.Length; } From 6b8b7f1426a510ce3f0d40d40f3ceb59ccc03934 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sat, 11 Sep 2021 14:19:50 +0200 Subject: [PATCH 6/8] Update Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs Co-authored-by: Bond-009 --- .../SymlinkFollowingPhysicalFileResultExecutor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs index 917384cc38..db09285c7e 100644 --- a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs +++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs @@ -124,7 +124,7 @@ namespace Jellyfin.Server.Infrastructure } // Copied from SendFileFallback.SendFileAsync - const int bufferSize = 1024 * 16; + const int BufferSize = 1024 * 16; await using var fileStream = new FileStream( filePath, From cec0dd94baac67852898cc129fc02ceca364b98c Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sat, 11 Sep 2021 15:06:56 +0200 Subject: [PATCH 7/8] Update Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs --- .../SymlinkFollowingPhysicalFileResultExecutor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs index db09285c7e..660f62428a 100644 --- a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs +++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs @@ -131,7 +131,7 @@ namespace Jellyfin.Server.Infrastructure FileMode.Open, FileAccess.Read, FileShare.ReadWrite, - bufferSize: bufferSize, + bufferSize: BufferSize, options: (AsyncFile.UseAsyncIO ? FileOptions.Asynchronous : FileOptions.None) | FileOptions.SequentialScan); fileStream.Seek(offset, SeekOrigin.Begin); From 62113c46034458d192804fb30708ed9ca414e131 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sat, 11 Sep 2021 15:07:09 +0200 Subject: [PATCH 8/8] Update Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs --- .../SymlinkFollowingPhysicalFileResultExecutor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs index 660f62428a..e171fc145c 100644 --- a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs +++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs @@ -136,7 +136,7 @@ namespace Jellyfin.Server.Infrastructure fileStream.Seek(offset, SeekOrigin.Begin); await StreamCopyOperation - .CopyToAsync(fileStream, response.Body, count, bufferSize, CancellationToken.None) + .CopyToAsync(fileStream, response.Body, count, BufferSize, CancellationToken.None) .ConfigureAwait(true); }