File: Infrastructure\PhysicalFileResultExecutor.cs
Web Access
Project: src\src\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj (Microsoft.AspNetCore.Mvc.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
 
namespace Microsoft.AspNetCore.Mvc.Infrastructure;
 
/// <summary>
/// A <see cref="IActionResultExecutor{PhysicalFileResult}"/> for <see cref="PhysicalFileResult"/>.
/// </summary>
public partial class PhysicalFileResultExecutor : FileResultExecutorBase, IActionResultExecutor<PhysicalFileResult>
{
    /// <summary>
    /// Initializes a new instance of <see cref="PhysicalFileResultExecutor"/>.
    /// </summary>
    /// <param name="loggerFactory">The factory used to create loggers.</param>
    public PhysicalFileResultExecutor(ILoggerFactory loggerFactory)
        : base(CreateLogger<PhysicalFileResultExecutor>(loggerFactory))
    {
    }
 
    /// <inheritdoc />
    public virtual Task ExecuteAsync(ActionContext context, PhysicalFileResult result)
    {
        ArgumentNullException.ThrowIfNull(context);
        ArgumentNullException.ThrowIfNull(result);
 
        var fileInfo = GetFileInfo(result.FileName);
        if (!fileInfo.Exists)
        {
            throw new FileNotFoundException(
                Resources.FormatFileResult_InvalidPath(result.FileName), result.FileName);
        }
 
        Log.ExecutingFileResult(Logger, result, result.FileName);
 
        var lastModified = result.LastModified ?? fileInfo.LastModified;
        var (range, rangeLength, serveBody) = SetHeadersAndLog(
            context,
            result,
            fileInfo.Length,
            result.EnableRangeProcessing,
            lastModified,
            result.EntityTag);
 
        if (serveBody)
        {
            return WriteFileAsync(context, result, range, rangeLength);
        }
 
        return Task.CompletedTask;
    }
 
    /// <inheritdoc/>
    protected virtual Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength)
    {
        return WriteFileAsyncInternal(context.HttpContext, result, range, rangeLength, Logger);
    }
 
    internal static Task WriteFileAsyncInternal(
        HttpContext httpContext,
        PhysicalFileResult result,
        RangeItemHeaderValue? range,
        long rangeLength,
        ILogger logger)
    {
        ArgumentNullException.ThrowIfNull(httpContext);
        ArgumentNullException.ThrowIfNull(result);
 
        if (range != null && rangeLength == 0)
        {
            return Task.CompletedTask;
        }
 
        var response = httpContext.Response;
        if (!Path.IsPathRooted(result.FileName))
        {
            throw new NotSupportedException(Resources.FormatFileResult_PathNotRooted(result.FileName));
        }
 
        if (range != null)
        {
            Log.WritingRangeToBody(logger);
        }
 
        if (range != null)
        {
            return response.SendFileAsync(result.FileName,
                offset: range.From ?? 0L,
                count: rangeLength);
        }
 
        return response.SendFileAsync(result.FileName,
            offset: 0,
            count: null);
    }
 
    /// <summary>
    /// Obsolete. This API is no longer called.
    /// </summary>
    [Obsolete("This API is no longer called.")]
    protected virtual Stream GetFileStream(string path)
    {
        ArgumentNullException.ThrowIfNull(path);
 
        return new FileStream(
                path,
                FileMode.Open,
                FileAccess.Read,
                FileShare.ReadWrite,
                BufferSize,
                FileOptions.Asynchronous | FileOptions.SequentialScan);
    }
 
    /// <summary>
    /// Get the file metadata for a path.
    /// </summary>
    /// <param name="path">The file path.</param>
    /// <returns>The <see cref="FileMetadata"/> for the path.</returns>
    protected virtual FileMetadata GetFileInfo(string path)
    {
        var fileInfo = new FileInfo(path);
 
        // It means we are dealing with a symlink and need to get the information
        // from the target file instead.
        if (fileInfo.Exists && !string.IsNullOrEmpty(fileInfo.LinkTarget))
        {
            fileInfo = (FileInfo?)fileInfo.ResolveLinkTarget(returnFinalTarget: true) ?? fileInfo;
        }
 
        return new FileMetadata
        {
            Exists = fileInfo.Exists,
            Length = fileInfo.Length,
            LastModified = fileInfo.LastWriteTimeUtc,
        };
    }
 
    /// <summary>
    /// Represents metadata for a file.
    /// </summary>
    protected class FileMetadata
    {
        /// <summary>
        /// Whether a file exists.
        /// </summary>
        public bool Exists { get; set; }
 
        /// <summary>
        /// The file length.
        /// </summary>
        public long Length { get; set; }
 
        /// <summary>
        /// When the file was last modified.
        /// </summary>
        public DateTimeOffset LastModified { get; set; }
    }
 
    private static partial class Log
    {
        public static void ExecutingFileResult(ILogger logger, FileResult fileResult, string fileName)
        {
            if (logger.IsEnabled(LogLevel.Information))
            {
                var fileResultType = fileResult.GetType().Name;
                ExecutingFileResult(logger, fileResultType, fileName, fileResult.FileDownloadName);
            }
        }
 
        [LoggerMessage(1, LogLevel.Information, "Executing {FileResultType}, sending file '{FileDownloadPath}' with download name '{FileDownloadName}' ...", EventName = "ExecutingFileResult", SkipEnabledCheck = true)]
        private static partial void ExecutingFileResult(ILogger logger, string fileResultType, string fileDownloadPath, string fileDownloadName);
 
        [LoggerMessage(17, LogLevel.Debug, "Writing the requested range of bytes to the body...", EventName = "WritingRangeToBody")]
        public static partial void WritingRangeToBody(ILogger logger);
    }
}