parent
ece5e748ad
commit
9aff0a67c1
|
|
@ -1135,8 +1135,10 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a file with the specified <paramref name="fileContents" /> as content
|
/// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>),
|
||||||
/// (<see cref="StatusCodes.Status200OK"/>) and the specified <paramref name="contentType" /> as the Content-Type.
|
/// 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>
|
/// </summary>
|
||||||
/// <param name="fileContents">The file contents.</param>
|
/// <param name="fileContents">The file contents.</param>
|
||||||
/// <param name="contentType">The Content-Type of the file.</param>
|
/// <param name="contentType">The Content-Type of the file.</param>
|
||||||
|
|
@ -1149,8 +1151,9 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>), the
|
/// 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="contentType" /> as the Content-Type and the specified <paramref name="fileDownloadName" /> as the suggested file name.
|
||||||
/// 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>
|
/// </summary>
|
||||||
/// <param name="fileContents">The file contents.</param>
|
/// <param name="fileContents">The file contents.</param>
|
||||||
/// <param name="contentType">The Content-Type of the file.</param>
|
/// <param name="contentType">The Content-Type of the file.</param>
|
||||||
|
|
@ -1163,8 +1166,54 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>)
|
/// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>),
|
||||||
/// with the specified <paramref name="contentType" /> as the Content-Type.
|
/// 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>
|
/// </summary>
|
||||||
/// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
|
/// <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="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
|
/// 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="contentType" /> as the Content-Type and the
|
||||||
/// specified <paramref name="fileDownloadName" /> as the suggested file name.
|
/// 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>
|
/// </summary>
|
||||||
/// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
|
/// <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="contentType">The Content-Type of the file.</param>
|
||||||
|
|
@ -1190,9 +1241,55 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
return new FileStreamResult(fileStream, contentType) { FileDownloadName = fileDownloadName };
|
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>
|
/// <summary>
|
||||||
/// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
|
/// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
|
||||||
/// specified <paramref name="contentType" /> as the Content-Type.
|
/// 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>
|
/// </summary>
|
||||||
/// <param name="virtualPath">The virtual path of the file to be returned.</param>
|
/// <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="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
|
/// 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="contentType" /> as the Content-Type and the
|
||||||
/// specified <paramref name="fileDownloadName" /> as the suggested file name.
|
/// 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>
|
/// </summary>
|
||||||
/// <param name="virtualPath">The virtual path of the file to be returned.</param>
|
/// <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="contentType">The Content-Type of the file.</param>
|
||||||
|
|
@ -1218,9 +1317,55 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
return new VirtualFileResult(virtualPath, contentType) { FileDownloadName = fileDownloadName };
|
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>
|
/// <summary>
|
||||||
/// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
|
/// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
|
||||||
/// specified <paramref name="contentType" /> as the Content-Type.
|
/// 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>
|
/// </summary>
|
||||||
/// <param name="physicalPath">The physical path of the file to be returned.</param>
|
/// <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="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
|
/// 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="contentType" /> as the Content-Type and the
|
||||||
/// specified <paramref name="fileDownloadName" /> as the suggested file name.
|
/// 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>
|
/// </summary>
|
||||||
/// <param name="physicalPath">The physical path of the file to be returned.</param>
|
/// <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="contentType">The Content-Type of the file.</param>
|
||||||
|
|
@ -1249,6 +1396,50 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
return new PhysicalFileResult(physicalPath, contentType) { FileDownloadName = fileDownloadName };
|
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>
|
/// <summary>
|
||||||
/// Creates an <see cref="UnauthorizedResult"/> that produces an <see cref="StatusCodes.Status401Unauthorized"/> response.
|
/// Creates an <see cref="UnauthorizedResult"/> that produces an <see cref="StatusCodes.Status401Unauthorized"/> response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Core;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Net.Http.Headers;
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using Microsoft.Net.Http.Headers;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc
|
namespace Microsoft.AspNetCore.Mvc
|
||||||
{
|
{
|
||||||
|
|
@ -43,5 +42,15 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
get { return _fileDownloadName ?? string.Empty; }
|
get { return _fileDownloadName ?? string.Empty; }
|
||||||
set { _fileDownloadName = value; }
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
|
|
@ -26,11 +28,22 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
throw new ArgumentNullException(nameof(result));
|
throw new ArgumentNullException(nameof(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
SetHeadersAndLog(context, result);
|
var (range, rangeLength, serveBody) = SetHeadersAndLog(
|
||||||
return WriteFileAsync(context, result);
|
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)
|
if (context == null)
|
||||||
{
|
{
|
||||||
|
|
@ -42,9 +55,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
throw new ArgumentNullException(nameof(result));
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
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.Extensions.Logging;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
|
|
@ -9,20 +17,38 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
public class FileResultExecutorBase
|
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)
|
public FileResultExecutorBase(ILogger logger)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal enum PreconditionState
|
||||||
|
{
|
||||||
|
Unspecified,
|
||||||
|
NotModified,
|
||||||
|
ShouldProcess,
|
||||||
|
PreconditionFailed,
|
||||||
|
IgnoreRangeRequest
|
||||||
|
}
|
||||||
|
|
||||||
protected ILogger Logger { get; }
|
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)
|
if (context == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(context));
|
throw new ArgumentNullException(nameof(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == null)
|
if (result == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(result));
|
throw new ArgumentNullException(nameof(result));
|
||||||
|
|
@ -31,9 +57,56 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
SetContentType(context, result);
|
SetContentType(context, result);
|
||||||
SetContentDispositionHeader(context, result);
|
SetContentDispositionHeader(context, result);
|
||||||
Logger.FileResultExecuting(result.FileDownloadName);
|
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))
|
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;
|
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)
|
protected static ILogger CreateLogger<T>(ILoggerFactory factory)
|
||||||
|
|
@ -63,5 +325,31 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
return factory.CreateLogger<T>();
|
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;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
public class FileStreamResultExecutor : FileResultExecutorBase
|
public class FileStreamResultExecutor : FileResultExecutorBase
|
||||||
{
|
{
|
||||||
// default buffer size as defined in BufferedStream type
|
|
||||||
private const int BufferSize = 0x1000;
|
|
||||||
|
|
||||||
public FileStreamResultExecutor(ILoggerFactory loggerFactory)
|
public FileStreamResultExecutor(ILoggerFactory loggerFactory)
|
||||||
: base(CreateLogger<VirtualFileResultExecutor>(loggerFactory))
|
: base(CreateLogger<VirtualFileResultExecutor>(loggerFactory))
|
||||||
{
|
{
|
||||||
|
|
@ -29,11 +27,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
throw new ArgumentNullException(nameof(result));
|
throw new ArgumentNullException(nameof(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
SetHeadersAndLog(context, result);
|
long? fileLength = null;
|
||||||
return WriteFileAsync(context, result);
|
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)
|
if (context == null)
|
||||||
{
|
{
|
||||||
|
|
@ -45,13 +60,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
throw new ArgumentNullException(nameof(result));
|
throw new ArgumentNullException(nameof(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (range != null && rangeLength == 0)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
var response = context.HttpContext.Response;
|
var response = context.HttpContext.Response;
|
||||||
var outputStream = response.Body;
|
var outputStream = response.Body;
|
||||||
|
return WriteFileAsync(context.HttpContext, result.FileStream, range, rangeLength);
|
||||||
using (result.FileStream)
|
|
||||||
{
|
|
||||||
await result.FileStream.CopyToAsync(outputStream, BufferSize);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,12 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Mvc.Core;
|
using Microsoft.AspNetCore.Mvc.Core;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
public class PhysicalFileResultExecutor : FileResultExecutorBase
|
public class PhysicalFileResultExecutor : FileResultExecutorBase
|
||||||
{
|
{
|
||||||
private const int DefaultBufferSize = 0x1000;
|
|
||||||
|
|
||||||
public PhysicalFileResultExecutor(ILoggerFactory loggerFactory)
|
public PhysicalFileResultExecutor(ILoggerFactory loggerFactory)
|
||||||
: base(CreateLogger<PhysicalFileResultExecutor>(loggerFactory))
|
: base(CreateLogger<PhysicalFileResultExecutor>(loggerFactory))
|
||||||
{
|
{
|
||||||
|
|
@ -32,11 +31,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
throw new ArgumentNullException(nameof(result));
|
throw new ArgumentNullException(nameof(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
SetHeadersAndLog(context, result);
|
var fileInfo = GetFileInfo(result.FileName);
|
||||||
return WriteFileAsync(context, 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, 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)
|
if (context == null)
|
||||||
{
|
{
|
||||||
|
|
@ -48,8 +66,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
throw new ArgumentNullException(nameof(result));
|
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))
|
if (!Path.IsPathRooted(result.FileName))
|
||||||
{
|
{
|
||||||
throw new NotSupportedException(Resources.FormatFileResult_PathNotRooted(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>();
|
var sendFile = response.HttpContext.Features.Get<IHttpSendFileFeature>();
|
||||||
if (sendFile != null)
|
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,
|
result.FileName,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
count: null,
|
count: null,
|
||||||
cancellation: default(CancellationToken));
|
cancellation: default(CancellationToken));
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
var fileStream = GetFileStream(result.FileName);
|
|
||||||
|
|
||||||
using (fileStream)
|
return WriteFileAsync(context.HttpContext, GetFileStream(result.FileName), range, rangeLength);
|
||||||
{
|
|
||||||
await fileStream.CopyToAsync(response.Body, DefaultBufferSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual Stream GetFileStream(string path)
|
protected virtual Stream GetFileStream(string path)
|
||||||
|
|
@ -87,8 +111,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
FileMode.Open,
|
FileMode.Open,
|
||||||
FileAccess.Read,
|
FileAccess.Read,
|
||||||
FileShare.ReadWrite,
|
FileShare.ReadWrite,
|
||||||
DefaultBufferSize,
|
BufferSize,
|
||||||
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
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.AspNetCore.Mvc.Core;
|
||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.Extensions.FileProviders;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
public class VirtualFileResultExecutor : FileResultExecutorBase
|
public class VirtualFileResultExecutor : FileResultExecutorBase
|
||||||
{
|
{
|
||||||
private const int DefaultBufferSize = 0x1000;
|
|
||||||
private readonly IHostingEnvironment _hostingEnvironment;
|
private readonly IHostingEnvironment _hostingEnvironment;
|
||||||
|
|
||||||
public VirtualFileResultExecutor(ILoggerFactory loggerFactory, IHostingEnvironment hostingEnvironment)
|
public VirtualFileResultExecutor(ILoggerFactory loggerFactory, IHostingEnvironment hostingEnvironment)
|
||||||
|
|
@ -41,11 +41,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
throw new ArgumentNullException(nameof(result));
|
throw new ArgumentNullException(nameof(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
SetHeadersAndLog(context, result);
|
var fileInfo = GetFileInformation(result);
|
||||||
return WriteFileAsync(context, 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)
|
if (context == null)
|
||||||
{
|
{
|
||||||
|
|
@ -57,7 +76,37 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
throw new ArgumentNullException(nameof(result));
|
throw new ArgumentNullException(nameof(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (range != null && rangeLength == 0)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
var response = context.HttpContext.Response;
|
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 fileProvider = GetFileProvider(result);
|
||||||
|
|
||||||
var normalizedPath = result.FileName;
|
var normalizedPath = result.FileName;
|
||||||
|
|
@ -67,32 +116,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileInfo = fileProvider.GetFileInfo(normalizedPath);
|
var fileInfo = fileProvider.GetFileInfo(normalizedPath);
|
||||||
if (fileInfo.Exists)
|
return fileInfo;
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IFileProvider GetFileProvider(VirtualFileResult result)
|
private IFileProvider GetFileProvider(VirtualFileResult result)
|
||||||
|
|
@ -103,7 +127,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
result.FileProvider = _hostingEnvironment.WebRootFileProvider;
|
result.FileProvider = _hostingEnvironment.WebRootFileProvider;
|
||||||
|
|
||||||
return result.FileProvider;
|
return result.FileProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ Microsoft.AspNetCore.Mvc.RouteAttribute</Description>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Http" 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.ResponseCaching.Abstractions" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Routing" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Routing" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.ClosedGenericMatcher.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
|
<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.PropertyActivator.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
|
||||||
<PackageReference Include="Microsoft.Extensions.PropertyHelper.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="Microsoft.Extensions.SecurityHelper.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
|
||||||
|
<PackageReference Include="System.ValueTuple" Version="$(CoreFxVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1427,6 +1427,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
||||||
Assert.Equal(string.Empty, result.FileDownloadName);
|
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]
|
[Fact]
|
||||||
public void File_WithContentsAndFileDownloadName()
|
public void File_WithContentsAndFileDownloadName()
|
||||||
{
|
{
|
||||||
|
|
@ -1444,6 +1469,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
||||||
Assert.Equal("someDownloadName", result.FileDownloadName);
|
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]
|
[Fact]
|
||||||
public void File_WithPath()
|
public void File_WithPath()
|
||||||
{
|
{
|
||||||
|
|
@ -1461,6 +1511,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
||||||
Assert.Equal(string.Empty, result.FileDownloadName);
|
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]
|
[Fact]
|
||||||
public void File_WithPathAndFileDownloadName()
|
public void File_WithPathAndFileDownloadName()
|
||||||
{
|
{
|
||||||
|
|
@ -1478,6 +1553,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
||||||
Assert.Equal("someDownloadName", result.FileDownloadName);
|
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]
|
[Fact]
|
||||||
public void File_WithStream()
|
public void File_WithStream()
|
||||||
{
|
{
|
||||||
|
|
@ -1500,6 +1600,36 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
||||||
Assert.Equal(string.Empty, result.FileDownloadName);
|
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]
|
[Fact]
|
||||||
public void File_WithStreamAndFileDownloadName()
|
public void File_WithStreamAndFileDownloadName()
|
||||||
{
|
{
|
||||||
|
|
@ -1521,6 +1651,35 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
||||||
Assert.Equal("someDownloadName", result.FileDownloadName);
|
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]
|
[Fact]
|
||||||
public void HttpUnauthorized_SetsStatusCode()
|
public void HttpUnauthorized_SetsStatusCode()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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.IO;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
|
@ -11,6 +13,7 @@ using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc
|
namespace Microsoft.AspNetCore.Mvc
|
||||||
|
|
@ -46,6 +49,29 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
|
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]
|
[Fact]
|
||||||
public async Task WriteFileAsync_CopiesBuffer_ToOutputStream()
|
public async Task WriteFileAsync_CopiesBuffer_ToOutputStream()
|
||||||
{
|
{
|
||||||
|
|
@ -68,6 +94,268 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
Assert.Equal(buffer, outStream.ToArray());
|
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]
|
[Fact]
|
||||||
public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
|
public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,160 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
Assert.Equal(expectedOutput, actual);
|
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()
|
private static IServiceCollection CreateServices()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
|
|
@ -297,13 +451,13 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
private class EmptyFileResultExecutor : FileResultExecutorBase
|
private class EmptyFileResultExecutor : FileResultExecutorBase
|
||||||
{
|
{
|
||||||
public EmptyFileResultExecutor(ILoggerFactory loggerFactory)
|
public EmptyFileResultExecutor(ILoggerFactory loggerFactory)
|
||||||
:base(CreateLogger<EmptyFileResultExecutor>(loggerFactory))
|
: base(CreateLogger<EmptyFileResultExecutor>(loggerFactory))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ExecuteAsync(ActionContext context, EmptyFileResult result)
|
public Task ExecuteAsync(ActionContext context, EmptyFileResult result)
|
||||||
{
|
{
|
||||||
SetHeadersAndLog(context, result);
|
SetHeadersAndLog(context, result, 0L);
|
||||||
result.WasWriteFileCalled = true;
|
result.WasWriteFileCalled = true;
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -13,6 +15,7 @@ using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -49,6 +52,348 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
|
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]
|
[Fact]
|
||||||
public async Task WriteFileAsync_WritesResponse_InChunksOfFourKilobytes()
|
public async Task WriteFileAsync_WritesResponse_InChunksOfFourKilobytes()
|
||||||
{
|
{
|
||||||
|
|
@ -153,7 +498,6 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
|
|
||||||
var httpContext = new DefaultHttpContext();
|
var httpContext = new DefaultHttpContext();
|
||||||
httpContext.RequestServices = services.BuildServiceProvider();
|
httpContext.RequestServices = services.BuildServiceProvider();
|
||||||
|
|
||||||
return httpContext;
|
return httpContext;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
|
@ -16,6 +15,7 @@ using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -52,6 +52,198 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
|
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]
|
[Fact]
|
||||||
public async Task ExecuteResultAsync_FallsbackToStreamCopy_IfNoIHttpSendFilePresent()
|
public async Task ExecuteResultAsync_FallsbackToStreamCopy_IfNoIHttpSendFilePresent()
|
||||||
{
|
{
|
||||||
|
|
@ -94,6 +286,46 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
sendFileMock.Verify();
|
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]
|
[Fact]
|
||||||
public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
|
public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
|
||||||
{
|
{
|
||||||
|
|
@ -236,6 +468,35 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
return new MemoryStream(Encoding.UTF8.GetBytes("FilePathResultTestFile contents<74>"));
|
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()
|
private static IServiceCollection CreateServices()
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.Extensions.FileProviders;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -51,8 +52,272 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(path, result.FileName);
|
Assert.Equal(path, result.FileName);
|
||||||
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
|
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]
|
[Fact]
|
||||||
public async Task ExecuteResultAsync_FallsBackToWebRootFileProvider_IfNoFileProviderIsPresent()
|
public async Task ExecuteResultAsync_FallsBackToWebRootFileProvider_IfNoFileProviderIsPresent()
|
||||||
{
|
{
|
||||||
|
|
@ -133,6 +398,58 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
sendFileMock.Verify();
|
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]
|
[Fact]
|
||||||
public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
|
public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
|
||||||
{
|
{
|
||||||
|
|
@ -325,7 +642,11 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
private static IFileProvider GetFileProvider(string path)
|
private static IFileProvider GetFileProvider(string path)
|
||||||
{
|
{
|
||||||
var fileInfo = new Mock<IFileInfo>();
|
var fileInfo = new Mock<IFileInfo>();
|
||||||
|
fileInfo.SetupGet(fi => fi.Length).Returns(33);
|
||||||
fileInfo.SetupGet(fi => fi.Exists).Returns(true);
|
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);
|
fileInfo.SetupGet(fi => fi.PhysicalPath).Returns(path);
|
||||||
var fileProvider = new Mock<IFileProvider>();
|
var fileProvider = new Mock<IFileProvider>();
|
||||||
fileProvider.Setup(fp => fp.GetFileInfo(path))
|
fileProvider.Setup(fp => fp.GetFileInfo(path))
|
||||||
|
|
@ -355,7 +676,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
private class TestVirtualFileResultExecutor : VirtualFileResultExecutor
|
private class TestVirtualFileResultExecutor : VirtualFileResultExecutor
|
||||||
{
|
{
|
||||||
public TestVirtualFileResultExecutor(ILoggerFactory loggerFactory, IHostingEnvironment hostingEnvironment)
|
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.
|
// 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.
|
// 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;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Testing.xunit;
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -37,6 +40,92 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
Assert.Equal("This is a sample text file", body);
|
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]
|
[ConditionalFact]
|
||||||
// https://github.com/aspnet/Mvc/issues/2727
|
// https://github.com/aspnet/Mvc/issues/2727
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
|
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
|
||||||
|
|
@ -60,6 +149,45 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
|
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]
|
[Fact]
|
||||||
public async Task FileFromStream_ReturnsFile()
|
public async Task FileFromStream_ReturnsFile()
|
||||||
{
|
{
|
||||||
|
|
@ -77,6 +205,49 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
Assert.Equal("This is sample text from a stream", body);
|
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]
|
[Fact]
|
||||||
public async Task FileFromStream_ReturnsFileWithFileName()
|
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);
|
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]
|
[Fact]
|
||||||
public async Task FileFromBinaryData_ReturnsFile()
|
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);
|
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]
|
[Fact]
|
||||||
public async Task FileFromBinaryData_ReturnsFileWithFileName()
|
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);
|
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]
|
[Fact]
|
||||||
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName()
|
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName()
|
||||||
{
|
{
|
||||||
|
|
@ -159,5 +451,99 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
Assert.NotNull(contentDisposition);
|
Assert.NotNull(contentDisposition);
|
||||||
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", 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="FSharp.Core" Version="$(FSharpCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.ChunkingCookieManager.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
|
<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.TestHost" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(AspNetCoreVersion)" />
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace FilesWebSite
|
namespace FilesWebSite
|
||||||
{
|
{
|
||||||
|
|
@ -23,12 +25,28 @@ namespace FilesWebSite
|
||||||
return PhysicalFile(path, "text/plain");
|
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()
|
public IActionResult DownloadFromDiskWithFileName()
|
||||||
{
|
{
|
||||||
var path = Path.Combine(_hostingEnvironment.ContentRootPath, "sample.txt");
|
var path = Path.Combine(_hostingEnvironment.ContentRootPath, "sample.txt");
|
||||||
return PhysicalFile(path, "text/plain", "downloadName.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()
|
public IActionResult DownloadFromStream()
|
||||||
{
|
{
|
||||||
var stream = new MemoryStream();
|
var stream = new MemoryStream();
|
||||||
|
|
@ -51,6 +69,17 @@ namespace FilesWebSite
|
||||||
return File(stream, "text/plain", "downloadName.txt");
|
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()
|
public IActionResult DownloadFromBinaryData()
|
||||||
{
|
{
|
||||||
var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");
|
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");
|
var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");
|
||||||
return File(data, "text/plain", "downloadName.txt");
|
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"
|
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