parent
ece5e748ad
commit
9aff0a67c1
|
|
@ -1135,8 +1135,10 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a file with the specified <paramref name="fileContents" /> as content
|
||||
/// (<see cref="StatusCodes.Status200OK"/>) and the specified <paramref name="contentType" /> as the Content-Type.
|
||||
/// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>),
|
||||
/// and the specified <paramref name="contentType" /> as the Content-Type.
|
||||
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
|
||||
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
|
||||
/// </summary>
|
||||
/// <param name="fileContents">The file contents.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
|
|
@ -1149,8 +1151,9 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
|
||||
/// <summary>
|
||||
/// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>), the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type and the
|
||||
/// specified <paramref name="fileDownloadName" /> as the suggested file name.
|
||||
/// specified <paramref name="contentType" /> as the Content-Type and the specified <paramref name="fileDownloadName" /> as the suggested file name.
|
||||
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
|
||||
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
|
||||
/// </summary>
|
||||
/// <param name="fileContents">The file contents.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
|
|
@ -1163,8 +1166,54 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>)
|
||||
/// with the specified <paramref name="contentType" /> as the Content-Type.
|
||||
/// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>),
|
||||
/// and the specified <paramref name="contentType" /> as the Content-Type.
|
||||
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
|
||||
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
|
||||
/// </summary>
|
||||
/// <param name="fileContents">The file contents.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
|
||||
/// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
|
||||
/// <returns>The created <see cref="FileContentResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual FileContentResult File(byte[] fileContents, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
|
||||
{
|
||||
return new FileContentResult(fileContents, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>), the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type, and the specified <paramref name="fileDownloadName" /> as the suggested file name.
|
||||
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
|
||||
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
|
||||
/// </summary>
|
||||
/// <param name="fileContents">The file contents.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
/// <param name="fileDownloadName">The suggested file name.</param>
|
||||
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
|
||||
/// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
|
||||
/// <returns>The created <see cref="FileContentResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual FileContentResult File(byte[] fileContents, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
|
||||
{
|
||||
return new FileContentResult(fileContents, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag,
|
||||
FileDownloadName = fileDownloadName,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>), with the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type.
|
||||
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
|
||||
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
|
||||
/// </summary>
|
||||
/// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
|
|
@ -1179,6 +1228,8 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>) with the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type and the
|
||||
/// specified <paramref name="fileDownloadName" /> as the suggested file name.
|
||||
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
|
||||
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
|
||||
/// </summary>
|
||||
/// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
|
|
@ -1190,9 +1241,55 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
return new FileStreamResult(fileStream, contentType) { FileDownloadName = fileDownloadName };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>),
|
||||
/// and the specified <paramref name="contentType" /> as the Content-Type.
|
||||
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
|
||||
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
|
||||
/// </summary>
|
||||
/// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
|
||||
/// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
|
||||
/// <returns>The created <see cref="FileStreamResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual FileStreamResult File(Stream fileStream, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
|
||||
{
|
||||
return new FileStreamResult(fileStream, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>), the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type, and the specified <paramref name="fileDownloadName" /> as the suggested file name.
|
||||
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
|
||||
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
|
||||
/// </summary>
|
||||
/// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
/// <param name="fileDownloadName">The suggested file name.</param>
|
||||
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
|
||||
/// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
|
||||
/// <returns>The created <see cref="FileStreamResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
|
||||
{
|
||||
return new FileStreamResult(fileStream, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag,
|
||||
FileDownloadName = fileDownloadName,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type.
|
||||
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
|
||||
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
|
||||
/// </summary>
|
||||
/// <param name="virtualPath">The virtual path of the file to be returned.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
|
|
@ -1207,6 +1304,8 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type and the
|
||||
/// specified <paramref name="fileDownloadName" /> as the suggested file name.
|
||||
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
|
||||
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
|
||||
/// </summary>
|
||||
/// <param name="virtualPath">The virtual path of the file to be returned.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
|
|
@ -1218,9 +1317,55 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
return new VirtualFileResult(virtualPath, contentType) { FileDownloadName = fileDownloadName };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>), and the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type.
|
||||
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
|
||||
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
|
||||
/// </summary>
|
||||
/// <param name="virtualPath">The virtual path of the file to be returned.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
|
||||
/// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
|
||||
/// <returns>The created <see cref="VirtualFileResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual VirtualFileResult File(string virtualPath, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
|
||||
{
|
||||
return new VirtualFileResult(virtualPath, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>), the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type, and the specified <paramref name="fileDownloadName" /> as the suggested file name.
|
||||
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
|
||||
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
|
||||
/// </summary>
|
||||
/// <param name="virtualPath">The virtual path of the file to be returned.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
/// <param name="fileDownloadName">The suggested file name.</param>
|
||||
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
|
||||
/// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
|
||||
/// <returns>The created <see cref="VirtualFileResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual VirtualFileResult File(string virtualPath, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
|
||||
{
|
||||
return new VirtualFileResult(virtualPath, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag,
|
||||
FileDownloadName = fileDownloadName,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type.
|
||||
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
|
||||
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
|
||||
/// </summary>
|
||||
/// <param name="physicalPath">The physical path of the file to be returned.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
|
|
@ -1235,6 +1380,8 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type and the
|
||||
/// specified <paramref name="fileDownloadName" /> as the suggested file name.
|
||||
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
|
||||
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
|
||||
/// </summary>
|
||||
/// <param name="physicalPath">The physical path of the file to be returned.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
|
|
@ -1249,6 +1396,50 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
return new PhysicalFileResult(physicalPath, contentType) { FileDownloadName = fileDownloadName };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>), and
|
||||
/// the specified <paramref name="contentType" /> as the Content-Type.
|
||||
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
|
||||
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
|
||||
/// </summary>
|
||||
/// <param name="physicalPath">The physical path of the file to be returned.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
|
||||
/// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
|
||||
/// <returns>The created <see cref="PhysicalFileResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual PhysicalFileResult PhysicalFile(string physicalPath, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
|
||||
{
|
||||
return new PhysicalFileResult(physicalPath, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>), the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type, and the specified <paramref name="fileDownloadName" /> as the suggested file name.
|
||||
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
|
||||
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
|
||||
/// </summary>
|
||||
/// <param name="physicalPath">The physical path of the file to be returned.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
/// <param name="fileDownloadName">The suggested file name.</param>
|
||||
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
|
||||
/// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
|
||||
/// <returns>The created <see cref="PhysicalFileResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual PhysicalFileResult PhysicalFile(string physicalPath, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
|
||||
{
|
||||
return new PhysicalFileResult(physicalPath, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag,
|
||||
FileDownloadName = fileDownloadName,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="UnauthorizedResult"/> that produces an <see cref="StatusCodes.Status401Unauthorized"/> response.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
|
|
@ -43,5 +42,15 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
get { return _fileDownloadName ?? string.Empty; }
|
||||
set { _fileDownloadName = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last modified information associated with the <see cref="FileResult"/>.
|
||||
/// </summary>
|
||||
public DateTimeOffset? LastModified { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the etag associated with the <see cref="FileResult"/>.
|
||||
/// </summary>
|
||||
public EntityTagHeaderValue EntityTag { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,10 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||
{
|
||||
|
|
@ -26,11 +28,22 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
SetHeadersAndLog(context, result);
|
||||
return WriteFileAsync(context, result);
|
||||
var (range, rangeLength, serveBody) = SetHeadersAndLog(
|
||||
context,
|
||||
result,
|
||||
result.FileContents.Length,
|
||||
result.LastModified,
|
||||
result.EntityTag);
|
||||
|
||||
if (!serveBody)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return WriteFileAsync(context, result, range, rangeLength);
|
||||
}
|
||||
|
||||
protected virtual Task WriteFileAsync(ActionContext context, FileContentResult result)
|
||||
protected virtual Task WriteFileAsync(ActionContext context, FileContentResult result, RangeItemHeaderValue range, long rangeLength)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
|
|
@ -42,9 +55,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
if (range != null && rangeLength == 0)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return response.Body.WriteAsync(result.FileContents, offset: 0, count: result.FileContents.Length);
|
||||
var response = context.HttpContext.Response;
|
||||
var outputStream = response.Body;
|
||||
var fileContentsStream = new MemoryStream(result.FileContents);
|
||||
return WriteFileAsync(context.HttpContext, fileContentsStream, range, rangeLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Http.Headers;
|
||||
using Microsoft.AspNetCore.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
|
|
@ -9,20 +17,38 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
{
|
||||
public class FileResultExecutorBase
|
||||
{
|
||||
private const string AcceptRangeHeaderValue = "bytes";
|
||||
|
||||
// default buffer size as defined in BufferedStream type
|
||||
protected const int BufferSize = 0x1000;
|
||||
|
||||
public FileResultExecutorBase(ILogger logger)
|
||||
{
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
internal enum PreconditionState
|
||||
{
|
||||
Unspecified,
|
||||
NotModified,
|
||||
ShouldProcess,
|
||||
PreconditionFailed,
|
||||
IgnoreRangeRequest
|
||||
}
|
||||
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
protected virtual void SetHeadersAndLog(ActionContext context, FileResult result)
|
||||
protected virtual (RangeItemHeaderValue range, long rangeLength, bool serveBody) SetHeadersAndLog(
|
||||
ActionContext context,
|
||||
FileResult result, long?
|
||||
fileLength, DateTimeOffset?
|
||||
lastModified = null,
|
||||
EntityTagHeaderValue etag = null)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(result));
|
||||
|
|
@ -31,9 +57,56 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
SetContentType(context, result);
|
||||
SetContentDispositionHeader(context, result);
|
||||
Logger.FileResultExecuting(result.FileDownloadName);
|
||||
if (fileLength.HasValue)
|
||||
{
|
||||
SetAcceptRangeHeader(context);
|
||||
}
|
||||
|
||||
var request = context.HttpContext.Request;
|
||||
var httpRequestHeaders = request.GetTypedHeaders();
|
||||
var response = context.HttpContext.Response;
|
||||
var httpResponseHeaders = response.GetTypedHeaders();
|
||||
if (lastModified.HasValue)
|
||||
{
|
||||
httpResponseHeaders.LastModified = lastModified;
|
||||
}
|
||||
if (etag != null)
|
||||
{
|
||||
httpResponseHeaders.ETag = etag;
|
||||
}
|
||||
|
||||
var serveBody = !HttpMethods.IsHead(request.Method);
|
||||
if (HttpMethods.IsHead(request.Method) || HttpMethods.IsGet(request.Method))
|
||||
{
|
||||
var preconditionState = GetPreconditionState(context, httpRequestHeaders, lastModified, etag);
|
||||
if (request.Headers.ContainsKey(HeaderNames.Range) &&
|
||||
(preconditionState == PreconditionState.Unspecified ||
|
||||
preconditionState == PreconditionState.ShouldProcess))
|
||||
{
|
||||
return SetRangeHeaders(context, httpRequestHeaders, fileLength, lastModified, etag);
|
||||
}
|
||||
if (preconditionState == PreconditionState.NotModified)
|
||||
{
|
||||
serveBody = false;
|
||||
response.StatusCode = StatusCodes.Status304NotModified;
|
||||
}
|
||||
else if (preconditionState == PreconditionState.PreconditionFailed)
|
||||
{
|
||||
serveBody = false;
|
||||
response.StatusCode = StatusCodes.Status412PreconditionFailed;
|
||||
}
|
||||
}
|
||||
|
||||
return (range: null, rangeLength: 0, serveBody: serveBody);
|
||||
}
|
||||
|
||||
private void SetContentDispositionHeader(ActionContext context, FileResult result)
|
||||
private static void SetContentType(ActionContext context, FileResult result)
|
||||
{
|
||||
var response = context.HttpContext.Response;
|
||||
response.ContentType = result.ContentType;
|
||||
}
|
||||
|
||||
private static void SetContentDispositionHeader(ActionContext context, FileResult result)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(result.FileDownloadName))
|
||||
{
|
||||
|
|
@ -48,10 +121,199 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
}
|
||||
}
|
||||
|
||||
private void SetContentType(ActionContext context, FileResult result)
|
||||
private static void SetAcceptRangeHeader(ActionContext context)
|
||||
{
|
||||
var response = context.HttpContext.Response;
|
||||
response.ContentType = result.ContentType;
|
||||
response.Headers[HeaderNames.AcceptRanges] = AcceptRangeHeaderValue;
|
||||
}
|
||||
|
||||
private static PreconditionState GetEtagMatchState(
|
||||
IList<EntityTagHeaderValue> etagHeader,
|
||||
EntityTagHeaderValue etag,
|
||||
PreconditionState matchFoundState,
|
||||
PreconditionState matchNotFoundState)
|
||||
{
|
||||
if (etagHeader != null && etagHeader.Any())
|
||||
{
|
||||
var state = matchNotFoundState;
|
||||
foreach (var entityTag in etagHeader)
|
||||
{
|
||||
if (entityTag.Equals(EntityTagHeaderValue.Any) || entityTag.Compare(etag, useStrongComparison: true))
|
||||
{
|
||||
state = matchFoundState;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
return PreconditionState.Unspecified;
|
||||
}
|
||||
|
||||
// Internal for testing
|
||||
internal static PreconditionState GetPreconditionState(
|
||||
ActionContext context,
|
||||
RequestHeaders httpRequestHeaders,
|
||||
DateTimeOffset? lastModified = null,
|
||||
EntityTagHeaderValue etag = null)
|
||||
{
|
||||
var ifMatchState = PreconditionState.Unspecified;
|
||||
var ifNoneMatchState = PreconditionState.Unspecified;
|
||||
var ifModifiedSinceState = PreconditionState.Unspecified;
|
||||
var ifUnmodifiedSinceState = PreconditionState.Unspecified;
|
||||
var ifRangeState = PreconditionState.Unspecified;
|
||||
|
||||
// 14.24 If-Match
|
||||
var ifMatch = httpRequestHeaders.IfMatch;
|
||||
if (etag != null)
|
||||
{
|
||||
ifMatchState = GetEtagMatchState(
|
||||
etagHeader: ifMatch,
|
||||
etag: etag,
|
||||
matchFoundState: PreconditionState.ShouldProcess,
|
||||
matchNotFoundState: PreconditionState.PreconditionFailed);
|
||||
}
|
||||
|
||||
// 14.26 If-None-Match
|
||||
var ifNoneMatch = httpRequestHeaders.IfNoneMatch;
|
||||
if (etag != null)
|
||||
{
|
||||
ifNoneMatchState = GetEtagMatchState(
|
||||
etagHeader: ifNoneMatch,
|
||||
etag: etag,
|
||||
matchFoundState: PreconditionState.NotModified,
|
||||
matchNotFoundState: PreconditionState.ShouldProcess);
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
// 14.25 If-Modified-Since
|
||||
var ifModifiedSince = httpRequestHeaders.IfModifiedSince;
|
||||
if (lastModified.HasValue && ifModifiedSince.HasValue && ifModifiedSince <= now)
|
||||
{
|
||||
var modified = ifModifiedSince < lastModified;
|
||||
ifModifiedSinceState = modified ? PreconditionState.ShouldProcess : PreconditionState.NotModified;
|
||||
}
|
||||
|
||||
// 14.28 If-Unmodified-Since
|
||||
var ifUnmodifiedSince = httpRequestHeaders.IfUnmodifiedSince;
|
||||
if (lastModified.HasValue && ifUnmodifiedSince.HasValue && ifUnmodifiedSince <= now)
|
||||
{
|
||||
var unmodified = ifUnmodifiedSince >= lastModified;
|
||||
ifUnmodifiedSinceState = unmodified ? PreconditionState.ShouldProcess : PreconditionState.PreconditionFailed;
|
||||
}
|
||||
|
||||
var ifRange = httpRequestHeaders.IfRange;
|
||||
if (ifRange != null)
|
||||
{
|
||||
// If the validator given in the If-Range header field matches the
|
||||
// current validator for the selected representation of the target
|
||||
// resource, then the server SHOULD process the Range header field as
|
||||
// requested. If the validator does not match, the server MUST ignore
|
||||
// the Range header field.
|
||||
if (ifRange.LastModified.HasValue)
|
||||
{
|
||||
if (lastModified.HasValue && lastModified > ifRange.LastModified)
|
||||
{
|
||||
ifRangeState = PreconditionState.IgnoreRangeRequest;
|
||||
}
|
||||
}
|
||||
else if (etag != null && ifRange.EntityTag != null && !ifRange.EntityTag.Compare(etag, useStrongComparison: true))
|
||||
{
|
||||
ifRangeState = PreconditionState.IgnoreRangeRequest;
|
||||
}
|
||||
}
|
||||
|
||||
var state = GetMaxPreconditionState(ifMatchState, ifNoneMatchState, ifModifiedSinceState, ifUnmodifiedSinceState, ifRangeState);
|
||||
return state;
|
||||
}
|
||||
|
||||
private static PreconditionState GetMaxPreconditionState(params PreconditionState[] states)
|
||||
{
|
||||
var max = PreconditionState.Unspecified;
|
||||
for (var i = 0; i < states.Length; i++)
|
||||
{
|
||||
if (states[i] > max)
|
||||
{
|
||||
max = states[i];
|
||||
}
|
||||
}
|
||||
|
||||
return max;
|
||||
}
|
||||
|
||||
private static (RangeItemHeaderValue range, long rangeLength, bool serveBody) SetRangeHeaders(
|
||||
ActionContext context,
|
||||
RequestHeaders httpRequestHeaders,
|
||||
long? fileLength,
|
||||
DateTimeOffset? lastModified = null,
|
||||
EntityTagHeaderValue etag = null)
|
||||
{
|
||||
var response = context.HttpContext.Response;
|
||||
var httpResponseHeaders = response.GetTypedHeaders();
|
||||
|
||||
// Checked for presence of Range header explicitly before calling this method.
|
||||
// Range may be null for parsing errors, multiple ranges and when the file length is missing.
|
||||
var range = fileLength.HasValue ? ParseRange(context, httpRequestHeaders, fileLength.Value, lastModified, etag) : null;
|
||||
if (range == null)
|
||||
{
|
||||
// 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
|
||||
response.StatusCode = StatusCodes.Status416RangeNotSatisfiable;
|
||||
if (fileLength.HasValue)
|
||||
{
|
||||
httpResponseHeaders.ContentRange = new ContentRangeHeaderValue(fileLength.Value);
|
||||
}
|
||||
|
||||
return (range: null, rangeLength: 0, serveBody: false);
|
||||
}
|
||||
|
||||
httpResponseHeaders.ContentRange = new ContentRangeHeaderValue(
|
||||
range.From.Value,
|
||||
range.To.Value,
|
||||
fileLength.Value);
|
||||
|
||||
response.StatusCode = StatusCodes.Status206PartialContent;
|
||||
var rangeLength = SetContentLength(context, range);
|
||||
return (range, rangeLength, serveBody: true);
|
||||
}
|
||||
|
||||
private static long SetContentLength(ActionContext context, RangeItemHeaderValue range)
|
||||
{
|
||||
var start = range.From.Value;
|
||||
var end = range.To.Value;
|
||||
var length = end - start + 1;
|
||||
var response = context.HttpContext.Response;
|
||||
response.ContentLength = length;
|
||||
return length;
|
||||
}
|
||||
|
||||
private static RangeItemHeaderValue ParseRange(
|
||||
ActionContext context,
|
||||
RequestHeaders httpRequestHeaders,
|
||||
long fileLength,
|
||||
DateTimeOffset? lastModified = null,
|
||||
EntityTagHeaderValue etag = null)
|
||||
{
|
||||
var httpContext = context.HttpContext;
|
||||
var response = httpContext.Response;
|
||||
|
||||
var range = RangeHelper.ParseRange(httpContext, httpRequestHeaders, lastModified, etag);
|
||||
|
||||
if (range != null)
|
||||
{
|
||||
var normalizedRanges = RangeHelper.NormalizeRanges(range, fileLength);
|
||||
if (normalizedRanges == null || normalizedRanges.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalizedRanges.Single();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static ILogger CreateLogger<T>(ILoggerFactory factory)
|
||||
|
|
@ -63,5 +325,31 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
|
||||
return factory.CreateLogger<T>();
|
||||
}
|
||||
|
||||
protected static async Task WriteFileAsync(HttpContext context, Stream fileStream, RangeItemHeaderValue range, long rangeLength)
|
||||
{
|
||||
var outputStream = context.Response.Body;
|
||||
using (fileStream)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (range == null)
|
||||
{
|
||||
await StreamCopyOperation.CopyToAsync(fileStream, outputStream, count: null, bufferSize: BufferSize, cancel: context.RequestAborted);
|
||||
}
|
||||
else
|
||||
{
|
||||
fileStream.Seek(range.From.Value, SeekOrigin.Begin);
|
||||
await StreamCopyOperation.CopyToAsync(fileStream, outputStream, rangeLength, BufferSize, context.RequestAborted);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Don't throw this exception, it's most likely caused by the client disconnecting.
|
||||
// However, if it was cancelled for any other reason we need to prevent empty responses.
|
||||
context.Abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||
{
|
||||
public class FileStreamResultExecutor : FileResultExecutorBase
|
||||
{
|
||||
// default buffer size as defined in BufferedStream type
|
||||
private const int BufferSize = 0x1000;
|
||||
|
||||
public FileStreamResultExecutor(ILoggerFactory loggerFactory)
|
||||
: base(CreateLogger<VirtualFileResultExecutor>(loggerFactory))
|
||||
{
|
||||
|
|
@ -29,11 +27,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
SetHeadersAndLog(context, result);
|
||||
return WriteFileAsync(context, result);
|
||||
long? fileLength = null;
|
||||
if (result.FileStream.CanSeek)
|
||||
{
|
||||
fileLength = result.FileStream.Length;
|
||||
}
|
||||
|
||||
var (range, rangeLength, serveBody) = SetHeadersAndLog(
|
||||
context,
|
||||
result,
|
||||
fileLength,
|
||||
result.LastModified,
|
||||
result.EntityTag);
|
||||
|
||||
if (!serveBody)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return WriteFileAsync(context, result, range, rangeLength);
|
||||
}
|
||||
|
||||
protected virtual async Task WriteFileAsync(ActionContext context, FileStreamResult result)
|
||||
protected virtual Task WriteFileAsync(ActionContext context, FileStreamResult result, RangeItemHeaderValue range, long rangeLength)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
|
|
@ -45,13 +60,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
if (range != null && rangeLength == 0)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
var outputStream = response.Body;
|
||||
|
||||
using (result.FileStream)
|
||||
{
|
||||
await result.FileStream.CopyToAsync(outputStream, BufferSize);
|
||||
}
|
||||
return WriteFileAsync(context.HttpContext, result.FileStream, range, rangeLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,12 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||
{
|
||||
public class PhysicalFileResultExecutor : FileResultExecutorBase
|
||||
{
|
||||
private const int DefaultBufferSize = 0x1000;
|
||||
|
||||
public PhysicalFileResultExecutor(ILoggerFactory loggerFactory)
|
||||
: base(CreateLogger<PhysicalFileResultExecutor>(loggerFactory))
|
||||
{
|
||||
|
|
@ -32,11 +31,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
SetHeadersAndLog(context, result);
|
||||
return WriteFileAsync(context, result);
|
||||
var fileInfo = GetFileInfo(result.FileName);
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
Resources.FormatFileResult_InvalidPath(result.FileName), result.FileName);
|
||||
}
|
||||
|
||||
var lastModified = result.LastModified ?? fileInfo.LastModified;
|
||||
var (range, rangeLength, serveBody) = SetHeadersAndLog(
|
||||
context,
|
||||
result,
|
||||
fileInfo.Length,
|
||||
lastModified,
|
||||
result.EntityTag);
|
||||
|
||||
if (serveBody)
|
||||
{
|
||||
return WriteFileAsync(context, result, range, rangeLength);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected virtual async Task WriteFileAsync(ActionContext context, PhysicalFileResult result)
|
||||
protected virtual Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue range, long rangeLength)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
|
|
@ -48,8 +66,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
if (range != null && rangeLength == 0)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
if (!Path.IsPathRooted(result.FileName))
|
||||
{
|
||||
throw new NotSupportedException(Resources.FormatFileResult_PathNotRooted(result.FileName));
|
||||
|
|
@ -58,21 +80,23 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
var sendFile = response.HttpContext.Features.Get<IHttpSendFileFeature>();
|
||||
if (sendFile != null)
|
||||
{
|
||||
await sendFile.SendFileAsync(
|
||||
if (range != null)
|
||||
{
|
||||
return sendFile.SendFileAsync(
|
||||
result.FileName,
|
||||
offset: range.From ?? 0L,
|
||||
count: rangeLength,
|
||||
cancellation: default(CancellationToken));
|
||||
}
|
||||
|
||||
return sendFile.SendFileAsync(
|
||||
result.FileName,
|
||||
offset: 0,
|
||||
count: null,
|
||||
cancellation: default(CancellationToken));
|
||||
}
|
||||
else
|
||||
{
|
||||
var fileStream = GetFileStream(result.FileName);
|
||||
|
||||
using (fileStream)
|
||||
{
|
||||
await fileStream.CopyToAsync(response.Body, DefaultBufferSize);
|
||||
}
|
||||
}
|
||||
return WriteFileAsync(context.HttpContext, GetFileStream(result.FileName), range, rangeLength);
|
||||
}
|
||||
|
||||
protected virtual Stream GetFileStream(string path)
|
||||
|
|
@ -87,8 +111,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite,
|
||||
DefaultBufferSize,
|
||||
BufferSize,
|
||||
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||
}
|
||||
|
||||
protected virtual FileMetadata GetFileInfo(string path)
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
return new FileMetadata
|
||||
{
|
||||
Exists = fileInfo.Exists,
|
||||
Length = fileInfo.Length,
|
||||
LastModified = fileInfo.LastWriteTimeUtc,
|
||||
};
|
||||
}
|
||||
|
||||
protected class FileMetadata
|
||||
{
|
||||
public bool Exists { get; set; }
|
||||
|
||||
public long Length { get; set; }
|
||||
|
||||
public DateTimeOffset LastModified { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ using Microsoft.AspNetCore.Http.Features;
|
|||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||
{
|
||||
public class VirtualFileResultExecutor : FileResultExecutorBase
|
||||
{
|
||||
private const int DefaultBufferSize = 0x1000;
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
|
||||
public VirtualFileResultExecutor(ILoggerFactory loggerFactory, IHostingEnvironment hostingEnvironment)
|
||||
|
|
@ -41,11 +41,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
SetHeadersAndLog(context, result);
|
||||
return WriteFileAsync(context, result);
|
||||
var fileInfo = GetFileInformation(result);
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
Resources.FormatFileResult_InvalidPath(result.FileName), result.FileName);
|
||||
}
|
||||
|
||||
var lastModified = result.LastModified ?? fileInfo.LastModified;
|
||||
var (range, rangeLength, serveBody) = SetHeadersAndLog(
|
||||
context,
|
||||
result,
|
||||
fileInfo.Length,
|
||||
lastModified,
|
||||
result.EntityTag);
|
||||
|
||||
if (serveBody)
|
||||
{
|
||||
return WriteFileAsync(context, result, fileInfo, range, rangeLength);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected virtual async Task WriteFileAsync(ActionContext context, VirtualFileResult result)
|
||||
protected virtual Task WriteFileAsync(ActionContext context, VirtualFileResult result, IFileInfo fileInfo, RangeItemHeaderValue range, long rangeLength)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
|
|
@ -57,7 +76,37 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
if (range != null && rangeLength == 0)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
var physicalPath = fileInfo.PhysicalPath;
|
||||
var sendFile = response.HttpContext.Features.Get<IHttpSendFileFeature>();
|
||||
if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
|
||||
{
|
||||
if (range != null)
|
||||
{
|
||||
return sendFile.SendFileAsync(
|
||||
physicalPath,
|
||||
offset: range.From ?? 0L,
|
||||
count: rangeLength,
|
||||
cancellation: default(CancellationToken));
|
||||
}
|
||||
|
||||
return sendFile.SendFileAsync(
|
||||
physicalPath,
|
||||
offset: 0,
|
||||
count: null,
|
||||
cancellation: default(CancellationToken));
|
||||
}
|
||||
|
||||
return WriteFileAsync(context.HttpContext, GetFileStream(fileInfo), range, rangeLength);
|
||||
}
|
||||
|
||||
private IFileInfo GetFileInformation(VirtualFileResult result)
|
||||
{
|
||||
var fileProvider = GetFileProvider(result);
|
||||
|
||||
var normalizedPath = result.FileName;
|
||||
|
|
@ -67,32 +116,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
}
|
||||
|
||||
var fileInfo = fileProvider.GetFileInfo(normalizedPath);
|
||||
if (fileInfo.Exists)
|
||||
{
|
||||
var physicalPath = fileInfo.PhysicalPath;
|
||||
var sendFile = response.HttpContext.Features.Get<IHttpSendFileFeature>();
|
||||
if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
|
||||
{
|
||||
await sendFile.SendFileAsync(
|
||||
physicalPath,
|
||||
offset: 0,
|
||||
count: null,
|
||||
cancellation: default(CancellationToken));
|
||||
}
|
||||
else
|
||||
{
|
||||
var fileStream = GetFileStream(fileInfo);
|
||||
using (fileStream)
|
||||
{
|
||||
await fileStream.CopyToAsync(response.Body, DefaultBufferSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
Resources.FormatFileResult_InvalidPath(result.FileName), result.FileName);
|
||||
}
|
||||
return fileInfo;
|
||||
}
|
||||
|
||||
private IFileProvider GetFileProvider(VirtualFileResult result)
|
||||
|
|
@ -103,7 +127,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
}
|
||||
|
||||
result.FileProvider = _hostingEnvironment.WebRootFileProvider;
|
||||
|
||||
return result.FileProvider;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ Microsoft.AspNetCore.Mvc.RouteAttribute</Description>
|
|||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.RangeHelper.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.ResponseCaching.Abstractions" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Routing" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.ClosedGenericMatcher.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
|
||||
|
|
@ -37,6 +39,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute</Description>
|
|||
<PackageReference Include="Microsoft.Extensions.PropertyActivator.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.Extensions.PropertyHelper.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.Extensions.SecurityHelper.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
|
||||
<PackageReference Include="System.ValueTuple" Version="$(CoreFxVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1427,6 +1427,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
|||
Assert.Equal(string.Empty, result.FileDownloadName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, null)]
|
||||
[InlineData(null, "\"Etag\"")]
|
||||
[InlineData("05/01/2008 +1:00", null)]
|
||||
[InlineData("05/01/2008 +1:00", "\"Etag\"")]
|
||||
public void File_WithContents_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
|
||||
{
|
||||
// Arrange
|
||||
var controller = new TestableController();
|
||||
var fileContents = new byte[0];
|
||||
var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
|
||||
var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
|
||||
|
||||
// Act
|
||||
var result = controller.File(fileContents, "application/pdf", lastModified, entityTag);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Same(fileContents, result.FileContents);
|
||||
Assert.Equal("application/pdf", result.ContentType.ToString());
|
||||
Assert.Equal(string.Empty, result.FileDownloadName);
|
||||
Assert.Equal(lastModified, result.LastModified);
|
||||
Assert.Equal(entityTag, result.EntityTag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void File_WithContentsAndFileDownloadName()
|
||||
{
|
||||
|
|
@ -1444,6 +1469,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
|||
Assert.Equal("someDownloadName", result.FileDownloadName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, null)]
|
||||
[InlineData(null, "\"Etag\"")]
|
||||
[InlineData("05/01/2008 +1:00", null)]
|
||||
[InlineData("05/01/2008 +1:00", "\"Etag\"")]
|
||||
public void File_WithContentsAndFileDownloadName_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
|
||||
{
|
||||
// Arrange
|
||||
var controller = new TestableController();
|
||||
var fileContents = new byte[0];
|
||||
var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
|
||||
var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
|
||||
|
||||
// Act
|
||||
var result = controller.File(fileContents, "application/pdf", "someDownloadName", lastModified, entityTag);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Same(fileContents, result.FileContents);
|
||||
Assert.Equal("application/pdf", result.ContentType.ToString());
|
||||
Assert.Equal("someDownloadName", result.FileDownloadName);
|
||||
Assert.Equal(lastModified, result.LastModified);
|
||||
Assert.Equal(entityTag, result.EntityTag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void File_WithPath()
|
||||
{
|
||||
|
|
@ -1461,6 +1511,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
|||
Assert.Equal(string.Empty, result.FileDownloadName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, null)]
|
||||
[InlineData(null, "\"Etag\"")]
|
||||
[InlineData("05/01/2008 +1:00", null)]
|
||||
[InlineData("05/01/2008 +1:00", "\"Etag\"")]
|
||||
public void File_WithPath_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
|
||||
{
|
||||
// Arrange
|
||||
var controller = new TestableController();
|
||||
var path = Path.GetFullPath("somepath");
|
||||
var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
|
||||
var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
|
||||
|
||||
// Act
|
||||
var result = controller.File(path, "application/pdf", lastModified, entityTag);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(path, result.FileName);
|
||||
Assert.Equal("application/pdf", result.ContentType.ToString());
|
||||
Assert.Equal(string.Empty, result.FileDownloadName);
|
||||
Assert.Equal(lastModified, result.LastModified);
|
||||
Assert.Equal(entityTag, result.EntityTag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void File_WithPathAndFileDownloadName()
|
||||
{
|
||||
|
|
@ -1478,6 +1553,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
|||
Assert.Equal("someDownloadName", result.FileDownloadName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, null)]
|
||||
[InlineData(null, "\"Etag\"")]
|
||||
[InlineData("05/01/2008 +1:00", null)]
|
||||
[InlineData("05/01/2008 +1:00", "\"Etag\"")]
|
||||
public void File_WithPathAndFileDownloadName_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
|
||||
{
|
||||
// Arrange
|
||||
var controller = new TestableController();
|
||||
var path = Path.GetFullPath("somepath");
|
||||
var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
|
||||
var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
|
||||
|
||||
// Act
|
||||
var result = controller.File(path, "application/pdf", "someDownloadName", lastModified, entityTag);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(path, result.FileName);
|
||||
Assert.Equal("application/pdf", result.ContentType.ToString());
|
||||
Assert.Equal("someDownloadName", result.FileDownloadName);
|
||||
Assert.Equal(lastModified, result.LastModified);
|
||||
Assert.Equal(entityTag, result.EntityTag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void File_WithStream()
|
||||
{
|
||||
|
|
@ -1500,6 +1600,36 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
|||
Assert.Equal(string.Empty, result.FileDownloadName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, null)]
|
||||
[InlineData(null, "\"Etag\"")]
|
||||
[InlineData("05/01/2008 +1:00", null)]
|
||||
[InlineData("05/01/2008 +1:00", "\"Etag\"")]
|
||||
public void File_WithStream_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
|
||||
{
|
||||
// Arrange
|
||||
var mockHttpContext = new Mock<HttpContext>();
|
||||
mockHttpContext.Setup(x => x.Response.RegisterForDispose(It.IsAny<IDisposable>()));
|
||||
|
||||
var controller = new TestableController();
|
||||
controller.ControllerContext.HttpContext = mockHttpContext.Object;
|
||||
|
||||
var fileStream = Stream.Null;
|
||||
var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
|
||||
var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
|
||||
|
||||
// Act
|
||||
var result = controller.File(fileStream, "application/pdf", lastModified, entityTag);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Same(fileStream, result.FileStream);
|
||||
Assert.Equal("application/pdf", result.ContentType.ToString());
|
||||
Assert.Equal(string.Empty, result.FileDownloadName);
|
||||
Assert.Equal(lastModified, result.LastModified);
|
||||
Assert.Equal(entityTag, result.EntityTag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void File_WithStreamAndFileDownloadName()
|
||||
{
|
||||
|
|
@ -1521,6 +1651,35 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
|||
Assert.Equal("someDownloadName", result.FileDownloadName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, null)]
|
||||
[InlineData(null, "\"Etag\"")]
|
||||
[InlineData("05/01/2008 +1:00", null)]
|
||||
[InlineData("05/01/2008 +1:00", "\"Etag\"")]
|
||||
public void File_WithStreamAndFileDownloadName_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
|
||||
{
|
||||
// Arrange
|
||||
var mockHttpContext = new Mock<HttpContext>();
|
||||
|
||||
var controller = new TestableController();
|
||||
controller.ControllerContext.HttpContext = mockHttpContext.Object;
|
||||
|
||||
var fileStream = Stream.Null;
|
||||
var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
|
||||
var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
|
||||
|
||||
// Act
|
||||
var result = controller.File(fileStream, "application/pdf", "someDownloadName", lastModified, entityTag);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Same(fileStream, result.FileStream);
|
||||
Assert.Equal("application/pdf", result.ContentType.ToString());
|
||||
Assert.Equal("someDownloadName", result.FileDownloadName);
|
||||
Assert.Equal(lastModified, result.LastModified);
|
||||
Assert.Equal(entityTag, result.EntityTag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HttpUnauthorized_SetsStatusCode()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
|
|
@ -11,6 +13,7 @@ using Microsoft.AspNetCore.Routing;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
|
|
@ -46,6 +49,29 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsLastModifiedAndEtag()
|
||||
{
|
||||
// Arrange
|
||||
var fileContents = new byte[0];
|
||||
var contentType = "text/plain";
|
||||
var expectedMediaType = contentType;
|
||||
var lastModified = new DateTimeOffset();
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
|
||||
// Act
|
||||
var result = new FileContentResult(fileContents, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal(lastModified, result.LastModified);
|
||||
Assert.Equal(entityTag, result.EntityTag);
|
||||
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_CopiesBuffer_ToOutputStream()
|
||||
{
|
||||
|
|
@ -68,6 +94,268 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
Assert.Equal(buffer, outStream.ToArray());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 4, "Hello", 5)]
|
||||
[InlineData(6, 10, "World", 5)]
|
||||
[InlineData(null, 5, "World", 5)]
|
||||
[InlineData(6, null, "World", 5)]
|
||||
public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
|
||||
{
|
||||
// Arrange
|
||||
var contentType = "text/plain";
|
||||
var lastModified = new DateTimeOffset();
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var byteArray = Encoding.ASCII.GetBytes("Hello World");
|
||||
|
||||
var result = new FileContentResult(byteArray, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag
|
||||
};
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.Range = new RangeHeaderValue(start, end);
|
||||
requestHeaders.IfMatch = new[]
|
||||
{
|
||||
new EntityTagHeaderValue("\"Etag\""),
|
||||
};
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
start = start ?? 11 - end;
|
||||
end = start + contentLength - 1;
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, byteArray.Length);
|
||||
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
|
||||
Assert.Equal(contentLength, httpResponse.ContentLength);
|
||||
Assert.Equal(expectedString, body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
|
||||
{
|
||||
// Arrange
|
||||
var contentType = "text/plain";
|
||||
var lastModified = DateTimeOffset.MinValue;
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var byteArray = Encoding.ASCII.GetBytes("Hello World");
|
||||
|
||||
var result = new FileContentResult(byteArray, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag
|
||||
};
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfMatch = new[]
|
||||
{
|
||||
new EntityTagHeaderValue("\"Etag\""),
|
||||
};
|
||||
requestHeaders.Range = new RangeHeaderValue(0, 4);
|
||||
requestHeaders.IfRange = new RangeConditionHeaderValue(DateTimeOffset.MinValue);
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
var contentRange = new ContentRangeHeaderValue(0, 4, byteArray.Length);
|
||||
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
|
||||
Assert.Equal(5, httpResponse.ContentLength);
|
||||
Assert.Equal("Hello", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
|
||||
{
|
||||
// Arrange
|
||||
var contentType = "text/plain";
|
||||
var lastModified = DateTimeOffset.MinValue.AddDays(1);
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var byteArray = Encoding.ASCII.GetBytes("Hello World");
|
||||
|
||||
var result = new FileContentResult(byteArray, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag
|
||||
};
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfMatch = new[]
|
||||
{
|
||||
new EntityTagHeaderValue("\"Etag\""),
|
||||
};
|
||||
requestHeaders.Range = new RangeHeaderValue(0, 4);
|
||||
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
|
||||
Assert.Equal("Hello World", body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0-5")]
|
||||
[InlineData("bytes = 11-0")]
|
||||
[InlineData("bytes = 1-4, 5-11")]
|
||||
public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString)
|
||||
{
|
||||
// Arrange
|
||||
var contentType = "text/plain";
|
||||
var lastModified = new DateTimeOffset();
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var byteArray = Encoding.ASCII.GetBytes("Hello World");
|
||||
|
||||
var result = new FileContentResult(byteArray, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag
|
||||
};
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
httpContext.Request.Headers[HeaderNames.Range] = rangeString;
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
var contentRange = new ContentRangeHeaderValue(byteArray.Length);
|
||||
Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
|
||||
{
|
||||
// Arrange
|
||||
var contentType = "text/plain";
|
||||
var lastModified = new DateTimeOffset();
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var byteArray = Encoding.ASCII.GetBytes("Hello World");
|
||||
|
||||
var result = new FileContentResult(byteArray, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag
|
||||
};
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfMatch = new[]
|
||||
{
|
||||
new EntityTagHeaderValue("\"NotEtag\""),
|
||||
};
|
||||
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Null(httpResponse.ContentLength);
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_RangeRequested_NotModified()
|
||||
{
|
||||
// Arrange
|
||||
var contentType = "text/plain";
|
||||
var lastModified = new DateTimeOffset();
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var byteArray = Encoding.ASCII.GetBytes("Hello World");
|
||||
|
||||
var result = new FileContentResult(byteArray, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag
|
||||
};
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfNoneMatch = new[]
|
||||
{
|
||||
new EntityTagHeaderValue("\"Etag\""),
|
||||
};
|
||||
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Null(httpResponse.ContentLength);
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -250,6 +250,160 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
Assert.Equal(expectedOutput, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetsAcceptRangeHeader()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = GetHttpContext();
|
||||
var actionContext = CreateActionContext(httpContext);
|
||||
|
||||
var result = new EmptyFileResult("application/my-type");
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("bytes", httpContext.Response.Headers[HeaderNames.AcceptRanges]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("\"Etag\"", "\"NotEtag\"", "\"Etag\"")]
|
||||
[InlineData("\"Etag\"", null, null)]
|
||||
[InlineData(null, "\"NotEtag\"", "\"Etag\"")]
|
||||
public void GetPreconditionState_ShouldProcess(string ifMatch, string ifNoneMatch, string ifRange)
|
||||
{
|
||||
// Arrange
|
||||
var actionContext = new ActionContext();
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
var httpRequestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
var lastModified = DateTimeOffset.MinValue;
|
||||
lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0));
|
||||
var etag = new EntityTagHeaderValue("\"Etag\"");
|
||||
httpRequestHeaders.IfMatch = ifMatch == null ? null : new[]
|
||||
{
|
||||
new EntityTagHeaderValue(ifMatch),
|
||||
};
|
||||
|
||||
httpRequestHeaders.IfNoneMatch = ifNoneMatch == null ? null : new[]
|
||||
{
|
||||
new EntityTagHeaderValue(ifNoneMatch),
|
||||
};
|
||||
httpRequestHeaders.IfRange = ifRange == null ? null : new RangeConditionHeaderValue(ifRange);
|
||||
httpRequestHeaders.IfUnmodifiedSince = lastModified;
|
||||
httpRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1);
|
||||
actionContext.HttpContext = httpContext;
|
||||
|
||||
// Act
|
||||
var state = FileResultExecutorBase.GetPreconditionState(
|
||||
actionContext,
|
||||
httpRequestHeaders,
|
||||
lastModified,
|
||||
etag);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(FileResultExecutorBase.PreconditionState.ShouldProcess, state);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("\"NotEtag\"", null)]
|
||||
[InlineData("\"Etag\"", "\"Etag\"")]
|
||||
[InlineData(null, null)]
|
||||
public void GetPreconditionState_ShouldNotProcess_PreconditionFailed(string ifMatch, string ifNoneMatch)
|
||||
{
|
||||
// Arrange
|
||||
var actionContext = new ActionContext();
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Method = HttpMethods.Delete;
|
||||
var httpRequestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
var lastModified = DateTimeOffset.MinValue.AddDays(1);
|
||||
var etag = new EntityTagHeaderValue("\"Etag\"");
|
||||
httpRequestHeaders.IfMatch = ifMatch == null ? null : new[]
|
||||
{
|
||||
new EntityTagHeaderValue(ifMatch),
|
||||
};
|
||||
|
||||
httpRequestHeaders.IfNoneMatch = ifNoneMatch == null ? null : new[]
|
||||
{
|
||||
new EntityTagHeaderValue(ifNoneMatch),
|
||||
};
|
||||
httpRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue;
|
||||
httpRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(2);
|
||||
actionContext.HttpContext = httpContext;
|
||||
|
||||
// Act
|
||||
var state = FileResultExecutorBase.GetPreconditionState(
|
||||
actionContext,
|
||||
httpRequestHeaders,
|
||||
lastModified,
|
||||
etag);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(FileResultExecutorBase.PreconditionState.PreconditionFailed, state);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, "\"Etag\"")]
|
||||
[InlineData(null, null)]
|
||||
public void GetPreconditionState_ShouldNotProcess_NotModified(string ifMatch, string ifNoneMatch)
|
||||
{
|
||||
// Arrange
|
||||
var actionContext = new ActionContext();
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
var httpRequestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
var lastModified = DateTimeOffset.MinValue;
|
||||
lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0));
|
||||
var etag = new EntityTagHeaderValue("\"Etag\"");
|
||||
httpRequestHeaders.IfMatch = ifMatch == null ? null : new[]
|
||||
{
|
||||
new EntityTagHeaderValue(ifMatch),
|
||||
};
|
||||
|
||||
httpRequestHeaders.IfNoneMatch = ifNoneMatch == null ? null : new[]
|
||||
{
|
||||
new EntityTagHeaderValue(ifNoneMatch),
|
||||
};
|
||||
httpRequestHeaders.IfModifiedSince = lastModified;
|
||||
actionContext.HttpContext = httpContext;
|
||||
|
||||
// Act
|
||||
var state = FileResultExecutorBase.GetPreconditionState(
|
||||
actionContext,
|
||||
httpRequestHeaders,
|
||||
lastModified,
|
||||
etag);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(FileResultExecutorBase.PreconditionState.NotModified, state);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPreconditionState_ShouldNotProcess_IgnoreRangeRequest()
|
||||
{
|
||||
// Arrange
|
||||
var actionContext = new ActionContext();
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
var httpRequestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
var lastModified = DateTimeOffset.MinValue;
|
||||
lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0));
|
||||
var etag = new EntityTagHeaderValue("\"Etag\"");
|
||||
httpRequestHeaders.IfRange = new RangeConditionHeaderValue("\"NotEtag\"");
|
||||
httpRequestHeaders.IfModifiedSince = lastModified;
|
||||
actionContext.HttpContext = httpContext;
|
||||
|
||||
// Act
|
||||
var state = FileResultExecutorBase.GetPreconditionState(
|
||||
actionContext,
|
||||
httpRequestHeaders,
|
||||
lastModified,
|
||||
etag);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(FileResultExecutorBase.PreconditionState.IgnoreRangeRequest, state);
|
||||
}
|
||||
|
||||
private static IServiceCollection CreateServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
|
@ -297,13 +451,13 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
private class EmptyFileResultExecutor : FileResultExecutorBase
|
||||
{
|
||||
public EmptyFileResultExecutor(ILoggerFactory loggerFactory)
|
||||
:base(CreateLogger<EmptyFileResultExecutor>(loggerFactory))
|
||||
: base(CreateLogger<EmptyFileResultExecutor>(loggerFactory))
|
||||
{
|
||||
}
|
||||
|
||||
public Task ExecuteAsync(ActionContext context, EmptyFileResult result)
|
||||
{
|
||||
SetHeadersAndLog(context, result);
|
||||
SetHeadersAndLog(context, result, 0L);
|
||||
result.WasWriteFileCalled = true;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
@ -13,6 +15,7 @@ using Microsoft.AspNetCore.Routing;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -49,6 +52,348 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsLastModifiedAndEtag()
|
||||
{
|
||||
// Arrange
|
||||
var stream = Stream.Null;
|
||||
var contentType = "text/plain";
|
||||
var expectedMediaType = contentType;
|
||||
var lastModified = new DateTimeOffset();
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
|
||||
// Act
|
||||
var result = new FileStreamResult(stream, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag,
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal(lastModified, result.LastModified);
|
||||
Assert.Equal(entityTag, result.EntityTag);
|
||||
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 4, "Hello", 5)]
|
||||
[InlineData(6, 10, "World", 5)]
|
||||
[InlineData(null, 5, "World", 5)]
|
||||
[InlineData(6, null, "World", 5)]
|
||||
public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
|
||||
{
|
||||
// Arrange
|
||||
var contentType = "text/plain";
|
||||
var lastModified = new DateTimeOffset();
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var byteArray = Encoding.ASCII.GetBytes("Hello World");
|
||||
var readStream = new MemoryStream(byteArray);
|
||||
readStream.SetLength(11);
|
||||
|
||||
var result = new FileStreamResult(readStream, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag,
|
||||
};
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.Range = new RangeHeaderValue(start, end);
|
||||
requestHeaders.IfMatch = new[]
|
||||
{
|
||||
new EntityTagHeaderValue("\"Etag\""),
|
||||
};
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
start = start ?? 11 - end;
|
||||
end = start + contentLength - 1;
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, byteArray.Length);
|
||||
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
|
||||
Assert.Equal(contentLength, httpResponse.ContentLength);
|
||||
Assert.Equal(expectedString, body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
|
||||
{
|
||||
// Arrange
|
||||
var contentType = "text/plain";
|
||||
var lastModified = DateTimeOffset.MinValue;
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var byteArray = Encoding.ASCII.GetBytes("Hello World");
|
||||
var readStream = new MemoryStream(byteArray);
|
||||
readStream.SetLength(11);
|
||||
|
||||
var result = new FileStreamResult(readStream, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag,
|
||||
};
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfMatch = new[]
|
||||
{
|
||||
new EntityTagHeaderValue("\"Etag\""),
|
||||
};
|
||||
requestHeaders.Range = new RangeHeaderValue(0, 4);
|
||||
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
var contentRange = new ContentRangeHeaderValue(0, 4, byteArray.Length);
|
||||
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
|
||||
Assert.Equal(5, httpResponse.ContentLength);
|
||||
Assert.Equal("Hello", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
|
||||
{
|
||||
// Arrange
|
||||
var contentType = "text/plain";
|
||||
var lastModified = DateTimeOffset.MinValue.AddDays(1);
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var byteArray = Encoding.ASCII.GetBytes("Hello World");
|
||||
var readStream = new MemoryStream(byteArray);
|
||||
readStream.SetLength(11);
|
||||
|
||||
var result = new FileStreamResult(readStream, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag,
|
||||
};
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfMatch = new[]
|
||||
{
|
||||
new EntityTagHeaderValue("\"Etag\""),
|
||||
};
|
||||
requestHeaders.Range = new RangeHeaderValue(0, 4);
|
||||
requestHeaders.IfRange = new RangeConditionHeaderValue(DateTimeOffset.MinValue);
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
|
||||
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
|
||||
Assert.Equal("Hello World", body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0-5")]
|
||||
[InlineData("bytes = 11-0")]
|
||||
[InlineData("bytes = 1-4, 5-11")]
|
||||
public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString)
|
||||
{
|
||||
// Arrange
|
||||
var contentType = "text/plain";
|
||||
var lastModified = new DateTimeOffset();
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var byteArray = Encoding.ASCII.GetBytes("Hello World");
|
||||
var readStream = new MemoryStream(byteArray);
|
||||
|
||||
var result = new FileStreamResult(readStream, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag,
|
||||
};
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
httpContext.Request.Headers[HeaderNames.Range] = rangeString;
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
var contentRange = new ContentRangeHeaderValue(byteArray.Length);
|
||||
Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
|
||||
{
|
||||
// Arrange
|
||||
var contentType = "text/plain";
|
||||
var lastModified = new DateTimeOffset();
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var byteArray = Encoding.ASCII.GetBytes("Hello World");
|
||||
var readStream = new MemoryStream(byteArray);
|
||||
|
||||
var result = new FileStreamResult(readStream, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag,
|
||||
};
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfMatch = new[]
|
||||
{
|
||||
new EntityTagHeaderValue("\"NotEtag\""),
|
||||
};
|
||||
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Null(httpResponse.ContentLength);
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_RangeRequested_NotModified()
|
||||
{
|
||||
// Arrange
|
||||
var contentType = "text/plain";
|
||||
var lastModified = new DateTimeOffset();
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var byteArray = Encoding.ASCII.GetBytes("Hello World");
|
||||
var readStream = new MemoryStream(byteArray);
|
||||
|
||||
var result = new FileStreamResult(readStream, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag,
|
||||
};
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfNoneMatch = new[]
|
||||
{
|
||||
new EntityTagHeaderValue("\"Etag\""),
|
||||
};
|
||||
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Null(httpResponse.ContentLength);
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(null)]
|
||||
public async Task WriteFileAsync_RangeRequested_FileLengthZeroOrNull(long? fileLength)
|
||||
{
|
||||
// Arrange
|
||||
var contentType = "text/plain";
|
||||
var lastModified = new DateTimeOffset();
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var byteArray = Encoding.ASCII.GetBytes("");
|
||||
var readStream = new MemoryStream(byteArray);
|
||||
fileLength = fileLength ?? 0L;
|
||||
readStream.SetLength(fileLength.Value);
|
||||
var result = new FileStreamResult(readStream, contentType)
|
||||
{
|
||||
LastModified = lastModified,
|
||||
EntityTag = entityTag,
|
||||
};
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.Range = new RangeHeaderValue(0, 5);
|
||||
requestHeaders.IfMatch = new[]
|
||||
{
|
||||
new EntityTagHeaderValue("\"Etag\""),
|
||||
};
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
|
||||
var contentRange = new ContentRangeHeaderValue(byteArray.Length);
|
||||
Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_WritesResponse_InChunksOfFourKilobytes()
|
||||
{
|
||||
|
|
@ -153,7 +498,6 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.RequestServices = services.BuildServiceProvider();
|
||||
|
||||
return httpContext;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ using System.IO;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
|
|
@ -16,6 +15,7 @@ using Microsoft.AspNetCore.Routing;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -52,6 +52,198 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 3, "File", 4)]
|
||||
[InlineData(8, 13, "Result", 6)]
|
||||
[InlineData(null, 5, "ts<74>", 5)]
|
||||
[InlineData(8, null, "ResultTestFile contents<74>", 26)]
|
||||
public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
|
||||
{
|
||||
// Arrange
|
||||
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
|
||||
var result = new TestPhysicalFileResult(path, "text/plain");
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
|
||||
requestHeaders.Range = new RangeHeaderValue(start, end);
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
start = start ?? 34 - end;
|
||||
end = start + contentLength - 1;
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, 34);
|
||||
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Equal(contentLength, httpResponse.ContentLength);
|
||||
Assert.Equal(expectedString, body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
|
||||
{
|
||||
// Arrange
|
||||
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
|
||||
var result = new TestPhysicalFileResult(path, "text/plain");
|
||||
var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
|
||||
requestHeaders.Range = new RangeHeaderValue(0, 3);
|
||||
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
var contentRange = new ContentRangeHeaderValue(0, 3, 34);
|
||||
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
|
||||
Assert.Equal(4, httpResponse.ContentLength);
|
||||
Assert.Equal("File", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
|
||||
{
|
||||
// Arrange
|
||||
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
|
||||
var result = new TestPhysicalFileResult(path, "text/plain");
|
||||
var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
|
||||
requestHeaders.Range = new RangeHeaderValue(0, 3);
|
||||
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Equal("FilePathResultTestFile contents<74>", body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0-5")]
|
||||
[InlineData("bytes = 11-0")]
|
||||
[InlineData("bytes = 1-4, 5-11")]
|
||||
public async Task WriteFileAsync_RangeRequested_NotSatisfiable(string rangeString)
|
||||
{
|
||||
// Arrange
|
||||
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
|
||||
var result = new TestPhysicalFileResult(path, "text/plain");
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
|
||||
httpContext.Request.Headers[HeaderNames.Range] = rangeString;
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
var contentRange = new ContentRangeHeaderValue(34);
|
||||
Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
|
||||
{
|
||||
// Arrange
|
||||
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
|
||||
var result = new TestPhysicalFileResult(path, "text/plain");
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue;
|
||||
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Null(httpResponse.ContentLength);
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_RangeRequested_NotModified()
|
||||
{
|
||||
// Arrange
|
||||
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
|
||||
var result = new TestPhysicalFileResult(path, "text/plain");
|
||||
var httpContext = GetHttpContext();
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1);
|
||||
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Null(httpResponse.ContentLength);
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteResultAsync_FallsbackToStreamCopy_IfNoIHttpSendFilePresent()
|
||||
{
|
||||
|
|
@ -94,6 +286,46 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
sendFileMock.Verify();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 3, "File", 4)]
|
||||
[InlineData(8, 13, "Result", 6)]
|
||||
[InlineData(null, 3, "ts¡", 3)]
|
||||
[InlineData(8, null, "ResultTestFile contents¡", 26)]
|
||||
public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(long? start, long? end, string expectedString, long contentLength)
|
||||
{
|
||||
// Arrange
|
||||
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
|
||||
var result = new TestPhysicalFileResult(path, "text/plain");
|
||||
|
||||
var sendFile = new TestSendFileFeature();
|
||||
var httpContext = GetHttpContext();
|
||||
httpContext.Features.Set<IHttpSendFileFeature>(sendFile);
|
||||
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.Range = new RangeHeaderValue(start, end);
|
||||
requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
start = start ?? 34 - end;
|
||||
end = start + contentLength - 1;
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
Assert.Equal(Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")), sendFile.Name);
|
||||
Assert.Equal(start, sendFile.Offset);
|
||||
Assert.Equal(contentLength, sendFile.Length);
|
||||
Assert.Equal(CancellationToken.None, sendFile.Token);
|
||||
var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, 34);
|
||||
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Equal(contentLength, httpResponse.ContentLength);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
|
||||
{
|
||||
|
|
@ -236,6 +468,35 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
return new MemoryStream(Encoding.UTF8.GetBytes("FilePathResultTestFile contents<74>"));
|
||||
}
|
||||
}
|
||||
|
||||
protected override FileMetadata GetFileInfo(string path)
|
||||
{
|
||||
var lastModified = DateTimeOffset.MinValue.AddDays(1);
|
||||
return new FileMetadata
|
||||
{
|
||||
Exists = true,
|
||||
Length = 34,
|
||||
LastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private class TestSendFileFeature : IHttpSendFileFeature
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public long Offset { get; set; }
|
||||
public long? Length { get; set; }
|
||||
public CancellationToken Token { get; set; }
|
||||
|
||||
public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
|
||||
{
|
||||
Name = path;
|
||||
Offset = offset;
|
||||
Length = length;
|
||||
Token = cancellation;
|
||||
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static IServiceCollection CreateServices()
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -51,8 +52,272 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
// Assert
|
||||
Assert.Equal(path, result.FileName);
|
||||
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
|
||||
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 3, "File", 4)]
|
||||
[InlineData(8, 13, "Result", 6)]
|
||||
[InlineData(null, 4, "ts¡", 4)]
|
||||
[InlineData(8, null, "ResultTestFile contents¡", 25)]
|
||||
public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
|
||||
{
|
||||
// Arrange
|
||||
var path = Path.GetFullPath("helllo.txt");
|
||||
var contentType = "text/plain; charset=us-ascii; p1=p1-value";
|
||||
var result = new TestVirtualFileResult(path, contentType);
|
||||
var appEnvironment = new Mock<IHostingEnvironment>();
|
||||
appEnvironment.Setup(app => app.WebRootFileProvider)
|
||||
.Returns(GetFileProvider(path));
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
httpContext.RequestServices = new ServiceCollection()
|
||||
.AddSingleton(appEnvironment.Object)
|
||||
.AddTransient<TestVirtualFileResultExecutor>()
|
||||
.AddTransient<ILoggerFactory, LoggerFactory>()
|
||||
.BuildServiceProvider();
|
||||
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.Range = new RangeHeaderValue(start, end);
|
||||
requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
start = start ?? 33 - end;
|
||||
end = start + contentLength - 1;
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, 33);
|
||||
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Equal(contentLength, httpResponse.ContentLength);
|
||||
Assert.Equal(expectedString, body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
|
||||
{
|
||||
// Arrange
|
||||
var path = Path.GetFullPath("helllo.txt");
|
||||
var contentType = "text/plain; charset=us-ascii; p1=p1-value";
|
||||
var result = new TestVirtualFileResult(path, contentType);
|
||||
var appEnvironment = new Mock<IHostingEnvironment>();
|
||||
appEnvironment.Setup(app => app.WebRootFileProvider)
|
||||
.Returns(GetFileProvider(path));
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
httpContext.RequestServices = new ServiceCollection()
|
||||
.AddSingleton(appEnvironment.Object)
|
||||
.AddTransient<TestVirtualFileResultExecutor>()
|
||||
.AddTransient<ILoggerFactory, LoggerFactory>()
|
||||
.BuildServiceProvider();
|
||||
|
||||
var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
|
||||
requestHeaders.Range = new RangeHeaderValue(0, 3);
|
||||
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
var contentRange = new ContentRangeHeaderValue(0, 3, 33);
|
||||
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
|
||||
Assert.Equal(4, httpResponse.ContentLength);
|
||||
Assert.Equal("File", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
|
||||
{
|
||||
// Arrange
|
||||
var path = Path.GetFullPath("helllo.txt");
|
||||
var contentType = "text/plain; charset=us-ascii; p1=p1-value";
|
||||
var result = new TestVirtualFileResult(path, contentType);
|
||||
var appEnvironment = new Mock<IHostingEnvironment>();
|
||||
appEnvironment.Setup(app => app.WebRootFileProvider)
|
||||
.Returns(GetFileProvider(path));
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
httpContext.RequestServices = new ServiceCollection()
|
||||
.AddSingleton(appEnvironment.Object)
|
||||
.AddTransient<TestVirtualFileResultExecutor>()
|
||||
.AddTransient<ILoggerFactory, LoggerFactory>()
|
||||
.BuildServiceProvider();
|
||||
|
||||
var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
|
||||
requestHeaders.Range = new RangeHeaderValue(0, 3);
|
||||
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
|
||||
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Equal("FilePathResultTestFile contents¡", body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0-5")]
|
||||
[InlineData("bytes = 11-0")]
|
||||
[InlineData("bytes = 1-4, 5-11")]
|
||||
public async Task WriteFileAsync_RangeRequested_NotSatisfiable(string rangeString)
|
||||
{
|
||||
// Arrange
|
||||
var path = Path.GetFullPath("helllo.txt");
|
||||
var contentType = "text/plain; charset=us-ascii; p1=p1-value";
|
||||
var result = new TestVirtualFileResult(path, contentType);
|
||||
var appEnvironment = new Mock<IHostingEnvironment>();
|
||||
appEnvironment.Setup(app => app.WebRootFileProvider)
|
||||
.Returns(GetFileProvider(path));
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
httpContext.RequestServices = new ServiceCollection()
|
||||
.AddSingleton(appEnvironment.Object)
|
||||
.AddTransient<TestVirtualFileResultExecutor>()
|
||||
.AddTransient<ILoggerFactory, LoggerFactory>()
|
||||
.BuildServiceProvider();
|
||||
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
httpContext.Request.Headers[HeaderNames.Range] = rangeString;
|
||||
requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
var contentRange = new ContentRangeHeaderValue(33);
|
||||
Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
|
||||
{
|
||||
// Arrange
|
||||
var path = Path.GetFullPath("helllo.txt");
|
||||
var contentType = "text/plain; charset=us-ascii; p1=p1-value";
|
||||
var result = new TestVirtualFileResult(path, contentType);
|
||||
var appEnvironment = new Mock<IHostingEnvironment>();
|
||||
appEnvironment.Setup(app => app.WebRootFileProvider)
|
||||
.Returns(GetFileProvider(path));
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
httpContext.RequestServices = new ServiceCollection()
|
||||
.AddSingleton(appEnvironment.Object)
|
||||
.AddTransient<TestVirtualFileResultExecutor>()
|
||||
.AddTransient<ILoggerFactory, LoggerFactory>()
|
||||
.BuildServiceProvider();
|
||||
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue;
|
||||
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Null(httpResponse.ContentLength);
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_RangeRequested_NotModified()
|
||||
{
|
||||
// Arrange
|
||||
var path = Path.GetFullPath("helllo.txt");
|
||||
var contentType = "text/plain; charset=us-ascii; p1=p1-value";
|
||||
var result = new TestVirtualFileResult(path, contentType);
|
||||
var appEnvironment = new Mock<IHostingEnvironment>();
|
||||
appEnvironment.Setup(app => app.WebRootFileProvider)
|
||||
.Returns(GetFileProvider(path));
|
||||
|
||||
var httpContext = GetHttpContext();
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
httpContext.RequestServices = new ServiceCollection()
|
||||
.AddSingleton(appEnvironment.Object)
|
||||
.AddTransient<TestVirtualFileResultExecutor>()
|
||||
.AddTransient<ILoggerFactory, LoggerFactory>()
|
||||
.BuildServiceProvider();
|
||||
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1);
|
||||
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||
var streamReader = new StreamReader(httpResponse.Body);
|
||||
var body = streamReader.ReadToEndAsync().Result;
|
||||
Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Null(httpResponse.ContentLength);
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteResultAsync_FallsBackToWebRootFileProvider_IfNoFileProviderIsPresent()
|
||||
{
|
||||
|
|
@ -133,6 +398,58 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
sendFileMock.Verify();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 3, "File", 4)]
|
||||
[InlineData(8, 13, "Result", 6)]
|
||||
[InlineData(null, 3, "ts¡", 3)]
|
||||
[InlineData(8, null, "ResultTestFile contents¡", 25)]
|
||||
public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(long? start, long? end, string expectedString, long contentLength)
|
||||
{
|
||||
// Arrange
|
||||
var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt");
|
||||
var result = new TestVirtualFileResult(path, "text/plain")
|
||||
{
|
||||
FileProvider = GetFileProvider(path),
|
||||
};
|
||||
|
||||
var sendFile = new TestSendFileFeature();
|
||||
var httpContext = GetHttpContext();
|
||||
httpContext.Features.Set<IHttpSendFileFeature>(sendFile);
|
||||
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
var appEnvironment = new Mock<IHostingEnvironment>();
|
||||
appEnvironment.Setup(app => app.WebRootFileProvider)
|
||||
.Returns(GetFileProvider(path));
|
||||
httpContext.RequestServices = new ServiceCollection()
|
||||
.AddSingleton(appEnvironment.Object)
|
||||
.AddTransient<TestVirtualFileResultExecutor>()
|
||||
.AddTransient<ILoggerFactory, LoggerFactory>()
|
||||
.BuildServiceProvider();
|
||||
|
||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||
requestHeaders.Range = new RangeHeaderValue(start, end);
|
||||
requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
|
||||
httpContext.Request.Method = HttpMethods.Get;
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
start = start ?? 33 - end;
|
||||
end = start + contentLength - 1;
|
||||
var httpResponse = actionContext.HttpContext.Response;
|
||||
Assert.Equal(Path.Combine("TestFiles", "FilePathResultTestFile.txt"), sendFile.Name);
|
||||
Assert.Equal(start, sendFile.Offset);
|
||||
Assert.Equal(contentLength, sendFile.Length);
|
||||
Assert.Equal(CancellationToken.None, sendFile.Token);
|
||||
var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, 33);
|
||||
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
|
||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
||||
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||
Assert.Equal(contentLength, httpResponse.ContentLength);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
|
||||
{
|
||||
|
|
@ -325,7 +642,11 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
private static IFileProvider GetFileProvider(string path)
|
||||
{
|
||||
var fileInfo = new Mock<IFileInfo>();
|
||||
fileInfo.SetupGet(fi => fi.Length).Returns(33);
|
||||
fileInfo.SetupGet(fi => fi.Exists).Returns(true);
|
||||
var lastModified = DateTimeOffset.MinValue.AddDays(1);
|
||||
lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0));
|
||||
fileInfo.SetupGet(fi => fi.LastModified).Returns(lastModified);
|
||||
fileInfo.SetupGet(fi => fi.PhysicalPath).Returns(path);
|
||||
var fileProvider = new Mock<IFileProvider>();
|
||||
fileProvider.Setup(fp => fp.GetFileInfo(path))
|
||||
|
|
@ -355,7 +676,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
private class TestVirtualFileResultExecutor : VirtualFileResultExecutor
|
||||
{
|
||||
public TestVirtualFileResultExecutor(ILoggerFactory loggerFactory, IHostingEnvironment hostingEnvironment)
|
||||
: base(loggerFactory,hostingEnvironment)
|
||||
: base(loggerFactory, hostingEnvironment)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -373,5 +694,23 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TestSendFileFeature : IHttpSendFileFeature
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public long Offset { get; set; }
|
||||
public long? Length { get; set; }
|
||||
public CancellationToken Token { get; set; }
|
||||
|
||||
public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
|
||||
{
|
||||
Name = path;
|
||||
Offset = offset;
|
||||
Length = length;
|
||||
Token = cancellation;
|
||||
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Xunit;
|
||||
|
|
@ -37,6 +40,92 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal("This is a sample text file", body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 6, "This is")]
|
||||
[InlineData(17, 25, "text file")]
|
||||
[InlineData(0, 50, "This is a sample text file")]
|
||||
public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequest(long start, long end, string expectedBody)
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDisk");
|
||||
httpRequestMessage.Headers.Range = new RangeHeaderValue(start, end);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal(expectedBody, body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0-6")]
|
||||
[InlineData("bytes = 11-6")]
|
||||
[InlineData("bytes = 1-4, 5-11")]
|
||||
public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestNotSatisfiable(string rangeString)
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDisk");
|
||||
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 6, "This is")]
|
||||
[InlineData(17, 25, "text file")]
|
||||
[InlineData(0, 50, "This is a sample text file")]
|
||||
public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequest_WithLastModifiedAndEtag(long start, long end, string expectedBody)
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDisk_WithLastModifiedAndEtag");
|
||||
httpRequestMessage.Headers.Range = new RangeHeaderValue(start, end);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal(expectedBody, body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0-6")]
|
||||
[InlineData("bytes = 11-6")]
|
||||
[InlineData("bytes = 1-4, 5-11")]
|
||||
public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestNotSatisfiable_WithLastModifiedAndEtag(string rangeString)
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDiskWithFileName_WithLastModifiedAndEtag");
|
||||
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
// https://github.com/aspnet/Mvc/issues/2727
|
||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
|
||||
|
|
@ -60,6 +149,45 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromDisk_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest_WithLastModifiedAndEtag()
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDiskWithFileName_WithLastModifiedAndEtag");
|
||||
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
|
||||
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal("This is", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromDisk_ReturnsFileWithFileName_IfRangeHeaderInvalid_RangeRequestIgnored_WithLastModifiedAndEtag()
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDiskWithFileName_WithLastModifiedAndEtag");
|
||||
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
|
||||
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal("This is a sample text file", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromStream_ReturnsFile()
|
||||
{
|
||||
|
|
@ -77,6 +205,49 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal("This is sample text from a stream", body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 6, "This is")]
|
||||
[InlineData(25, 32, "a stream")]
|
||||
[InlineData(0, 50, "This is sample text from a stream")]
|
||||
public async Task FileFromStream_ReturnsFile_RangeRequest(long start, long end, string expectedBody)
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStream");
|
||||
httpRequestMessage.Headers.Range = new RangeHeaderValue(start, end);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal(expectedBody, body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0-6")]
|
||||
[InlineData("bytes = 11-6")]
|
||||
[InlineData("bytes = 1-4, 5-11")]
|
||||
public async Task FileFromStream_ReturnsFile_RangeRequestNotSatisfiable(string rangeString)
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStream");
|
||||
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromStream_ReturnsFileWithFileName()
|
||||
{
|
||||
|
|
@ -98,6 +269,45 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromStream_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest()
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStreamWithFileName_WithEtag");
|
||||
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
|
||||
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal("This is", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromStream_ReturnsFileWithFileName_IfRangeHeaderInvalid_RangeRequestNotSatisfiable()
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStreamWithFileName_WithEtag");
|
||||
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
|
||||
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal("This is sample text from a stream", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromBinaryData_ReturnsFile()
|
||||
{
|
||||
|
|
@ -115,6 +325,49 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal("This is a sample text from a binary array", body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 6, "This is")]
|
||||
[InlineData(29, 40, "binary array")]
|
||||
[InlineData(0, 50, "This is a sample text from a binary array")]
|
||||
public async Task FileFromBinaryData_ReturnsFile_RangeRequest(long start, long end, string expectedBody)
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryData");
|
||||
httpRequestMessage.Headers.Range = new RangeHeaderValue(start, end);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal(expectedBody, body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0-6")]
|
||||
[InlineData("bytes = 11-6")]
|
||||
[InlineData("bytes = 1-4, 5-11")]
|
||||
public async Task FileFromBinaryData_ReturnsFile_RangeRequestNotSatisfiable(string rangeString)
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryData");
|
||||
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.Empty(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromBinaryData_ReturnsFileWithFileName()
|
||||
{
|
||||
|
|
@ -136,6 +389,45 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromBinaryData_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest()
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryDataWithFileName_WithEtag");
|
||||
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
|
||||
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal("This is", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromBinaryData_ReturnsFileWithFileName_IfRangeHeaderInvalid_RangeRequestIgnored()
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryDataWithFileName_WithEtag");
|
||||
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
|
||||
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal("This is a sample text from a binary array", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName()
|
||||
{
|
||||
|
|
@ -159,5 +451,99 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.NotNull(contentDisposition);
|
||||
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 6, "Sample ")]
|
||||
[InlineData(20, 37, "embedded resource.")]
|
||||
[InlineData(7, 50, "text file as embedded resource.")]
|
||||
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeRequest(long start, long end, string expectedBody)
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName");
|
||||
httpRequestMessage.Headers.Range = new RangeHeaderValue(start, end);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal(expectedBody, body);
|
||||
var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
|
||||
Assert.NotNull(contentDisposition);
|
||||
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest()
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName_WithEtag");
|
||||
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
|
||||
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal("Sample ", body);
|
||||
var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
|
||||
Assert.NotNull(contentDisposition);
|
||||
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_IfRangeHeaderInvalid_RangeRequestIgnored()
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName_WithEtag");
|
||||
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
|
||||
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal("Sample text file as embedded resource.", body);
|
||||
var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
|
||||
Assert.NotNull(contentDisposition);
|
||||
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0-6")]
|
||||
[InlineData("bytes = 11-6")]
|
||||
[InlineData("bytes = 1-4, 5-11")]
|
||||
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeRequestNotSatisfiable(string rangeString)
|
||||
{
|
||||
// Arrange
|
||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName");
|
||||
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(httpRequestMessage);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode);
|
||||
Assert.NotNull(response.Content.Headers.ContentType);
|
||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.Empty(body);
|
||||
var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
|
||||
Assert.NotNull(contentDisposition);
|
||||
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
|
||||
<PackageReference Include="FSharp.Core" Version="$(FSharpCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.ChunkingCookieManager.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(AspNetCoreVersion)" />
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace FilesWebSite
|
||||
{
|
||||
|
|
@ -23,12 +25,28 @@ namespace FilesWebSite
|
|||
return PhysicalFile(path, "text/plain");
|
||||
}
|
||||
|
||||
public IActionResult DownloadFromDisk_WithLastModifiedAndEtag()
|
||||
{
|
||||
var path = Path.Combine(_hostingEnvironment.ContentRootPath, "sample.txt");
|
||||
var lastModified = new DateTimeOffset(year: 1999, month: 11, day: 04, hour: 3, minute: 0, second: 0, offset: new TimeSpan(0));
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
return PhysicalFile(path, "text/plain", lastModified, entityTag);
|
||||
}
|
||||
|
||||
public IActionResult DownloadFromDiskWithFileName()
|
||||
{
|
||||
var path = Path.Combine(_hostingEnvironment.ContentRootPath, "sample.txt");
|
||||
return PhysicalFile(path, "text/plain", "downloadName.txt");
|
||||
}
|
||||
|
||||
public IActionResult DownloadFromDiskWithFileName_WithLastModifiedAndEtag()
|
||||
{
|
||||
var path = Path.Combine(_hostingEnvironment.ContentRootPath, "sample.txt");
|
||||
var lastModified = new DateTimeOffset(year: 1999, month: 11, day: 04, hour: 3, minute: 0, second: 0, offset: new TimeSpan(0));
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
return PhysicalFile(path, "text/plain", "downloadName.txt", lastModified, entityTag);
|
||||
}
|
||||
|
||||
public IActionResult DownloadFromStream()
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
|
|
@ -51,6 +69,17 @@ namespace FilesWebSite
|
|||
return File(stream, "text/plain", "downloadName.txt");
|
||||
}
|
||||
|
||||
public IActionResult DownloadFromStreamWithFileName_WithEtag()
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
var writer = new StreamWriter(stream);
|
||||
writer.Write("This is sample text from a stream");
|
||||
writer.Flush();
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
return File(stream, "text/plain", "downloadName.txt", lastModified: null, entityTag: entityTag);
|
||||
}
|
||||
|
||||
public IActionResult DownloadFromBinaryData()
|
||||
{
|
||||
var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");
|
||||
|
|
@ -62,5 +91,12 @@ namespace FilesWebSite
|
|||
var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");
|
||||
return File(data, "text/plain", "downloadName.txt");
|
||||
}
|
||||
|
||||
public IActionResult DownloadFromBinaryDataWithFileName_WithEtag()
|
||||
{
|
||||
var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");
|
||||
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||
return File(data, "text/plain", "downloadName.txt", lastModified: null, entityTag: entityTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,5 +17,17 @@ namespace FilesWebSite
|
|||
FileDownloadName = "downloadName.txt"
|
||||
};
|
||||
}
|
||||
|
||||
public IActionResult DownloadFileWithFileName_WithEtag()
|
||||
{
|
||||
var file = new VirtualFileResult("/Greetings.txt", "text/plain")
|
||||
{
|
||||
FileProvider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, "FilesWebSite.EmbeddedResources"),
|
||||
FileDownloadName = "downloadName.txt"
|
||||
};
|
||||
|
||||
file.EntityTag = new Microsoft.Net.Http.Headers.EntityTagHeaderValue("\"Etag\"");
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
Sample text file as embedded resource.
|
||||
Sample text file as embedded resource.
|
||||
|
|
@ -1 +1 @@
|
|||
This is a sample text file
|
||||
This is a sample text file
|
||||
Loading…
Reference in New Issue