[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
|
EndProject
|
||||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ReflectedModelWebSite", "test\WebSites\ReflectedModelWebSite\ReflectedModelWebSite.kproj", "{C2EF54F8-8886-4260-A322-44F76245F95D}"
|
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ReflectedModelWebSite", "test\WebSites\ReflectedModelWebSite\ReflectedModelWebSite.kproj", "{C2EF54F8-8886-4260-A322-44F76245F95D}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "FilesWebSite", "test\WebSites\FilesWebSite\FilesWebSite.kproj", "{0EF9860B-10D7-452F-B0F4-A405B88BEBB3}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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.ActiveCfg = Release|Any CPU
|
||||||
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|Mixed Platforms.Build.0 = 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
|
{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.ActiveCfg = Debug|Any CPU
|
||||||
{61061528-071E-424E-965A-07BCC2F02672}.Debug|Any CPU.Build.0 = 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
|
{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}
|
{1976AC4A-FEA4-4587-A158-D9F79736D2B6} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
||||||
{96107AC0-18E2-474D-BAB4-2FFF2185FBCD} = {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}
|
{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}
|
{61061528-071E-424E-965A-07BCC2F02672} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
||||||
{C2EF54F8-8886-4260-A322-44F76245F95D} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
{C2EF54F8-8886-4260-A322-44F76245F95D} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,16 @@ namespace MvcSample.Web
|
||||||
return HttpNotFound();
|
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()
|
public bool IsDefaultNameSpace()
|
||||||
{
|
{
|
||||||
var namespaceToken = ActionContext.RouteData.DataTokens["NameSpace"] as string;
|
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.
|
// 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.Security.Principal;
|
using System.Security.Principal;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -424,6 +425,90 @@ namespace Microsoft.AspNet.Mvc
|
||||||
return new RedirectToRouteResult(Url, routeName, routeValues, permanent: true);
|
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>
|
/// <summary>
|
||||||
/// Creates an <see cref="HttpNotFoundResult"/> that produces a Not Found (404) response.
|
/// Creates an <see cref="HttpNotFoundResult"/> that produces a Not Found (404) response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -1482,6 +1482,22 @@ namespace Microsoft.AspNet.Mvc.Core
|
||||||
return GetString("AttributeRoute_NullTemplateRepresentation");
|
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>
|
/// <summary>
|
||||||
/// Multiple actions matched. The following actions matched route data and had all constraints satisfied:{0}{0}{1}
|
/// Multiple actions matched. The following actions matched route data and had all constraints satisfied:{0}{0}{1}
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -406,4 +406,8 @@
|
||||||
<value>Multiple actions matched. The following actions matched route data and had all constraints satisfied:{0}{0}{1}</value>
|
<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>
|
<comment>0 is the newline - 1 is a newline separate list of action display names</comment>
|
||||||
</data>
|
</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>
|
</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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using Microsoft.AspNet.Http;
|
||||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||||
using Microsoft.AspNet.Routing;
|
using Microsoft.AspNet.Routing;
|
||||||
using Microsoft.AspNet.Testing;
|
using Microsoft.AspNet.Testing;
|
||||||
|
|
@ -13,7 +15,6 @@ using Microsoft.AspNet.Testing;
|
||||||
using Moq;
|
using Moq;
|
||||||
#endif
|
#endif
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Microsoft.AspNet.Http;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Test
|
namespace Microsoft.AspNet.Mvc.Test
|
||||||
{
|
{
|
||||||
|
|
@ -368,6 +369,109 @@ namespace Microsoft.AspNet.Mvc.Test
|
||||||
Assert.Equal(expected, resultPermanent.RouteValues);
|
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]
|
[Fact]
|
||||||
public void HttpNotFound_SetsStatusCode()
|
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": "",
|
"BasicWebSite": "",
|
||||||
"CompositeViewEngine": "",
|
"CompositeViewEngine": "",
|
||||||
"ConnegWebsite": "",
|
"ConnegWebsite": "",
|
||||||
|
"FilesWebSite": "",
|
||||||
"FiltersWebSite": "",
|
"FiltersWebSite": "",
|
||||||
"FormatterWebSite": "",
|
"FormatterWebSite": "",
|
||||||
"InlineConstraintsWebSite": "",
|
"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