[Fixes #2086] FilePathResult WriteFileAsync uses SendFile feature incorrectly
This commit is contained in:
parent
f06007d428
commit
3d247ec028
|
|
@ -80,27 +80,23 @@ namespace Microsoft.AspNet.Mvc
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task WriteFileAsync(HttpResponse response, CancellationToken cancellation)
|
protected override Task WriteFileAsync(HttpResponse response, CancellationToken cancellation)
|
||||||
{
|
{
|
||||||
var sendFile = response.HttpContext.GetFeature<IHttpSendFileFeature>();
|
|
||||||
|
|
||||||
var fileProvider = GetFileProvider(response.HttpContext.RequestServices);
|
var fileProvider = GetFileProvider(response.HttpContext.RequestServices);
|
||||||
|
|
||||||
var filePath = ResolveFilePath(fileProvider);
|
var resolveFilePathResult = ResolveFilePath(fileProvider);
|
||||||
|
|
||||||
if (sendFile != null)
|
if (resolveFilePathResult.PhysicalFilePath != null)
|
||||||
{
|
{
|
||||||
return sendFile.SendFileAsync(
|
return CopyPhysicalFileToResponseAsync(response, resolveFilePathResult.PhysicalFilePath, cancellation);
|
||||||
filePath,
|
|
||||||
offset: 0,
|
|
||||||
length: null,
|
|
||||||
cancellation: cancellation);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return CopyStreamToResponse(filePath, response, cancellation);
|
// Example: An embedded resource
|
||||||
|
var sourceStream = resolveFilePathResult.FileInfo.CreateReadStream();
|
||||||
|
return CopyStreamToResponseAsync(sourceStream, response, cancellation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal string ResolveFilePath(IFileProvider fileProvider)
|
internal ResolveFilePathResult ResolveFilePath(IFileProvider fileProvider)
|
||||||
{
|
{
|
||||||
// Let the file system try to get the file and if it can't,
|
// Let the file system try to get the file and if it can't,
|
||||||
// fallback to trying the path directly unless the path starts with '/'.
|
// fallback to trying the path directly unless the path starts with '/'.
|
||||||
|
|
@ -109,12 +105,17 @@ namespace Microsoft.AspNet.Mvc
|
||||||
|
|
||||||
var path = NormalizePath(FileName);
|
var path = NormalizePath(FileName);
|
||||||
|
|
||||||
|
// Note that we cannot use 'File.Exists' check as the file could be a non-physical
|
||||||
|
// file. For example, an embedded resource.
|
||||||
if (IsPathRooted(path))
|
if (IsPathRooted(path))
|
||||||
{
|
{
|
||||||
// The path is absolute
|
// The path is absolute
|
||||||
// C:\...\file.ext
|
// C:\...\file.ext
|
||||||
// C:/.../file.ext
|
// C:/.../file.ext
|
||||||
return path;
|
return new ResolveFilePathResult()
|
||||||
|
{
|
||||||
|
PhysicalFilePath = path
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileInfo = fileProvider.GetFileInfo(path);
|
var fileInfo = fileProvider.GetFileInfo(path);
|
||||||
|
|
@ -122,7 +123,12 @@ namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
// The path is relative and IFileProvider found the file, so return the full
|
// The path is relative and IFileProvider found the file, so return the full
|
||||||
// path.
|
// path.
|
||||||
return fileInfo.PhysicalPath;
|
return new ResolveFilePathResult()
|
||||||
|
{
|
||||||
|
// Note that physical path could be null in case of non-disk file (ex: embedded resource)
|
||||||
|
PhysicalFilePath = fileInfo.PhysicalPath,
|
||||||
|
FileInfo = fileInfo
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// We are absolutely sure the path is relative, and couldn't find the file
|
// We are absolutely sure the path is relative, and couldn't find the file
|
||||||
|
|
@ -208,22 +214,50 @@ namespace Microsoft.AspNet.Mvc
|
||||||
return FileProvider;
|
return FileProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task CopyStreamToResponse(
|
private Task CopyPhysicalFileToResponseAsync(
|
||||||
string fileName,
|
HttpResponse response,
|
||||||
|
string physicalFilePath,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var sendFile = response.HttpContext.GetFeature<IHttpSendFileFeature>();
|
||||||
|
if (sendFile != null)
|
||||||
|
{
|
||||||
|
return sendFile.SendFileAsync(
|
||||||
|
physicalFilePath,
|
||||||
|
offset: 0,
|
||||||
|
length: null,
|
||||||
|
cancellation: cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var fileStream = new FileStream(
|
||||||
|
physicalFilePath,
|
||||||
|
FileMode.Open,
|
||||||
|
FileAccess.Read,
|
||||||
|
FileShare.ReadWrite,
|
||||||
|
DefaultBufferSize,
|
||||||
|
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||||
|
|
||||||
|
return CopyStreamToResponseAsync(fileStream, response, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task CopyStreamToResponseAsync(
|
||||||
|
Stream sourceStream,
|
||||||
HttpResponse response,
|
HttpResponse response,
|
||||||
CancellationToken cancellation)
|
CancellationToken cancellation)
|
||||||
{
|
{
|
||||||
var fileStream = new FileStream(
|
using (sourceStream)
|
||||||
fileName, FileMode.Open,
|
|
||||||
FileAccess.Read,
|
|
||||||
FileShare.ReadWrite,
|
|
||||||
DefaultBufferSize,
|
|
||||||
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
|
||||||
|
|
||||||
using (fileStream)
|
|
||||||
{
|
{
|
||||||
await fileStream.CopyToAsync(response.Body, DefaultBufferSize, cancellation);
|
await sourceStream.CopyToAsync(response.Body, DefaultBufferSize, cancellation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
internal class ResolveFilePathResult
|
||||||
|
{
|
||||||
|
public IFileInfo FileInfo { get; set; }
|
||||||
|
|
||||||
|
public string PhysicalFilePath { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ using Microsoft.AspNet.Routing;
|
||||||
using Microsoft.Framework.DependencyInjection;
|
using Microsoft.Framework.DependencyInjection;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
|
|
@ -171,6 +172,35 @@ namespace Microsoft.AspNet.Mvc
|
||||||
Assert.Equal("FilePathResultTestFile contents", contents);
|
Assert.Equal("FilePathResultTestFile contents", contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteResultAsync_WorksWithNonDiskBasedFiles()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.Response.Body = new MemoryStream();
|
||||||
|
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||||
|
var expectedData = "This is an embedded resource";
|
||||||
|
var sourceStream = new MemoryStream(Encoding.UTF8.GetBytes(expectedData));
|
||||||
|
var nonDiskFileInfo = new Mock<IFileInfo>();
|
||||||
|
nonDiskFileInfo.SetupGet(fi => fi.Exists).Returns(true);
|
||||||
|
nonDiskFileInfo.SetupGet(fi => fi.PhysicalPath).Returns(() => null); // set null to indicate non-disk file
|
||||||
|
nonDiskFileInfo.Setup(fi => fi.CreateReadStream()).Returns(sourceStream);
|
||||||
|
var nonDiskFileProvider = new Mock<IFileProvider>();
|
||||||
|
nonDiskFileProvider.Setup(fp => fp.GetFileInfo(It.IsAny<string>())).Returns(nonDiskFileInfo.Object);
|
||||||
|
var filePathResult = new FilePathResult("/SampleEmbeddedFile.txt")
|
||||||
|
{
|
||||||
|
FileProvider = nonDiskFileProvider.Object
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await filePathResult.ExecuteResultAsync(actionContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
httpContext.Response.Body.Position = 0;
|
||||||
|
var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
|
||||||
|
Assert.Equal(expectedData, contents);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
// Root of the file system, forward slash and back slash
|
// Root of the file system, forward slash and back slash
|
||||||
[InlineData("FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")]
|
[InlineData("FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")]
|
||||||
|
|
@ -202,18 +232,20 @@ namespace Microsoft.AspNet.Mvc
|
||||||
public void GetFilePath_Resolves_RelativePaths(string path, string relativePathToFile)
|
public void GetFilePath_Resolves_RelativePaths(string path, string relativePathToFile)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var fileProvider = new PhysicalFileProvider(Path.GetFullPath("./TestFiles"));
|
|
||||||
var expectedPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePathToFile));
|
var expectedPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePathToFile));
|
||||||
|
var fileProvider = new PhysicalFileProvider(Path.GetFullPath("./TestFiles"));
|
||||||
var filePathResult = new FilePathResult(path, "text/plain")
|
var filePathResult = new FilePathResult(path, "text/plain")
|
||||||
{
|
{
|
||||||
FileProvider = fileProvider,
|
FileProvider = fileProvider,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = filePathResult.ResolveFilePath(fileProvider);
|
var resolveFilePathResult = filePathResult.ResolveFilePath(fileProvider);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(expectedPath, result);
|
Assert.NotNull(resolveFilePathResult);
|
||||||
|
Assert.NotNull(resolveFilePathResult.FileInfo);
|
||||||
|
Assert.Equal(expectedPath, resolveFilePathResult.PhysicalFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
|
||||||
|
|
@ -151,5 +151,31 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
Assert.NotNull(contentDisposition);
|
Assert.NotNull(contentDisposition);
|
||||||
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
|
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestHelper.CreateServer(_app, SiteName);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var expectedBody = "Sample text file as embedded resource.";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync("http://localhost/EmbeddedFiles/DownloadFileWithFileName");
|
||||||
|
|
||||||
|
// 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(expectedBody, body);
|
||||||
|
|
||||||
|
var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
|
||||||
|
Assert.NotNull(contentDisposition);
|
||||||
|
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
// 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 Microsoft.AspNet.FileProviders;
|
||||||
|
using Microsoft.AspNet.Mvc;
|
||||||
|
|
||||||
|
namespace FilesWebSite
|
||||||
|
{
|
||||||
|
public class EmbeddedFilesController : Controller
|
||||||
|
{
|
||||||
|
public IActionResult DownloadFileWithFileName()
|
||||||
|
{
|
||||||
|
return new FilePathResult("/Greetings.txt", "text/plain")
|
||||||
|
{
|
||||||
|
FileProvider = new EmbeddedFileProvider(GetType().Assembly, "EmbeddedResources"),
|
||||||
|
FileDownloadName = "downloadName.txt"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Sample text file as embedded resource.
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:5001",
|
"web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:5001",
|
||||||
"kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5000"
|
"kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5000"
|
||||||
},
|
},
|
||||||
|
"resources": "EmbeddedResources/**",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Kestrel": "1.0.0-*",
|
"Kestrel": "1.0.0-*",
|
||||||
"Microsoft.AspNet.Mvc": "6.0.0-*",
|
"Microsoft.AspNet.Mvc": "6.0.0-*",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue