[Fixes #429] FileResult
1) Implemented FilePathResult to efficiently return files from disk. 2) Implemented FileStreamResult to return content from a stream. 3) Implemented FileContentResult to return content from a byte array.
This commit is contained in:
parent
2f00fc6121
commit
a2023d35ee
13
Mvc.sln
13
Mvc.sln
|
|
@ -84,6 +84,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ApiExplorerWebSite", "test\
|
|||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ReflectedModelWebSite", "test\WebSites\ReflectedModelWebSite\ReflectedModelWebSite.kproj", "{C2EF54F8-8886-4260-A322-44F76245F95D}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "FilesWebSite", "test\WebSites\FilesWebSite\FilesWebSite.kproj", "{0EF9860B-10D7-452F-B0F4-A405B88BEBB3}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -414,6 +416,16 @@ Global
|
|||
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{61061528-071E-424E-965A-07BCC2F02672}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{61061528-071E-424E-965A-07BCC2F02672}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{61061528-071E-424E-965A-07BCC2F02672}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
|
|
@ -472,6 +484,7 @@ Global
|
|||
{1976AC4A-FEA4-4587-A158-D9F79736D2B6} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
||||
{96107AC0-18E2-474D-BAB4-2FFF2185FBCD} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
||||
{A192E504-2881-41DC-90D1-B7F1DD1134E8} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
||||
{0EF9860B-10D7-452F-B0F4-A405B88BEBB3} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
||||
{61061528-071E-424E-965A-07BCC2F02672} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
||||
{C2EF54F8-8886-4260-A322-44F76245F95D} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
||||
EndGlobalSection
|
||||
|
|
|
|||
|
|
@ -34,6 +34,16 @@ namespace MvcSample.Web
|
|||
return HttpNotFound();
|
||||
}
|
||||
|
||||
public ActionResult SendFileFromDisk()
|
||||
{
|
||||
return File("sample.txt", "text/plain");
|
||||
}
|
||||
|
||||
public ActionResult SendFileFromDiskWithName()
|
||||
{
|
||||
return File("sample.txt", "text/plain", "sample-file.txt");
|
||||
}
|
||||
|
||||
public bool IsDefaultNameSpace()
|
||||
{
|
||||
var namespaceToken = ActionContext.RouteData.DataTokens["NameSpace"] as string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
This is a sample file returned from a controller
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an <see cref="ActionResult"/> that when executed will
|
||||
/// write a binary file to the response.
|
||||
/// </summary>
|
||||
public class FileContentResult : FileResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="FileContentResult"/> instance with
|
||||
/// the provided <paramref name="fileContents"/> and the
|
||||
/// provided <paramref name="contentType"/>.
|
||||
/// </summary>
|
||||
/// <param name="fileContents">The bytes that represent the file contents.</param>
|
||||
/// <param name="contentType">The Content-Type header of the response.</param>
|
||||
public FileContentResult([NotNull] byte[] fileContents, string contentType)
|
||||
: base(contentType)
|
||||
{
|
||||
FileContents = fileContents;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file contents.
|
||||
/// </summary>
|
||||
public byte[] FileContents { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task WriteFileAsync(HttpResponse response, CancellationToken cancellation)
|
||||
{
|
||||
return response.Body.WriteAsync(FileContents, 0, FileContents.Length, cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.HttpFeature;
|
||||
using Microsoft.AspNet.Mvc.Core;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an <see cref="ActionResult"/> that when executed will
|
||||
/// write a file from disk to the response using mechanisms provided
|
||||
/// by the host.
|
||||
/// </summary>
|
||||
public class FilePathResult : FileResult
|
||||
{
|
||||
private const int DefaultBufferSize = 0x1000;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="FilePathResult"/> instance with
|
||||
/// the provided <paramref name="fileName"/> and the
|
||||
/// provided <paramref name="contentType"/>.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The path to the file. The path must be an absolute
|
||||
/// path. Relative and virtual paths are not supported.</param>
|
||||
/// <param name="contentType">The Content-Type header of the response.</param>
|
||||
public FilePathResult([NotNull] string fileName, [NotNull] string contentType)
|
||||
: base(contentType)
|
||||
{
|
||||
if (!Path.IsPathRooted(fileName))
|
||||
{
|
||||
var message = Resources.FormatFileResult_InvalidPathType_RelativeOrVirtualPath(fileName);
|
||||
throw new ArgumentException(message, "fileName");
|
||||
}
|
||||
|
||||
FileName = fileName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the file that will be sent back as the response.
|
||||
/// </summary>
|
||||
public string FileName { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task WriteFileAsync(HttpResponse response, CancellationToken cancellation)
|
||||
{
|
||||
var sendFile = response.HttpContext.GetFeature<IHttpSendFileFeature>();
|
||||
if (sendFile != null)
|
||||
{
|
||||
return sendFile.SendFileAsync(
|
||||
FileName,
|
||||
offset: 0,
|
||||
length: null,
|
||||
cancellation: cancellation);
|
||||
}
|
||||
else
|
||||
{
|
||||
return CopyStreamToResponse(FileName, response, cancellation);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task CopyStreamToResponse(
|
||||
string fileName,
|
||||
HttpResponse response,
|
||||
CancellationToken cancellation)
|
||||
{
|
||||
var fileStream = new FileStream(
|
||||
fileName, FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite,
|
||||
DefaultBufferSize,
|
||||
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||
|
||||
using (fileStream)
|
||||
{
|
||||
await fileStream.CopyToAsync(response.Body, DefaultBufferSize, cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an <see cref="ActionResult"/> that when executed will
|
||||
/// write a file as the response.
|
||||
/// </summary>
|
||||
public abstract class FileResult : ActionResult
|
||||
{
|
||||
private string _fileDownloadName;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="FileResult"/> instance with
|
||||
/// the provided <paramref name="contentType"/>.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The Content-Type header of the response.</param>
|
||||
protected FileResult([NotNull] string contentType)
|
||||
{
|
||||
ContentType = contentType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Content-Type header value that will be written to the response.
|
||||
/// </summary>
|
||||
public string ContentType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file name that will be used in the Content-Disposition header of the response.
|
||||
/// </summary>
|
||||
public string FileDownloadName
|
||||
{
|
||||
get { return _fileDownloadName ?? string.Empty; }
|
||||
set { _fileDownloadName = value; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task ExecuteResultAsync([NotNull] ActionContext context)
|
||||
{
|
||||
var response = context.HttpContext.Response;
|
||||
response.ContentType = ContentType;
|
||||
|
||||
if (!string.IsNullOrEmpty(FileDownloadName))
|
||||
{
|
||||
// From RFC 2183, Sec. 2.3:
|
||||
// The sender may want to suggest a filename to be used if the entity is
|
||||
// detached and stored in a separate file. If the receiving MUA writes
|
||||
// the entity to a file, the suggested filename should be used as a
|
||||
// basis for the actual filename, where possible.
|
||||
var headerValue = ContentDispositionUtil.GetHeaderValue(FileDownloadName);
|
||||
context.HttpContext.Response.Headers.Set("Content-Disposition", headerValue);
|
||||
}
|
||||
|
||||
// We aren't flowing the cancellation token appropiately, see
|
||||
// https://github.com/aspnet/Mvc/issues/743 for details.
|
||||
return WriteFileAsync(response, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the file to the response.
|
||||
/// </summary>
|
||||
/// <param name="response">
|
||||
/// The <see cref="HttpResponse"/> where the file will be written
|
||||
/// </param>
|
||||
/// <param name="cancellation">The <see cref="CancellationToken"/>to cancel the operation.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="Task"/> that will complete when the file has been written to the response.
|
||||
/// </returns>
|
||||
protected abstract Task WriteFileAsync(HttpResponse response, CancellationToken cancellation);
|
||||
|
||||
// This is a temporary implementation until we have the right abstractions in HttpAbstractions.
|
||||
internal static class ContentDispositionUtil
|
||||
{
|
||||
private const string HexDigits = "0123456789ABCDEF";
|
||||
|
||||
private static void AddByteToStringBuilder(byte b, StringBuilder builder)
|
||||
{
|
||||
builder.Append('%');
|
||||
|
||||
int i = b;
|
||||
AddHexDigitToStringBuilder(i >> 4, builder);
|
||||
AddHexDigitToStringBuilder(i % 16, builder);
|
||||
}
|
||||
|
||||
private static void AddHexDigitToStringBuilder(int digit, StringBuilder builder)
|
||||
{
|
||||
builder.Append(HexDigits[digit]);
|
||||
}
|
||||
|
||||
private static string CreateRfc2231HeaderValue(string filename)
|
||||
{
|
||||
var builder = new StringBuilder("attachment; filename*=UTF-8''");
|
||||
|
||||
var filenameBytes = Encoding.UTF8.GetBytes(filename);
|
||||
foreach (var b in filenameBytes)
|
||||
{
|
||||
if (IsByteValidHeaderValueCharacter(b))
|
||||
{
|
||||
builder.Append((char)b);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddByteToStringBuilder(b, builder);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public static string GetHeaderValue(string fileName)
|
||||
{
|
||||
// If fileName contains any Unicode characters, encode according
|
||||
// to RFC 2231 (with clarifications from RFC 5987)
|
||||
foreach (var c in fileName)
|
||||
{
|
||||
if ((int)c > 127)
|
||||
{
|
||||
return CreateRfc2231HeaderValue(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
return CreateNonUnicodeCharactersHeaderValue(fileName);
|
||||
}
|
||||
|
||||
private static string CreateNonUnicodeCharactersHeaderValue(string fileName)
|
||||
{
|
||||
var escapedFileName = EscapeFileName(fileName);
|
||||
return string.Format("attachment; filename={0}", escapedFileName);
|
||||
}
|
||||
|
||||
private static string EscapeFileName(string fileName)
|
||||
{
|
||||
var hasToBeQuoted = false;
|
||||
|
||||
// We can't break the loop earlier because we need to check the
|
||||
// whole name for \n, in which case we need to provide a special
|
||||
// encoding.
|
||||
for (var i = 0; i < fileName.Length; i++)
|
||||
{
|
||||
if (fileName[i] == '\n')
|
||||
{
|
||||
// See RFC 2047 for more details
|
||||
return GetRfc2047Base64EncodedWord(fileName);
|
||||
}
|
||||
|
||||
// Control characters = (octets 0 - 31) and DEL (127)
|
||||
if (char.IsControl(fileName[i]))
|
||||
{
|
||||
hasToBeQuoted = true;
|
||||
}
|
||||
|
||||
switch (fileName[i])
|
||||
{
|
||||
case '(':
|
||||
case ')':
|
||||
case '<':
|
||||
case '>':
|
||||
case '@':
|
||||
case ',':
|
||||
case ';':
|
||||
case ':':
|
||||
case '\\':
|
||||
case '/':
|
||||
case '[':
|
||||
case ']':
|
||||
case '?':
|
||||
case '=':
|
||||
case '{':
|
||||
case '}':
|
||||
case ' ':
|
||||
case '\t':
|
||||
case '"':
|
||||
hasToBeQuoted = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return hasToBeQuoted ? QuoteFileName(fileName) : fileName;
|
||||
}
|
||||
|
||||
private static string QuoteFileName(string fileName)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("\"");
|
||||
|
||||
for (var i = 0; i < fileName.Length; i++)
|
||||
{
|
||||
switch (fileName[i])
|
||||
{
|
||||
case '\\':
|
||||
// Escape \
|
||||
builder.Append("\\\\");
|
||||
break;
|
||||
case '"':
|
||||
// Escape "
|
||||
builder.Append("\\\"");
|
||||
break;
|
||||
default:
|
||||
builder.Append(fileName[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append("\"");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string GetRfc2047Base64EncodedWord(string fileName)
|
||||
{
|
||||
// See RFC 2047 for details. Section 8 for examples.
|
||||
const string charset = "utf-8";
|
||||
// B means Base64
|
||||
const string encoding = "B";
|
||||
|
||||
var fileNameBytes = Encoding.UTF8.GetBytes(fileName);
|
||||
var base64EncodedFileName = Convert.ToBase64String(fileNameBytes);
|
||||
|
||||
// Encoded words are defined as "=?{charset}?{encoding}?{encpoded value}?="
|
||||
return string.Format("\"=?{0}?{1}?{2}?=\"", charset, encoding, base64EncodedFileName);
|
||||
}
|
||||
|
||||
// Application of RFC 2231 Encoding to Hypertext Transfer Protocol (HTTP) Header Fields, sec. 3.2
|
||||
// http://greenbytes.de/tech/webdav/draft-reschke-rfc2231-in-http-latest.html
|
||||
private static bool IsByteValidHeaderValueCharacter(byte b)
|
||||
{
|
||||
if ((byte)'0' <= b && b <= (byte)'9')
|
||||
{
|
||||
return true; // is digit
|
||||
}
|
||||
if ((byte)'a' <= b && b <= (byte)'z')
|
||||
{
|
||||
return true; // lowercase letter
|
||||
}
|
||||
if ((byte)'A' <= b && b <= (byte)'Z')
|
||||
{
|
||||
return true; // uppercase letter
|
||||
}
|
||||
|
||||
switch (b)
|
||||
{
|
||||
case (byte)'-':
|
||||
case (byte)'.':
|
||||
case (byte)'_':
|
||||
case (byte)'~':
|
||||
case (byte)':':
|
||||
case (byte)'!':
|
||||
case (byte)'$':
|
||||
case (byte)'&':
|
||||
case (byte)'+':
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an <see cref="ActionResult"/> that when executed will
|
||||
/// write a file from a stream to the response.
|
||||
/// </summary>
|
||||
public class FileStreamResult : FileResult
|
||||
{
|
||||
// default buffer size as defined in BufferedStream type
|
||||
private const int BufferSize = 0x1000;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="FileStreamResult"/> instance with
|
||||
/// the provided <paramref name="fileStream"/> and the
|
||||
/// provided <paramref name="contentType"/>.
|
||||
/// </summary>
|
||||
/// <param name="fileStream">The stream with the file.</param>
|
||||
/// <param name="contentType">The Content-Type header of the response.</param>
|
||||
public FileStreamResult([NotNull] Stream fileStream, string contentType)
|
||||
: base(contentType)
|
||||
{
|
||||
FileStream = fileStream;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stream with the file that will be sent back as the response.
|
||||
/// </summary>
|
||||
public Stream FileStream { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected async override Task WriteFileAsync(HttpResponse response, CancellationToken cancellation)
|
||||
{
|
||||
var outputStream = response.Body;
|
||||
|
||||
using (FileStream)
|
||||
{
|
||||
await FileStream.CopyToAsync(outputStream, BufferSize, cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// 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.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -424,6 +425,90 @@ namespace Microsoft.AspNet.Mvc
|
|||
return new RedirectToRouteResult(Url, routeName, routeValues, permanent: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a file with the specified <paramref name="fileContents" /> as content and the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type.
|
||||
/// </summary>
|
||||
/// <param name="fileContents">The file contents.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
/// <returns>The created <see cref="FileContentResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual FileContentResult File(byte[] fileContents, string contentType)
|
||||
{
|
||||
return File(fileContents, contentType, fileDownloadName: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a file with the specified <paramref name="fileContents" /> as content, the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type and the
|
||||
/// specified <paramref name="fileDownloadName" /> as the suggested file name.
|
||||
/// </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>
|
||||
/// <returns>The created <see cref="FileContentResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual FileContentResult File(byte[] fileContents, string contentType, string fileDownloadName)
|
||||
{
|
||||
return new FileContentResult(fileContents, contentType) { FileDownloadName = fileDownloadName };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a file in the specified <paramref name="fileStream" /> with the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type.
|
||||
/// </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>
|
||||
/// <returns>The created <see cref="FileStreamResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual FileStreamResult File(Stream fileStream, string contentType)
|
||||
{
|
||||
return File(fileStream, contentType, fileDownloadName: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a file in the specified <paramref name="fileStream" /> with the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type and the
|
||||
/// specified <paramref name="fileDownloadName" /> as the suggested file name.
|
||||
/// </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>
|
||||
/// <returns>The created <see cref="FileStreamResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName)
|
||||
{
|
||||
return new FileStreamResult(fileStream, contentType) { FileDownloadName = fileDownloadName };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file specified by <paramref name="fileName" /> with the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The <see cref="Stream"/> with the contents of the file.</param>
|
||||
/// <param name="contentType">The Content-Type of the file.</param>
|
||||
/// <returns>The created <see cref="FilePathResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual FilePathResult File(string fileName, string contentType)
|
||||
{
|
||||
return File(fileName, contentType, fileDownloadName: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file specified by <paramref name="fileName" /> with the
|
||||
/// specified <paramref name="contentType" /> as the Content-Type and the
|
||||
/// specified <paramref name="fileDownloadName" /> as the suggested file name.
|
||||
/// </summary>
|
||||
/// <param name="fileName">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>
|
||||
/// <returns>The created <see cref="FilePathResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual FilePathResult File(string fileName, string contentType, string fileDownloadName)
|
||||
{
|
||||
return new FilePathResult(fileName, contentType) { FileDownloadName = fileDownloadName };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="HttpNotFoundResult"/> that produces a Not Found (404) response.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1482,6 +1482,22 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
return GetString("AttributeRoute_NullTemplateRepresentation");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// "The path to the file must be absolute: {0}"
|
||||
/// </summary>
|
||||
internal static string FileResult_InvalidPathType_RelativeOrVirtualPath
|
||||
{
|
||||
get { return GetString("FileResult_InvalidPathType_RelativeOrVirtualPath"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// "The path to the file must be absolute: {0}"
|
||||
/// </summary>
|
||||
internal static string FormatFileResult_InvalidPathType_RelativeOrVirtualPath(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("FileResult_InvalidPathType_RelativeOrVirtualPath"), p0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiple actions matched. The following actions matched route data and had all constraints satisfied:{0}{0}{1}
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -406,4 +406,8 @@
|
|||
<value>Multiple actions matched. The following actions matched route data and had all constraints satisfied:{0}{0}{1}</value>
|
||||
<comment>0 is the newline - 1 is a newline separate list of action display names</comment>
|
||||
</data>
|
||||
<data name="FileResult_InvalidPathType_RelativeOrVirtualPath" xml:space="preserve">
|
||||
<value>"The path to the file must be absolute: {0}"</value>
|
||||
<comment>{0} is the value for the provided path</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.PipelineCore;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public class FileContentResultTest
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsFileContents()
|
||||
{
|
||||
// Arrange
|
||||
var fileContents = new byte[0];
|
||||
|
||||
// Act
|
||||
var result = new FileContentResult(fileContents, "text/plain");
|
||||
|
||||
// Assert
|
||||
Assert.Same(fileContents, result.FileContents);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_CopiesBuffer_ToOutputStream()
|
||||
{
|
||||
// Arrange
|
||||
var buffer = new byte[] { 1, 2, 3, 4, 5 };
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
|
||||
var outStream = new MemoryStream();
|
||||
httpContext.Response.Body = outStream;
|
||||
|
||||
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
var result = new FileContentResult(buffer, "text/plain");
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(buffer, outStream.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.HttpFeature;
|
||||
using Microsoft.AspNet.PipelineCore;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public class FilePathResultTest
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsFileName()
|
||||
{
|
||||
// Arrange & Act
|
||||
var path = Path.GetFullPath("helllo.txt");
|
||||
var result = new FilePathResult(path, "text/plain");
|
||||
|
||||
// Act & Assert
|
||||
Assert.Equal(path, result.FileName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteResultAsync_FallsbackToStreamCopy_IfNoIHttpSendFilePresent()
|
||||
{
|
||||
// Arrange
|
||||
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
|
||||
var result = new FilePathResult(path, "text/plain");
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
|
||||
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(context);
|
||||
httpContext.Response.Body.Position = 0;
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(httpContext.Response.Body);
|
||||
var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
|
||||
Assert.Equal("FilePathResultTestFile contents", contents);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent()
|
||||
{
|
||||
// Arrange
|
||||
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
|
||||
var result = new FilePathResult(path, "text/plain");
|
||||
|
||||
var sendFileMock = new Mock<IHttpSendFileFeature>();
|
||||
sendFileMock
|
||||
.Setup(s => s.SendFileAsync(path, 0, null, CancellationToken.None))
|
||||
.Returns(Task.FromResult<int>(0));
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.SetFeature(sendFileMock.Object);
|
||||
|
||||
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(context);
|
||||
|
||||
// Assert
|
||||
sendFileMock.Verify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO;
|
||||
using System.Net.Mime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public class FileResultTest
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsContentType()
|
||||
{
|
||||
// Act
|
||||
var result = new EmptyFileResult("text/plain");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("text/plain", result.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ContentDispositionHeader_IsEncodedCorrectly()
|
||||
{
|
||||
// See comment in FileResult.cs detailing how the FileDownloadName should be encoded.
|
||||
|
||||
// Arrange
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
httpContext.SetupSet(c => c.Response.ContentType = "application/my-type").Verifiable();
|
||||
httpContext
|
||||
.Setup(c => c.Response.Headers.Set("Content-Disposition", @"attachment; filename=""some\\file"""))
|
||||
.Verifiable();
|
||||
|
||||
var actionContext = CreateActionContext(httpContext.Object);
|
||||
|
||||
var result = new EmptyFileResult("application/my-type")
|
||||
{
|
||||
FileDownloadName = @"some\file"
|
||||
};
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.WasWriteFileCalled);
|
||||
httpContext.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ContentDispositionHeader_IsEncodedCorrectly_ForUnicodeCharacters()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
httpContext.SetupSet(c => c.Response.ContentType = "application/my-type").Verifiable();
|
||||
httpContext
|
||||
.Setup(c => c.Response.Headers.Set(
|
||||
"Content-Disposition",
|
||||
@"attachment; filename*=UTF-8''ABCXYZabcxyz012789!%40%23$%25%5E&%2A%28%29-%3D_+.:~%CE%94"))
|
||||
.Verifiable();
|
||||
|
||||
var actionContext = CreateActionContext(httpContext.Object);
|
||||
|
||||
var result = new EmptyFileResult("application/my-type")
|
||||
{
|
||||
FileDownloadName = "ABCXYZabcxyz012789!@#$%^&*()-=_+.:~Δ"
|
||||
};
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.WasWriteFileCalled);
|
||||
httpContext.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteResultAsync_DoesNotSetContentDisposition_IfNotSpecified()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = new Mock<HttpContext>(MockBehavior.Strict);
|
||||
httpContext.SetupSet(c => c.Response.ContentType = "application/my-type").Verifiable();
|
||||
httpContext.Setup(c => c.Response.Body).Returns(Stream.Null);
|
||||
|
||||
var actionContext = CreateActionContext(httpContext.Object);
|
||||
|
||||
var result = new EmptyFileResult("application/my-type");
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.WasWriteFileCalled);
|
||||
httpContext.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteResultAsync_SetsContentDisposition_IfSpecified()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = new Mock<HttpContext>(MockBehavior.Strict);
|
||||
httpContext.SetupSet(c => c.Response.ContentType = "application/my-type").Verifiable();
|
||||
httpContext
|
||||
.Setup(c => c.Response.Headers.Set("Content-Disposition", "attachment; filename=filename.ext"))
|
||||
.Verifiable();
|
||||
|
||||
var actionContext = CreateActionContext(httpContext.Object);
|
||||
|
||||
var result = new EmptyFileResult("application/my-type")
|
||||
{
|
||||
FileDownloadName = "filename.ext"
|
||||
};
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.WasWriteFileCalled);
|
||||
httpContext.Verify();
|
||||
}
|
||||
|
||||
public static TheoryData<string, string> ContentDispositionData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<string, string>
|
||||
{
|
||||
// Non quoted values
|
||||
{ "09aAzZ", "attachment; filename=09aAzZ" },
|
||||
{ "a.b", "attachment; filename=a.b" },
|
||||
{ "#", "attachment; filename=#" },
|
||||
{ "-", "attachment; filename=-" },
|
||||
{ "_", "attachment; filename=_" },
|
||||
|
||||
// Values that need to be quoted
|
||||
{ ": :", "attachment; filename=\": :\"" },
|
||||
{ "~", "attachment; filename=~" },
|
||||
{ "$", "attachment; filename=$" },
|
||||
{ "&", "attachment; filename=&" },
|
||||
{ "+", "attachment; filename=+" },
|
||||
{ "(", "attachment; filename=\"(\"" },
|
||||
{ ")", "attachment; filename=\")\"" },
|
||||
{ "<", "attachment; filename=\"<\"" },
|
||||
{ ">", "attachment; filename=\">\"" },
|
||||
{ "@", "attachment; filename=\"@\"" },
|
||||
{ ",", "attachment; filename=\",\"" },
|
||||
{ ";", "attachment; filename=\";\"" },
|
||||
{ ":", "attachment; filename=\":\"" },
|
||||
{ "/", "attachment; filename=\"/\"" },
|
||||
{ "[", "attachment; filename=\"[\"" },
|
||||
{ "]", "attachment; filename=\"]\"" },
|
||||
{ "?", "attachment; filename=\"?\"" },
|
||||
{ "=", "attachment; filename=\"=\"" },
|
||||
{ "{", "attachment; filename=\"{\"" },
|
||||
{ "}", "attachment; filename=\"}\"" },
|
||||
{ " ", "attachment; filename=\" \"" },
|
||||
{ "a\tb", "attachment; filename=\"a\tb\"" },
|
||||
{ "a b", "attachment; filename=\"a b\"" },
|
||||
|
||||
// Values that need to be escaped
|
||||
{ "\"", "attachment; filename=\"\\\"\"" },
|
||||
{ "\\", "attachment; filename=\"\\\\\"" },
|
||||
|
||||
// Values that need to be specially encoded (Base64, see rfc2047)
|
||||
{ "a\nb", "attachment; filename=\"=?utf-8?B?YQpi?=\"" },
|
||||
|
||||
// Values with non unicode characters
|
||||
{ "résumé.txt", "attachment; filename*=UTF-8''r%C3%A9sum%C3%A9.txt" },
|
||||
{ "Δ", "attachment; filename*=UTF-8''%CE%94" },
|
||||
{ "Δ\t", "attachment; filename*=UTF-8''%CE%94%09" },
|
||||
{ "ABCXYZabcxyz012789!@#$%^&*()-=_+.:~Δ", @"attachment; filename*=UTF-8''ABCXYZabcxyz012789!%40%23$%25%5E&%2A%28%29-%3D_+.:~%CE%94" },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static TheoryData<string, string> ContentDispositionControlCharactersData
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<string, string>();
|
||||
for (var i = 0; i < 32; i++)
|
||||
{
|
||||
if (i == 10)
|
||||
{
|
||||
// skip \n as it has a special encoding
|
||||
continue;
|
||||
}
|
||||
|
||||
data.Add(char.ConvertFromUtf32(i), "attachment; filename=\"" + char.ConvertFromUtf32(i) + "\"");
|
||||
}
|
||||
|
||||
data.Add(char.ConvertFromUtf32(127), "attachment; filename=\"" + char.ConvertFromUtf32(127) + "\"");
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ContentDispositionData))]
|
||||
[MemberData(nameof(ContentDispositionControlCharactersData))]
|
||||
public void GetHeaderValue_Produces_Correct_ContentDisposition(string input, string expectedOutput)
|
||||
{
|
||||
// Arrange & Act
|
||||
var actual = FileResult.ContentDispositionUtil.GetHeaderValue(input);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedOutput, actual);
|
||||
}
|
||||
|
||||
private static ActionContext CreateActionContext(HttpContext context)
|
||||
{
|
||||
return new ActionContext(context, new RouteData(), new ActionDescriptor());
|
||||
}
|
||||
|
||||
private class EmptyFileResult : FileResult
|
||||
{
|
||||
public bool WasWriteFileCalled;
|
||||
|
||||
public EmptyFileResult()
|
||||
: this(MediaTypeNames.Application.Octet)
|
||||
{
|
||||
}
|
||||
|
||||
public EmptyFileResult(string contentType)
|
||||
: base(contentType)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task WriteFileAsync(HttpResponse response, CancellationToken cancellation)
|
||||
{
|
||||
WasWriteFileCalled = true;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.PipelineCore;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public class FileStreamResultTest
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsFileName()
|
||||
{
|
||||
// Arrange
|
||||
var stream = Stream.Null;
|
||||
|
||||
// Act
|
||||
var result = new FileStreamResult(stream, "text/plain");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(stream, result.FileStream);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_WritesResponse_InChunksOfFourKilobytes()
|
||||
{
|
||||
// Arrange
|
||||
var mockReadStream = new Mock<Stream>();
|
||||
mockReadStream.SetupSequence(s => s.ReadAsync(It.IsAny<byte[]>(), 0, 0x1000, CancellationToken.None))
|
||||
.Returns(Task.FromResult(0x1000))
|
||||
.Returns(Task.FromResult(0x500))
|
||||
.Returns(Task.FromResult(0));
|
||||
|
||||
var mockBodyStream = new Mock<Stream>();
|
||||
mockBodyStream
|
||||
.Setup(s => s.WriteAsync(It.IsAny<byte[]>(), 0, 0x1000, CancellationToken.None))
|
||||
.Returns(Task.FromResult(0));
|
||||
|
||||
mockBodyStream
|
||||
.Setup(s => s.WriteAsync(It.IsAny<byte[]>(), 0, 0x500, CancellationToken.None))
|
||||
.Returns(Task.FromResult(0));
|
||||
|
||||
var result = new FileStreamResult(mockReadStream.Object, "text/plain");
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Response.Body = mockBodyStream.Object;
|
||||
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
mockReadStream.Verify();
|
||||
mockBodyStream.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFileAsync_CopiesProvidedStream_ToOutputStream()
|
||||
{
|
||||
// Arrange
|
||||
// Generate an array of bytes with a predictable pattern
|
||||
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, 10, 11, 12, 13
|
||||
var originalBytes = Enumerable.Range(0, 0x1234)
|
||||
.Select(b => (byte)(b % 20)).ToArray();
|
||||
|
||||
var originalStream = new MemoryStream(originalBytes);
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var outStream = new MemoryStream();
|
||||
httpContext.Response.Body = outStream;
|
||||
|
||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
|
||||
var result = new FileStreamResult(originalStream, "text/plain");
|
||||
|
||||
// Act
|
||||
await result.ExecuteResultAsync(actionContext);
|
||||
|
||||
// Assert
|
||||
var outBytes = outStream.ToArray();
|
||||
Assert.True(originalBytes.SequenceEqual(outBytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,12 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.AspNet.Testing;
|
||||
|
|
@ -13,7 +15,6 @@ using Microsoft.AspNet.Testing;
|
|||
using Moq;
|
||||
#endif
|
||||
using Xunit;
|
||||
using Microsoft.AspNet.Http;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Test
|
||||
{
|
||||
|
|
@ -368,6 +369,109 @@ namespace Microsoft.AspNet.Mvc.Test
|
|||
Assert.Equal(expected, resultPermanent.RouteValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void File_WithContents()
|
||||
{
|
||||
// Arrange
|
||||
var controller = new Controller();
|
||||
var fileContents = new byte[0];
|
||||
|
||||
// Act
|
||||
var result = controller.File(fileContents, "someContentType");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Same(fileContents, result.FileContents);
|
||||
Assert.Equal("someContentType", result.ContentType);
|
||||
Assert.Equal(string.Empty, result.FileDownloadName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void File_WithContentsAndFileDownloadName()
|
||||
{
|
||||
// Arrange
|
||||
var controller = new Controller();
|
||||
var fileContents = new byte[0];
|
||||
|
||||
// Act
|
||||
var result = controller.File(fileContents, "someContentType", "someDownloadName");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Same(fileContents, result.FileContents);
|
||||
Assert.Equal("someContentType", result.ContentType);
|
||||
Assert.Equal("someDownloadName", result.FileDownloadName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void File_WithPath()
|
||||
{
|
||||
// Arrange
|
||||
var controller = new Controller();
|
||||
var path = Path.GetFullPath("somepath");
|
||||
|
||||
// Act
|
||||
var result = controller.File(path, "someContentType");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(path, result.FileName);
|
||||
Assert.Equal("someContentType", result.ContentType);
|
||||
Assert.Equal(string.Empty, result.FileDownloadName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void File_WithPathAndFileDownloadName()
|
||||
{
|
||||
// Arrange
|
||||
var controller = new Controller();
|
||||
var path = Path.GetFullPath("somepath");
|
||||
|
||||
// Act
|
||||
var result = controller.File(path, "someContentType", "someDownloadName");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(path, result.FileName);
|
||||
Assert.Equal("someContentType", result.ContentType);
|
||||
Assert.Equal("someDownloadName", result.FileDownloadName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void File_WithStream()
|
||||
{
|
||||
// Arrange
|
||||
var controller = new Controller();
|
||||
var fileStream = Stream.Null;
|
||||
|
||||
// Act
|
||||
var result = controller.File(fileStream, "someContentType");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Same(fileStream, result.FileStream);
|
||||
Assert.Equal("someContentType", result.ContentType);
|
||||
Assert.Equal(string.Empty, result.FileDownloadName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void File_WithStreamAndFileDownloadName()
|
||||
{
|
||||
// Arrange
|
||||
var controller = new Controller();
|
||||
var fileStream = Stream.Null;
|
||||
|
||||
// Act
|
||||
var result = controller.File(fileStream, "someContentType", "someDownloadName");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Same(fileStream, result.FileStream);
|
||||
Assert.Equal("someContentType", result.ContentType);
|
||||
Assert.Equal("someDownloadName", result.FileDownloadName);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void HttpNotFound_SetsStatusCode()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
FilePathResultTestFile contents
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Builder;
|
||||
using Microsoft.AspNet.TestHost;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||
{
|
||||
public class FileResultTests
|
||||
{
|
||||
private readonly IServiceProvider _services = TestHelper.CreateServices("FilesWebSite");
|
||||
private readonly Action<IApplicationBuilder> _app = new FilesWebSite.Startup().Configure;
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromDisk_CanBeEnabled_WithMiddleware()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/DownloadFiles/DowloadFromDisk");
|
||||
|
||||
// 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.NotNull(body);
|
||||
Assert.Equal("This is a sample text file", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromDisk_ReturnsFileWithFileName()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/DownloadFiles/DowloadFromDiskWithFileName");
|
||||
|
||||
// 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.NotNull(body);
|
||||
Assert.Equal("This is a sample text file", body);
|
||||
|
||||
var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
|
||||
Assert.NotNull(contentDisposition);
|
||||
Assert.Equal("attachment; filename=downloadName.txt", contentDisposition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromStream_ReturnsFile()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/DownloadFiles/DowloadFromStream");
|
||||
|
||||
// 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.NotNull(body);
|
||||
Assert.Equal("This is sample text from a stream", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromStream_ReturnsFileWithFileName()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/DownloadFiles/DowloadFromStreamWithFileName");
|
||||
|
||||
// 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.NotNull(body);
|
||||
Assert.Equal("This is sample text from a stream", body);
|
||||
|
||||
var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
|
||||
Assert.NotNull(contentDisposition);
|
||||
Assert.Equal("attachment; filename=downloadName.txt", contentDisposition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromBinaryData_ReturnsFile()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/DownloadFiles/DowloadFromBinaryData");
|
||||
|
||||
// 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.NotNull(body);
|
||||
Assert.Equal("This is a sample text from a binary array", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileFromBinaryData_ReturnsFileWithFileName()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/DownloadFiles/DowloadFromBinaryDataWithFileName");
|
||||
|
||||
// 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.NotNull(body);
|
||||
Assert.Equal("This is a sample text from a binary array", body);
|
||||
|
||||
var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
|
||||
Assert.NotNull(contentDisposition);
|
||||
Assert.Equal("attachment; filename=downloadName.txt", contentDisposition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
"BasicWebSite": "",
|
||||
"CompositeViewEngine": "",
|
||||
"ConnegWebsite": "",
|
||||
"FilesWebSite": "",
|
||||
"FiltersWebSite": "",
|
||||
"FormatterWebSite": "",
|
||||
"InlineConstraintsWebSite": "",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.AspNet.Mvc;
|
||||
using Microsoft.Framework.Runtime;
|
||||
|
||||
namespace FilesWebSite
|
||||
{
|
||||
public class DownloadFilesController : Controller
|
||||
{
|
||||
private readonly IApplicationEnvironment _appEnvironment;
|
||||
|
||||
public DownloadFilesController(IApplicationEnvironment appEnvironment)
|
||||
{
|
||||
_appEnvironment = appEnvironment;
|
||||
}
|
||||
|
||||
public IActionResult DowloadFromDisk()
|
||||
{
|
||||
var path = Path.Combine(_appEnvironment.ApplicationBasePath, "sample.txt");
|
||||
return File(path, "text/plain");
|
||||
}
|
||||
|
||||
public IActionResult DowloadFromDiskWithFileName()
|
||||
{
|
||||
var path = Path.Combine(_appEnvironment.ApplicationBasePath, "sample.txt");
|
||||
return File(path, "text/plain", "downloadName.txt");
|
||||
}
|
||||
|
||||
public IActionResult DowloadFromStream()
|
||||
{
|
||||
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);
|
||||
|
||||
return File(stream, "text/plain");
|
||||
}
|
||||
|
||||
public IActionResult DowloadFromStreamWithFileName()
|
||||
{
|
||||
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);
|
||||
|
||||
return File(stream, "text/plain", "downloadName.txt");
|
||||
}
|
||||
|
||||
public IActionResult DowloadFromBinaryData()
|
||||
{
|
||||
var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");
|
||||
return File(data, "text/plain");
|
||||
}
|
||||
|
||||
public IActionResult DowloadFromBinaryDataWithFileName()
|
||||
{
|
||||
var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");
|
||||
return File(data, "text/plain", "downloadName.txt");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">12.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>0ef9860b-10d7-452f-b0f4-a405b88bebb3</ProjectGuid>
|
||||
<OutputType>Web</OutputType>
|
||||
<RootNamespace>FilesWebSite</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="$(OutputType) == 'Console'">
|
||||
<DebuggerFlavor>ConsoleDebugger</DebuggerFlavor>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="$(OutputType) == 'Web'">
|
||||
<DebuggerFlavor>WebDebugger</DebuggerFlavor>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" Label="Configuration">
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<DevelopmentServerPort>18002</DevelopmentServerPort>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Framework.Runtime;
|
||||
using Microsoft.AspNet.Builder;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.HttpFeature;
|
||||
|
||||
namespace FilesWebSite
|
||||
{
|
||||
public class SendFileMiddleware
|
||||
{
|
||||
private const int DefaultBufferSize = 0x1000;
|
||||
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public SendFileMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
var environment = (IApplicationEnvironment)context.RequestServices.GetService(typeof(IApplicationEnvironment));
|
||||
|
||||
if (context.GetFeature<IHttpSendFileFeature>() == null)
|
||||
{
|
||||
var sendFile = new SendFileFallBack(context.Response.Body, environment.ApplicationBasePath);
|
||||
context.SetFeature<IHttpSendFileFeature>(sendFile);
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
|
||||
private class SendFileFallBack : IHttpSendFileFeature
|
||||
{
|
||||
private readonly string _appBasePath;
|
||||
private Stream _responseStream;
|
||||
|
||||
public SendFileFallBack(Stream responseStream, string appBasePath)
|
||||
{
|
||||
_responseStream = responseStream;
|
||||
_appBasePath = appBasePath;
|
||||
}
|
||||
|
||||
public async Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
|
||||
{
|
||||
using (var stream = new FileStream(Path.Combine(_appBasePath, path), FileMode.Open))
|
||||
{
|
||||
length = length ?? stream.Length - offset;
|
||||
|
||||
stream.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
var bufferSize = length < DefaultBufferSize ? length.Value : DefaultBufferSize;
|
||||
var buffer = new byte[bufferSize];
|
||||
var bytesRead = 0;
|
||||
|
||||
do
|
||||
{
|
||||
var bytesToRead = bufferSize < length ? bufferSize : length;
|
||||
bytesRead = await stream.ReadAsync(buffer, 0, (int)bytesToRead);
|
||||
length = length - bytesRead;
|
||||
|
||||
await _responseStream.WriteAsync(buffer, 0, bytesRead);
|
||||
} while (bytesRead > 0 && length > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNet.Builder;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
|
||||
namespace FilesWebSite
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
var configuration = app.GetTestConfiguration();
|
||||
|
||||
app.UseServices(services =>
|
||||
{
|
||||
services.AddMvc(configuration);
|
||||
});
|
||||
|
||||
app.UseMiddleware<SendFileMiddleware>();
|
||||
|
||||
app.UseMvc(routes =>
|
||||
{
|
||||
routes.MapRoute(name: null, template: "{controller}/{action}", defaults: null);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"Microsoft.AspNet.Mvc": "",
|
||||
"Microsoft.AspNet.Mvc.TestConfiguration": "",
|
||||
"Microsoft.AspNet.Server.IIS": "1.0.0-*"
|
||||
},
|
||||
"frameworks": {
|
||||
"aspnet50": { },
|
||||
"aspnetcore50": { }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
This is a sample text file
|
||||
Loading…
Reference in New Issue