[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:
jacalvar 2014-09-25 23:50:06 -07:00
parent 2f00fc6121
commit a2023d35ee
24 changed files with 1498 additions and 2 deletions

13
Mvc.sln
View File

@ -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

View File

@ -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;

View File

@ -0,0 +1 @@
This is a sample file returned from a controller

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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());
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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));
}
}
}

View File

@ -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()
{

View File

@ -0,0 +1 @@
FilePathResultTestFile contents

View File

@ -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);
}
}
}

View File

@ -10,6 +10,7 @@
"BasicWebSite": "",
"CompositeViewEngine": "",
"ConnegWebsite": "",
"FilesWebSite": "",
"FiltersWebSite": "",
"FormatterWebSite": "",
"InlineConstraintsWebSite": "",

View File

@ -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");
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}
}
}

View File

@ -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);
});
}
}
}

View File

@ -0,0 +1,11 @@
{
"dependencies": {
"Microsoft.AspNet.Mvc": "",
"Microsoft.AspNet.Mvc.TestConfiguration": "",
"Microsoft.AspNet.Server.IIS": "1.0.0-*"
},
"frameworks": {
"aspnet50": { },
"aspnetcore50": { }
}
}

View File

@ -0,0 +1 @@
This is a sample text file