aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
blob: 4f1ce57fc8346b80bb0caeb59fb28de4ee82828d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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
{
    /// <inheritdoc />
    public class SymlinkFollowingPhysicalFileResultExecutor : PhysicalFileResultExecutor
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="SymlinkFollowingPhysicalFileResultExecutor"/> class.
        /// </summary>
        /// <param name="loggerFactory"></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 Stream thisFileStream = AsyncFile.OpenRead(path);
                length = thisFileStream.Length;
            }

            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: (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;
        }
    }
}