| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139 | // 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)        {            ArgumentNullException.ThrowIfNull(context);            ArgumentNullException.ThrowIfNull(result);            if (range is not 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 is not 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;            var fileStream = new FileStream(                filePath,                FileMode.Open,                FileAccess.Read,                FileShare.ReadWrite,                bufferSize: BufferSize,                options: FileOptions.Asynchronous | FileOptions.SequentialScan);            await using (fileStream.ConfigureAwait(false))            {                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;    }}
 |