Skip to content

Commit

Permalink
Reduce StaticFileContext allocation/struct copy (#9815)
Browse files Browse the repository at this point in the history
  • Loading branch information
benaadams authored and Tratcher committed May 3, 2019
1 parent 3c40df4 commit 617bc1a
Show file tree
Hide file tree
Showing 3 changed files with 217 additions and 164 deletions.
234 changes: 136 additions & 98 deletions src/Middleware/StaticFiles/src/StaticFileContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Endpoints;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Headers;
Expand All @@ -22,128 +22,98 @@ namespace Microsoft.AspNetCore.StaticFiles
internal struct StaticFileContext
{
private const int StreamCopyBufferSize = 64 * 1024;

private readonly HttpContext _context;
private readonly StaticFileOptions _options;
private readonly PathString _matchUrl;
private readonly HttpRequest _request;
private readonly HttpResponse _response;
private readonly ILogger _logger;
private readonly IFileProvider _fileProvider;
private readonly IContentTypeProvider _contentTypeProvider;
private string _method;
private bool _isGet;
private bool _isHead;
private PathString _subPath;
private string _contentType;
private readonly string _method;
private readonly string _contentType;

private IFileInfo _fileInfo;
private long _length;
private DateTimeOffset _lastModified;
private EntityTagHeaderValue _etag;

private RequestHeaders _requestHeaders;
private ResponseHeaders _responseHeaders;
private RangeItemHeaderValue _range;

private long _length;
private readonly PathString _subPath;
private DateTimeOffset _lastModified;

private PreconditionState _ifMatchState;
private PreconditionState _ifNoneMatchState;
private PreconditionState _ifModifiedSinceState;
private PreconditionState _ifUnmodifiedSinceState;

private RangeItemHeaderValue _range;
private bool _isRangeRequest;
private RequestType _requestType;

public StaticFileContext(HttpContext context, StaticFileOptions options, PathString matchUrl, ILogger logger, IFileProvider fileProvider, IContentTypeProvider contentTypeProvider)
public StaticFileContext(HttpContext context, StaticFileOptions options, ILogger logger, IFileProvider fileProvider, string contentType, PathString subPath)
{
_context = context;
_options = options;
_matchUrl = matchUrl;
_request = context.Request;
_response = context.Response;
_logger = logger;
_requestHeaders = _request.GetTypedHeaders();
_responseHeaders = _response.GetTypedHeaders();
_fileProvider = fileProvider;
_contentTypeProvider = contentTypeProvider;

_method = null;
_isGet = false;
_isHead = false;
_subPath = PathString.Empty;
_contentType = null;
_method = _request.Method;
_contentType = contentType;
_fileInfo = null;
_etag = null;
_requestHeaders = null;
_responseHeaders = null;
_range = null;

_length = 0;
_subPath = subPath;
_lastModified = new DateTimeOffset();
_etag = null;
_ifMatchState = PreconditionState.Unspecified;
_ifNoneMatchState = PreconditionState.Unspecified;
_ifModifiedSinceState = PreconditionState.Unspecified;
_ifUnmodifiedSinceState = PreconditionState.Unspecified;
_range = null;
_isRangeRequest = false;
}

internal enum PreconditionState
{
Unspecified,
NotModified,
ShouldProcess,
PreconditionFailed
}

public bool IsHeadMethod
{
get { return _isHead; }
}

public bool IsRangeRequest
{
get { return _isRangeRequest; }
}

public string SubPath
{
get { return _subPath.Value; }
if (HttpMethods.IsGet(_method))
{
_requestType = RequestType.IsGet;
}
else if (HttpMethods.IsHead(_method))
{
_requestType = RequestType.IsHead;
}
else
{
_requestType = RequestType.Unspecified;
}
}

public string PhysicalPath
{
get { return _fileInfo?.PhysicalPath; }
}
private RequestHeaders RequestHeaders => (_requestHeaders ??= _request.GetTypedHeaders());

public bool ValidateNoEndpoint()
{
// Return true because we only want to run if there is no endpoint.
return _context.GetEndpoint() == null;
}
private ResponseHeaders ResponseHeaders => (_responseHeaders ??= _response.GetTypedHeaders());

public bool ValidateMethod()
{
_method = _request.Method;
_isGet = HttpMethods.IsGet(_method);
_isHead = HttpMethods.IsHead(_method);
return _isGet || _isHead;
}
public bool IsHeadMethod => _requestType.HasFlag(RequestType.IsHead);

// Check if the URL matches any expected paths
public bool ValidatePath()
{
return Helpers.TryMatchPath(_context, _matchUrl, forDirectory: false, subpath: out _subPath);
}
public bool IsGetMethod => _requestType.HasFlag(RequestType.IsGet);

public bool LookupContentType()
public bool IsRangeRequest
{
if (_contentTypeProvider.TryGetContentType(_subPath.Value, out _contentType))
get => _requestType.HasFlag(RequestType.IsRange);
private set
{
return true;
if (value)
{
_requestType |= RequestType.IsRange;
}
else
{
_requestType &= ~RequestType.IsRange;
}
}
}

if (_options.ServeUnknownFileTypes)
{
_contentType = _options.DefaultContentType;
return true;
}
public string SubPath => _subPath.Value;

return false;
}
public string PhysicalPath => _fileInfo?.PhysicalPath;

public bool LookupFileInfo()
{
Expand Down Expand Up @@ -175,8 +145,10 @@ public void ComprehendRequestHeaders()

private void ComputeIfMatch()
{
var requestHeaders = RequestHeaders;

// 14.24 If-Match
var ifMatch = _requestHeaders.IfMatch;
var ifMatch = requestHeaders.IfMatch;
if (ifMatch != null && ifMatch.Any())
{
_ifMatchState = PreconditionState.PreconditionFailed;
Expand All @@ -191,7 +163,7 @@ private void ComputeIfMatch()
}

// 14.26 If-None-Match
var ifNoneMatch = _requestHeaders.IfNoneMatch;
var ifNoneMatch = requestHeaders.IfNoneMatch;
if (ifNoneMatch != null && ifNoneMatch.Any())
{
_ifNoneMatchState = PreconditionState.ShouldProcess;
Expand All @@ -208,18 +180,19 @@ private void ComputeIfMatch()

private void ComputeIfModifiedSince()
{
var requestHeaders = RequestHeaders;
var now = DateTimeOffset.UtcNow;

// 14.25 If-Modified-Since
var ifModifiedSince = _requestHeaders.IfModifiedSince;
var ifModifiedSince = requestHeaders.IfModifiedSince;
if (ifModifiedSince.HasValue && ifModifiedSince <= now)
{
bool modified = ifModifiedSince < _lastModified;
_ifModifiedSinceState = modified ? PreconditionState.ShouldProcess : PreconditionState.NotModified;
}

// 14.28 If-Unmodified-Since
var ifUnmodifiedSince = _requestHeaders.IfUnmodifiedSince;
var ifUnmodifiedSince = requestHeaders.IfUnmodifiedSince;
if (ifUnmodifiedSince.HasValue && ifUnmodifiedSince <= now)
{
bool unmodified = ifUnmodifiedSince >= _lastModified;
Expand All @@ -230,7 +203,7 @@ private void ComputeIfModifiedSince()
private void ComputeIfRange()
{
// 14.27 If-Range
var ifRangeHeader = _requestHeaders.IfRange;
var ifRangeHeader = RequestHeaders.IfRange;
if (ifRangeHeader != null)
{
// If the validator given in the If-Range header field matches the
Expand All @@ -242,12 +215,12 @@ private void ComputeIfRange()
{
if (_lastModified > ifRangeHeader.LastModified)
{
_isRangeRequest = false;
IsRangeRequest = false;
}
}
else if (_etag != null && ifRangeHeader.EntityTag != null && !ifRangeHeader.EntityTag.Compare(_etag, useStrongComparison: true))
{
_isRangeRequest = false;
IsRangeRequest = false;
}
}
}
Expand All @@ -259,12 +232,15 @@ private void ComputeRange()

// A server MUST ignore a Range header field received with a request method other
// than GET.
if (!_isGet)
if (!IsGetMethod)
{
return;
}

(_isRangeRequest, _range) = RangeHelper.ParseRange(_context, _requestHeaders, _length, _logger);
(var isRangeRequest, var range) = RangeHelper.ParseRange(_context, RequestHeaders, _length, _logger);

_range = range;
IsRangeRequest = isRangeRequest;
}

public void ApplyResponseHeaders(int statusCode)
Expand All @@ -278,9 +254,11 @@ public void ApplyResponseHeaders(int statusCode)
{
_response.ContentType = _contentType;
}
_responseHeaders.LastModified = _lastModified;
_responseHeaders.ETag = _etag;
_responseHeaders.Headers[HeaderNames.AcceptRanges] = "bytes";

var responseHeaders = ResponseHeaders;
responseHeaders.LastModified = _lastModified;
responseHeaders.ETag = _etag;
responseHeaders.Headers[HeaderNames.AcceptRanges] = "bytes";
}
if (statusCode == Constants.Status200Ok)
{
Expand All @@ -294,10 +272,7 @@ public void ApplyResponseHeaders(int statusCode)
}

public PreconditionState GetPreconditionState()
{
return GetMaxPreconditionState(_ifMatchState, _ifNoneMatchState,
_ifModifiedSinceState, _ifUnmodifiedSinceState);
}
=> GetMaxPreconditionState(_ifMatchState, _ifNoneMatchState, _ifModifiedSinceState, _ifUnmodifiedSinceState);

private static PreconditionState GetMaxPreconditionState(params PreconditionState[] states)
{
Expand All @@ -320,6 +295,52 @@ public Task SendStatusAsync(int statusCode)
return Task.CompletedTask;
}

public async Task ServeStaticFile(HttpContext context, RequestDelegate next)
{
ComprehendRequestHeaders();
switch (GetPreconditionState())
{
case PreconditionState.Unspecified:
case PreconditionState.ShouldProcess:
if (IsHeadMethod)
{
await SendStatusAsync(Constants.Status200Ok);
return;
}

try
{
if (IsRangeRequest)
{
await SendRangeAsync();
return;
}

await SendAsync();
_logger.FileServed(SubPath, PhysicalPath);
return;
}
catch (FileNotFoundException)
{
context.Response.Clear();
}
await next(context);
return;
case PreconditionState.NotModified:
_logger.FileNotModified(SubPath);
await SendStatusAsync(Constants.Status304NotModified);
return;
case PreconditionState.PreconditionFailed:
_logger.PreconditionFailed(SubPath);
await SendStatusAsync(Constants.Status412PreconditionFailed);
return;
default:
var exception = new NotImplementedException(GetPreconditionState().ToString());
Debug.Fail(exception.ToString());
throw exception;
}
}

public async Task SendAsync()
{
SetCompressionMode();
Expand Down Expand Up @@ -358,14 +379,14 @@ internal async Task SendRangeAsync()
// 14.16 Content-Range - A server sending a response with status code 416 (Requested range not satisfiable)
// SHOULD include a Content-Range field with a byte-range-resp-spec of "*". The instance-length specifies
// the current length of the selected resource. e.g. */length
_responseHeaders.ContentRange = new ContentRangeHeaderValue(_length);
ResponseHeaders.ContentRange = new ContentRangeHeaderValue(_length);
ApplyResponseHeaders(Constants.Status416RangeNotSatisfiable);

_logger.RangeNotSatisfiable(SubPath);
return;
}

_responseHeaders.ContentRange = ComputeContentRange(_range, out var start, out var length);
ResponseHeaders.ContentRange = ComputeContentRange(_range, out var start, out var length);
_response.ContentLength = length;
SetCompressionMode();
ApplyResponseHeaders(Constants.Status206PartialContent);
Expand Down Expand Up @@ -416,5 +437,22 @@ private void SetCompressionMode()
responseCompressionFeature.Mode = _options.HttpsCompression;
}
}

internal enum PreconditionState : byte
{
Unspecified,
NotModified,
ShouldProcess,
PreconditionFailed
}

[Flags]
private enum RequestType : byte
{
Unspecified = 0b_000,
IsHead = 0b_001,
IsGet = 0b_010,
IsRange = 0b_100,
}
}
}
Loading

0 comments on commit 617bc1a

Please sign in to comment.