< Summary - Jellyfin

Information
Class: Jellyfin.Server.Infrastructure.SymlinkFollowingPhysicalFileResultExecutor
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 32
Coverable lines: 32
Total lines: 139
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 10
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
GetFileInfo(...)0%620%
WriteFileAsync(...)0%7280%
IsSymLink(...)100%210%

File(s)

/srv/git/jellyfin/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs

#LineLine coverage
 1// The MIT License (MIT)
 2//
 3// Copyright (c) .NET Foundation and Contributors
 4//
 5// All rights reserved.
 6//
 7// Permission is hereby granted, free of charge, to any person obtaining a copy
 8// of this software and associated documentation files (the "Software"), to deal
 9// in the Software without restriction, including without limitation the rights
 10// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 11// copies of the Software, and to permit persons to whom the Software is
 12// furnished to do so, subject to the following conditions:
 13//
 14// The above copyright notice and this permission notice shall be included in all
 15// copies or substantial portions of the Software.
 16//
 17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 18// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 19// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 20// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 21// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 22// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 23// SOFTWARE.
 24
 25using System;
 26using System.IO;
 27using System.Threading;
 28using System.Threading.Tasks;
 29using Microsoft.AspNetCore.Http;
 30using Microsoft.AspNetCore.Http.Extensions;
 31using Microsoft.AspNetCore.Mvc;
 32using Microsoft.AspNetCore.Mvc.Infrastructure;
 33using Microsoft.Extensions.Logging;
 34using Microsoft.Net.Http.Headers;
 35
 36namespace Jellyfin.Server.Infrastructure
 37{
 38    /// <inheritdoc />
 39    public class SymlinkFollowingPhysicalFileResultExecutor : PhysicalFileResultExecutor
 40    {
 41        /// <summary>
 42        /// Initializes a new instance of the <see cref="SymlinkFollowingPhysicalFileResultExecutor"/> class.
 43        /// </summary>
 44        /// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
 045        public SymlinkFollowingPhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory)
 46        {
 047        }
 48
 49        /// <inheritdoc />
 50        protected override FileMetadata GetFileInfo(string path)
 51        {
 052            var fileInfo = new FileInfo(path);
 053            var length = fileInfo.Length;
 54            // This may or may not be fixed in .NET 6, but looks like it will not https://github.com/dotnet/aspnetcore/i
 055            if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
 56            {
 057                using var fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
 058                length = RandomAccess.GetLength(fileHandle);
 59            }
 60
 061            return new FileMetadata
 062            {
 063                Exists = fileInfo.Exists,
 064                Length = length,
 065                LastModified = fileInfo.LastWriteTimeUtc
 066            };
 67        }
 68
 69        /// <inheritdoc />
 70        protected override Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? r
 71        {
 072            ArgumentNullException.ThrowIfNull(context);
 073            ArgumentNullException.ThrowIfNull(result);
 74
 075            if (range is not null && rangeLength == 0)
 76            {
 077                return Task.CompletedTask;
 78            }
 79
 80            // It's a bit of wasted IO to perform this check again, but non-symlinks shouldn't use this code
 081            if (!IsSymLink(result.FileName))
 82            {
 083                return base.WriteFileAsync(context, result, range, rangeLength);
 84            }
 85
 086            var response = context.HttpContext.Response;
 87
 088            if (range is not null)
 89            {
 090                return SendFileAsync(
 091                    result.FileName,
 092                    response,
 093                    offset: range.From ?? 0L,
 094                    count: rangeLength);
 95            }
 96
 097            return SendFileAsync(
 098                result.FileName,
 099                response,
 0100                offset: 0,
 0101                count: null);
 102        }
 103
 104        private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count)
 105        {
 106            var fileInfo = GetFileInfo(filePath);
 107            if (offset < 0 || offset > fileInfo.Length)
 108            {
 109                throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
 110            }
 111
 112            if (count.HasValue
 113                && (count.Value < 0 || count.Value > fileInfo.Length - offset))
 114            {
 115                throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
 116            }
 117
 118            // Copied from SendFileFallback.SendFileAsync
 119            const int BufferSize = 1024 * 16;
 120
 121            var fileStream = new FileStream(
 122                filePath,
 123                FileMode.Open,
 124                FileAccess.Read,
 125                FileShare.ReadWrite,
 126                bufferSize: BufferSize,
 127                options: FileOptions.Asynchronous | FileOptions.SequentialScan);
 128            await using (fileStream.ConfigureAwait(false))
 129            {
 130                fileStream.Seek(offset, SeekOrigin.Begin);
 131                await StreamCopyOperation
 132                    .CopyToAsync(fileStream, response.Body, count, BufferSize, CancellationToken.None)
 133                    .ConfigureAwait(true);
 134            }
 135        }
 136
 0137        private static bool IsSymLink(string path) => (File.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAt
 138    }
 139}