diff options
| author | Orry Verducci <orry@orryverducci.co.uk> | 2021-10-31 10:04:14 +0000 |
|---|---|---|
| committer | Orry Verducci <orry@orryverducci.co.uk> | 2021-10-31 10:04:14 +0000 |
| commit | 3a89e88033fc54360d926d73591293660e6bf43c (patch) | |
| tree | f3064d4893b72bb329521858ea51d1f283a3ae47 /Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs | |
| parent | d5b63092ed1b4b6ef4da2a5cdccec472aa1c06b3 (diff) | |
| parent | 5a7433472ef88c7e8e52840425a7296e242155ee (diff) | |
Merge remote-tracking branch 'upstream/master' into mbaff-interlace-detection
Diffstat (limited to 'Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs')
| -rw-r--r-- | Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs | 144 |
1 files changed, 144 insertions, 0 deletions
diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs new file mode 100644 index 000000000..73a619b8d --- /dev/null +++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs @@ -0,0 +1,144 @@ +// 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 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: FileOptions.Asynchronous | 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) => (File.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint; + } +} |
