From ddc74e5396c5a09e950c5d0029bc23d452bc7783 Mon Sep 17 00:00:00 2001 From: sornaks Date: Fri, 21 Aug 2015 13:40:42 -0700 Subject: [PATCH] Issue #2727 - Introducing PhysicalFilePathResult and VirtualFilePathResult instead of FilePathResult to handle app and physical file system paths separately. --- .../FilePathResult.cs | 265 --------- .../PhysicalFileProviderResult.cs | 112 ++++ .../VirtualFileProviderResult.cs | 139 +++++ .../Controller.cs | 49 +- .../FilePathResultTest.cs | 517 ------------------ .../PhysicalFileProviderResultTest.cs | 207 +++++++ .../TestFiles/FilePathResultTestFile.txt | 1 - .../FilePathResultTestFile_ASCII.txt | 1 - .../TestFiles/SubFolder/SubFolderTestFile.txt | 1 - .../VirtualFileProviderResultTest.cs | 326 +++++++++++ .../Controllers/DownloadFilesController.cs | 4 +- .../Controllers/EmbeddedFilesController.cs | 2 +- 12 files changed, 826 insertions(+), 798 deletions(-) delete mode 100644 src/Microsoft.AspNet.Mvc.Core/FilePathResult.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/PhysicalFileProviderResult.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/VirtualFileProviderResult.cs delete mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/FilePathResultTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/PhysicalFileProviderResultTest.cs delete mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/FilePathResultTestFile.txt delete mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/FilePathResultTestFile_ASCII.txt delete mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/SubFolder/SubFolderTestFile.txt create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/VirtualFileProviderResultTest.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/FilePathResult.cs b/src/Microsoft.AspNet.Mvc.Core/FilePathResult.cs deleted file mode 100644 index f3bddc4855..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/FilePathResult.cs +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNet.FileProviders; -using Microsoft.AspNet.Hosting; -using Microsoft.AspNet.Http; -using Microsoft.AspNet.Http.Features; -using Microsoft.AspNet.Mvc.Core; -using Microsoft.Framework.DependencyInjection; -using Microsoft.Framework.Internal; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.AspNet.Mvc -{ - /// - /// An that when executed will - /// write a file from disk to the response using mechanisms provided - /// by the host. - /// - public class FilePathResult : FileResult - { - private const int DefaultBufferSize = 0x1000; - - private string _fileName; - - /// - /// Creates a new instance with - /// the provided and the - /// provided . - /// - /// The path to the file. The path must be an absolute - /// path. Relative and virtual paths are not supported. - /// The Content-Type header of the response. - public FilePathResult([NotNull] string fileName, [NotNull] string contentType) - : this(fileName, new MediaTypeHeaderValue(contentType)) - { - } - - /// - /// Creates a new instance with - /// the provided and the - /// provided . - /// - /// The path to the file. The path must be an absolute - /// path. Relative and virtual paths are not supported. - /// The Content-Type header of the response. - public FilePathResult([NotNull] string fileName, [NotNull] MediaTypeHeaderValue contentType) - : base(contentType) - { - FileName = fileName; - } - - /// - /// Gets or sets the path to the file that will be sent back as the response. - /// - public string FileName - { - get - { - return _fileName; - } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - _fileName = value; - } - } - - /// - /// Gets or sets the used to resolve paths. - /// - public IFileProvider FileProvider { get; set; } - - /// - protected override Task WriteFileAsync(HttpResponse response, CancellationToken cancellation) - { - var fileProvider = GetFileProvider(response.HttpContext.RequestServices); - - var resolveFilePathResult = ResolveFilePath(fileProvider); - - if (resolveFilePathResult.PhysicalFilePath != null) - { - return CopyPhysicalFileToResponseAsync(response, resolveFilePathResult.PhysicalFilePath, cancellation); - } - else - { - // Example: An embedded resource - var sourceStream = resolveFilePathResult.FileInfo.CreateReadStream(); - return CopyStreamToResponseAsync(sourceStream, response, cancellation); - } - } - - internal ResolveFilePathResult ResolveFilePath(IFileProvider fileProvider) - { - // 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 '/'. - // In that case we consider it relative and won't try to resolve it as - // a full path - - 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)) - { - // The path is absolute - // C:\...\file.ext - // C:/.../file.ext - return new ResolveFilePathResult() - { - PhysicalFilePath = path - }; - } - - var fileInfo = fileProvider.GetFileInfo(path); - if (fileInfo.Exists) - { - // The path is relative and IFileProvider found the file, so return the full - // path. - 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 - // on the file system. - var message = Resources.FormatFileResult_InvalidPath(path); - throw new FileNotFoundException(message, path); - } - - /// - /// Creates a normalized representation of the given . The default - /// implementation doesn't support files with '\' in the file name and treats the '\' as - /// a directory separator. The default implementation will convert all the '\' into '/' - /// and will remove leading '~' characters. - /// - /// The path to normalize. - /// The normalized path. - // Internal for unit testing purposes only - protected internal virtual string NormalizePath([NotNull] string path) - { - // Unix systems support '\' as part of the file name. So '\' is not - // a valid directory separator in those systems. Here we make the conscious - // choice of replacing '\' for '/' which means that file names with '\' will - // not be supported. - - if (path.StartsWith("~/", StringComparison.Ordinal)) - { - // We don't support virtual paths for now, so we just treat them as relative - // paths. - return path.Substring(1).Replace('\\', '/'); - } - - if (path.StartsWith("~\\", StringComparison.Ordinal)) - { - // ~\ is not a valid virtual path, and we don't want to replace '\' with '/' as it - // obfuscates the error, so just return the original path and throw at a later point - // when we can't find the file. - return path; - } - - return path.Replace('\\', '/'); - } - - /// - /// Determines if the provided path is absolute or relative. The default implementation considers - /// paths starting with '/' to be relative. - /// - /// The path to examine. - /// True if the path is absolute. - // Internal for unit testing purposes only - protected internal virtual bool IsPathRooted([NotNull] string path) - { - // We consider paths to be rooted if they start with '<>:' and do - // not start with '\' or '/'. In those cases, even that the paths are 'traditionally' - // rooted, we consider them to be relative. - // In Unix rooted paths start with '/' which is not supported by this action result - // by default. - - return Path.IsPathRooted(path) && (IsNetworkPath(path) || !StartsWithForwardOrBackSlash(path)); - } - - private static bool StartsWithForwardOrBackSlash(string path) - { - return path.StartsWith("/", StringComparison.Ordinal) || - path.StartsWith("\\", StringComparison.Ordinal); - } - - private static bool IsNetworkPath(string path) - { - return path.StartsWith("//", StringComparison.Ordinal) || - path.StartsWith("\\\\", StringComparison.Ordinal); - } - - private IFileProvider GetFileProvider(IServiceProvider requestServices) - { - if (FileProvider != null) - { - return FileProvider; - } - - var hostingEnvironment = requestServices.GetService(); - FileProvider = hostingEnvironment.WebRootFileProvider; - - return FileProvider; - } - - private Task CopyPhysicalFileToResponseAsync( - HttpResponse response, - string physicalFilePath, - CancellationToken cancellationToken) - { - var sendFile = response.HttpContext.GetFeature(); - 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, - CancellationToken cancellation) - { - using (sourceStream) - { - await sourceStream.CopyToAsync(response.Body, DefaultBufferSize, cancellation); - } - } - } - - internal class ResolveFilePathResult - { - public IFileInfo FileInfo { get; set; } - - public string PhysicalFilePath { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/PhysicalFileProviderResult.cs b/src/Microsoft.AspNet.Mvc.Core/PhysicalFileProviderResult.cs new file mode 100644 index 0000000000..224758a97c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/PhysicalFileProviderResult.cs @@ -0,0 +1,112 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Features; +using Microsoft.AspNet.Mvc.Core; +using Microsoft.Framework.Internal; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// A on execution will write a file from disk to the response + /// using mechanisms provided by the host. + /// + public class PhysicalFileProviderResult : FileResult + { + private const int DefaultBufferSize = 0x1000; + private string _fileName; + + /// + /// Creates a new instance with + /// the provided and the provided . + /// + /// The path to the file. The path must be an absolute path. + /// The Content-Type header of the response. + public PhysicalFileProviderResult([NotNull] string fileName, [NotNull] string contentType) + : this(fileName, new MediaTypeHeaderValue(contentType)) + { + } + + /// + /// Creates a new instance with + /// the provided and the provided . + /// + /// The path to the file. The path must be an absolute path. + /// The Content-Type header of the response. + public PhysicalFileProviderResult([NotNull] string fileName, [NotNull] MediaTypeHeaderValue contentType) + : base(contentType) + { + FileName = fileName; + } + + /// + /// Gets or sets the path to the file that will be sent back as the response. + /// + public string FileName + { + get + { + return _fileName; + } + + [param: NotNull] + set + { + _fileName = value; + } + } + + /// + protected override async Task WriteFileAsync(HttpResponse response, CancellationToken cancellation) + { + if (!Path.IsPathRooted(FileName)) + { + throw new FileNotFoundException(Resources.FormatFileResult_InvalidPath(FileName), FileName); + } + + var sendFile = response.HttpContext.GetFeature(); + if (sendFile != null) + { + await sendFile.SendFileAsync( + FileName, + offset: 0, + length: null, + cancellation: cancellation); + + return; + } + else + { + var fileStream = GetFileStream(FileName); + + using (fileStream) + { + await fileStream.CopyToAsync(response.Body, DefaultBufferSize, cancellation); + } + + return; + } + } + + /// + /// Returns for the specified . + /// + /// The path for which the is needed. + /// for the specified . + protected virtual Stream GetFileStream([NotNull]string path) + { + return new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + DefaultBufferSize, + FileOptions.Asynchronous | FileOptions.SequentialScan); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/VirtualFileProviderResult.cs b/src/Microsoft.AspNet.Mvc.Core/VirtualFileProviderResult.cs new file mode 100644 index 0000000000..c2cccd6e8a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/VirtualFileProviderResult.cs @@ -0,0 +1,139 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.FileProviders; +using Microsoft.AspNet.Hosting; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Features; +using Microsoft.AspNet.Mvc.Core; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Internal; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// A that on execution writes the file specified using a virtual path to the response + /// using mechanisms provided by the host. + /// + public class VirtualFileProviderResult : FileResult + { + private const int DefaultBufferSize = 0x1000; + private string _fileName; + + /// + /// Creates a new instance with the provided + /// and the provided . + /// + /// The path to the file. The path must be relative/virtual. + /// The Content-Type header of the response. + public VirtualFileProviderResult([NotNull] string fileName, [NotNull] string contentType) + : this(fileName, new MediaTypeHeaderValue(contentType)) + { + } + + /// + /// Creates a new instance with + /// the provided and the + /// provided . + /// + /// The path to the file. The path must be relative/virtual. + /// The Content-Type header of the response. + public VirtualFileProviderResult([NotNull] string fileName, [NotNull] MediaTypeHeaderValue contentType) + : base(contentType) + { + FileName = fileName; + } + + /// + /// Gets or sets the path to the file that will be sent back as the response. + /// + public string FileName + { + get + { + return _fileName; + } + + [param: NotNull] + set + { + _fileName = value; + } + } + + /// + /// Gets or sets the used to resolve paths. + /// + public IFileProvider FileProvider { get; set; } + + /// + protected override async Task WriteFileAsync(HttpResponse response, CancellationToken cancellation) + { + var fileProvider = GetFileProvider(response.HttpContext.RequestServices); + + var normalizedPath = FileName; + if (normalizedPath.StartsWith("~")) + { + normalizedPath = normalizedPath.Substring(1); + } + + var fileInfo = fileProvider.GetFileInfo(normalizedPath); + if (fileInfo.Exists) + { + var physicalPath = fileInfo.PhysicalPath; + var sendFile = response.HttpContext.GetFeature(); + if (sendFile != null && !string.IsNullOrEmpty(physicalPath)) + { + await sendFile.SendFileAsync( + physicalPath, + offset: 0, + length: null, + cancellation: cancellation); + + return; + } + else + { + var fileStream = GetFileStream(fileInfo); + using (fileStream) + { + await fileStream.CopyToAsync(response.Body, DefaultBufferSize, cancellation); + } + + return; + } + } + + throw new FileNotFoundException( + Resources.FormatFileResult_InvalidPath(FileName), FileName); + } + + /// + /// Returns for the specified . + /// + /// The for which the stream is needed. + /// for the specified . + protected virtual Stream GetFileStream([NotNull]IFileInfo fileInfo) + { + return fileInfo.CreateReadStream(); + } + + private IFileProvider GetFileProvider(IServiceProvider requestServices) + { + if (FileProvider != null) + { + return FileProvider; + } + + var hostingEnvironment = requestServices.GetService(); + FileProvider = hostingEnvironment.WebRootFileProvider; + + return FileProvider; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Controller.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Controller.cs index 50c15a411d..ca30aa8cd3 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Controller.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Controller.cs @@ -779,31 +779,60 @@ namespace Microsoft.AspNet.Mvc } /// - /// Returns the file specified by with the + /// Returns the file specified by with the /// specified as the Content-Type. /// - /// The with the contents of the file. + /// The virtual path of the file to be returned. /// The Content-Type of the file. - /// The created for the response. + /// The created for the response. [NonAction] - public virtual FilePathResult File(string fileName, string contentType) + public virtual VirtualFileProviderResult File(string virtualPath, string contentType) { - return File(fileName, contentType, fileDownloadName: null); + return File(virtualPath, contentType, fileDownloadName: null); } /// - /// Returns the file specified by with the + /// Returns the file specified by with the /// specified as the Content-Type and the /// specified as the suggested file name. /// - /// The with the contents of the file. + /// The virtual path of the file to be returned. /// The Content-Type of the file. /// The suggested file name. - /// The created for the response. + /// The created for the response. [NonAction] - public virtual FilePathResult File(string fileName, string contentType, string fileDownloadName) + public virtual VirtualFileProviderResult File(string virtualPath, string contentType, string fileDownloadName) { - return new FilePathResult(fileName, contentType) { FileDownloadName = fileDownloadName }; + return new VirtualFileProviderResult(virtualPath, contentType) { FileDownloadName = fileDownloadName }; + } + + /// + /// Returns the file specified by with the + /// specified as the Content-Type. + /// + /// The physical path of the file to be returned. + /// The Content-Type of the file. + /// The created for the response. + [NonAction] + public virtual PhysicalFileProviderResult PhysicalFile(string physicalPath, string contentType) + { + return PhysicalFile(physicalPath, contentType, fileDownloadName: null); + } + + /// + /// Returns the file specified by with the + /// specified as the Content-Type and the + /// specified as the suggested file name. + /// + /// The physical path of the file to be returned. + /// The Content-Type of the file. + /// The suggested file name. + /// The created for the response. + [NonAction] + public virtual PhysicalFileProviderResult PhysicalFile( + string physicalPath, string contentType, string fileDownloadName) + { + return new PhysicalFileProviderResult(physicalPath, contentType) { FileDownloadName = fileDownloadName }; } /// diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/FilePathResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/FilePathResultTest.cs deleted file mode 100644 index 3c887f4d1a..0000000000 --- a/test/Microsoft.AspNet.Mvc.Core.Test/FilePathResultTest.cs +++ /dev/null @@ -1,517 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNet.FileProviders; -using Microsoft.AspNet.Hosting; -using Microsoft.AspNet.Http.Features; -using Microsoft.AspNet.Http.Internal; -using Microsoft.AspNet.Routing; -using Microsoft.AspNet.Testing.xunit; -using Microsoft.Framework.DependencyInjection; -using Microsoft.Net.Http.Headers; -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); - } - - [ConditionalTheory] - // https://github.com/aspnet/Mvc/issues/2727 - [FrameworkSkipCondition(RuntimeFrameworks.Mono)] - public async Task ExecuteResultAsync_FallsbackToStreamCopy_IfNoIHttpSendFilePresent() - { - // Arrange - var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); - - var result = new FilePathResult(path, "text/plain") - { - FileProvider = new PhysicalFileProvider(Path.GetFullPath(".")), - }; - - 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_FallsBackToThePhysicalFileProvider_IfNoFileProviderIsPresent() - { - // Arrange - var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt"); - var result = new FilePathResult(path, "text/plain"); - - var appEnvironment = new Mock(); - appEnvironment.Setup(app => app.WebRootFileProvider) - .Returns(new PhysicalFileProvider(Directory.GetCurrentDirectory())); - - var httpContext = new DefaultHttpContext(); - httpContext.Response.Body = new MemoryStream(); - httpContext.RequestServices = new ServiceCollection() - .AddInstance(appEnvironment.Object) - .BuildServiceProvider(); - - 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); - } - - [ConditionalTheory] - // https://github.com/aspnet/Mvc/issues/2727 - [FrameworkSkipCondition(RuntimeFrameworks.Mono)] - public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent() - { - // Arrange - var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); - - var result = new FilePathResult(path, "text/plain") - { - FileProvider = new PhysicalFileProvider(Path.GetFullPath(".")), - }; - - var sendFileMock = new Mock(); - sendFileMock - .Setup(s => s.SendFileAsync(path, 0, null, CancellationToken.None)) - .Returns(Task.FromResult(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(); - } - - [ConditionalTheory] - // https://github.com/aspnet/Mvc/issues/2727 - [FrameworkSkipCondition(RuntimeFrameworks.Mono)] - public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding() - { - // Arrange - var expectedContentType = "text/foo; charset=us-ascii"; - // path will be C:/.../TestFiles/FilePathResultTestFile_ASCII.txt - var path = Path.GetFullPath(Path.Combine(".", "TestFiles", "FilePathResultTestFile_ASCII.txt")); - path = path.Replace(@"\", "/"); - - // Point the FileProviderRoot to a subfolder - var result = new FilePathResult(path, MediaTypeHeaderValue.Parse(expectedContentType)) - { - FileProvider = new PhysicalFileProvider(Path.GetFullPath("Properties")), - }; - - var httpContext = new DefaultHttpContext(); - var memoryStream = new MemoryStream(); - httpContext.Response.Body = memoryStream; - - var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(context); - - // Assert - var contents = Encoding.ASCII.GetString(memoryStream.ToArray()); - Assert.Equal("FilePathResultTestFile contents ASCII encoded", contents); - Assert.Equal(expectedContentType, httpContext.Response.ContentType); - } - - [ConditionalTheory] - // https://github.com/aspnet/Mvc/issues/2727 - [FrameworkSkipCondition(RuntimeFrameworks.Mono)] - public async Task ExecuteResultAsync_WorksWithAbsolutePaths_UsingBackSlash() - { - // Arrange - // path will be C:\...\TestFiles\FilePathResultTestFile.txt - var path = Path.GetFullPath(Path.Combine(".", "TestFiles", "FilePathResultTestFile.txt")); - // We want ot ensure that the path that we provide has backslashes to ensure they get normalized into - // forward slashes. - path = path.Replace('/', '\\'); - - // Point the FileProviderRoot to a subfolder - var result = new FilePathResult(path, "text/plain") - { - FileProvider = new PhysicalFileProvider(Path.GetFullPath("Properties")), - }; - - 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); - } - - [ConditionalTheory] - // https://github.com/aspnet/Mvc/issues/2727 - [FrameworkSkipCondition(RuntimeFrameworks.Mono)] - public async Task ExecuteResultAsync_WorksWithAbsolutePaths_UsingForwardSlash() - { - // Arrange - // path will be C:/.../TestFiles/FilePathResultTestFile.txt - var path = Path.GetFullPath(Path.Combine(".", "TestFiles", "FilePathResultTestFile.txt")); - path = path.Replace(@"\", "/"); - - // Point the FileProviderRoot to a subfolder - var result = new FilePathResult(path, "text/plain") - { - FileProvider = new PhysicalFileProvider(Path.GetFullPath("Properties")), - }; - - 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_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(); - 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(); - nonDiskFileProvider.Setup(fp => fp.GetFileInfo(It.IsAny())).Returns(nonDiskFileInfo.Object); - var filePathResult = new FilePathResult("/SampleEmbeddedFile.txt", "text/plain") - { - 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] - // Root of the file system, forward slash and back slash - [InlineData("FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")] - [InlineData("/FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")] - [InlineData("\\FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")] - // Paths with subfolders and mixed slash kinds - [InlineData("/SubFolder/SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData("\\SubFolder\\SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData("/SubFolder\\SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData("\\SubFolder/SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")] - // '.' has no special meaning - [InlineData("./FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")] - [InlineData(".\\FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")] - [InlineData("./SubFolder/SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData(".\\SubFolder\\SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData("./SubFolder\\SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData(".\\SubFolder/SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")] - // Traverse to the parent directory and back to the file system directory - [InlineData("..\\TestFiles/FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")] - [InlineData("..\\TestFiles\\FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")] - [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")] - // '~/' and '~\' mean the application root folder - [InlineData("~/FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")] - [InlineData("~/SubFolder/SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData("~/SubFolder\\SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")] - public void GetFilePath_Resolves_RelativePaths(string path, string relativePathToFile) - { - // Arrange - var expectedPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePathToFile)); - var fileProvider = new PhysicalFileProvider(Path.GetFullPath("./TestFiles")); - var filePathResult = new FilePathResult(path, "text/plain") - { - FileProvider = fileProvider, - }; - - // Act - var resolveFilePathResult = filePathResult.ResolveFilePath(fileProvider); - - // Assert - Assert.NotNull(resolveFilePathResult); - Assert.NotNull(resolveFilePathResult.FileInfo); - Assert.Equal(expectedPath, resolveFilePathResult.PhysicalFilePath); - } - - [Theory] - [InlineData("~\\FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")] - [InlineData("~\\SubFolder\\SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData("~\\SubFolder/SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")] - public void GetFilePath_FailsToResolve_InvalidVirtualPaths(string path, string relativePathToFile) - { - // Arrange - var fileProvider = new PhysicalFileProvider(Path.GetFullPath("./TestFiles")); - var expectedPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePathToFile)); - var filePathResult = new FilePathResult(path, "text/plain") - { - FileProvider = fileProvider, - }; - - // Act - var ex = Assert.Throws(() => filePathResult.ResolveFilePath(fileProvider)); - - // Assert - Assert.Equal("Could not find file: " + path, ex.Message); - Assert.Equal(path, ex.FileName); - } - - [Theory] - // Root of the file system, forward slash and back slash - [InlineData("FilePathResultTestFile.txt")] - [InlineData("/FilePathResultTestFile.txt")] - [InlineData("\\FilePathResultTestFile.txt")] - // Paths with subfolders and mixed slash kinds - [InlineData("/SubFolder/SubFolderTestFile.txt")] - [InlineData("\\SubFolder\\SubFolderTestFile.txt")] - [InlineData("/SubFolder\\SubFolderTestFile.txt")] - [InlineData("\\SubFolder/SubFolderTestFile.txt")] - // '.' has no special meaning - [InlineData("./FilePathResultTestFile.txt")] - [InlineData(".\\FilePathResultTestFile.txt")] - [InlineData("./SubFolder/SubFolderTestFile.txt")] - [InlineData(".\\SubFolder\\SubFolderTestFile.txt")] - [InlineData("./SubFolder\\SubFolderTestFile.txt")] - [InlineData(".\\SubFolder/SubFolderTestFile.txt")] - // Traverse to the parent directory and back to the file system directory - [InlineData("..\\TestFiles/FilePathResultTestFile.txt")] - [InlineData("..\\TestFiles\\FilePathResultTestFile.txt")] - [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt")] - [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt")] - [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt")] - // '~/' and '~\' mean the application root folder - [InlineData("~/FilePathResultTestFile.txt")] - [InlineData("~/SubFolder/SubFolderTestFile.txt")] - [InlineData("~/SubFolder\\SubFolderTestFile.txt")] - public void GetFilePath_ThrowsFileNotFound_IfItCanNotFindTheFile(string path) - { - // Arrange - - // Point the IFileProvider root to a different subfolder - var fileProvider = new PhysicalFileProvider(Path.GetFullPath("./Properties")); - var filePathResult = new FilePathResult(path, "text/plain") - { - FileProvider = fileProvider, - }; - - var expectedFileName = path.TrimStart('~').Replace('\\', '/'); - var expectedMessage = "Could not find file: " + expectedFileName; - - // Act - var ex = Assert.Throws(() => filePathResult.ResolveFilePath(fileProvider)); - - // Assert - Assert.Equal(expectedMessage, ex.Message); - Assert.Equal(expectedFileName, ex.FileName); - } - - [Theory] - [InlineData("FilePathResultTestFile.txt", "FilePathResultTestFile.txt")] - [InlineData("/FilePathResultTestFile.txt", "/FilePathResultTestFile.txt")] - [InlineData("\\FilePathResultTestFile.txt", "/FilePathResultTestFile.txt")] - // Paths with subfolders and mixed slash kinds - [InlineData("/SubFolder/SubFolderTestFile.txt", "/SubFolder/SubFolderTestFile.txt")] - [InlineData("\\SubFolder\\SubFolderTestFile.txt", "/SubFolder/SubFolderTestFile.txt")] - [InlineData("/SubFolder\\SubFolderTestFile.txt", "/SubFolder/SubFolderTestFile.txt")] - [InlineData("\\SubFolder/SubFolderTestFile.txt", "/SubFolder/SubFolderTestFile.txt")] - // '.' has no special meaning - [InlineData("./FilePathResultTestFile.txt", "./FilePathResultTestFile.txt")] - [InlineData(".\\FilePathResultTestFile.txt", "./FilePathResultTestFile.txt")] - [InlineData("./SubFolder/SubFolderTestFile.txt", "./SubFolder/SubFolderTestFile.txt")] - [InlineData(".\\SubFolder\\SubFolderTestFile.txt", "./SubFolder/SubFolderTestFile.txt")] - [InlineData("./SubFolder\\SubFolderTestFile.txt", "./SubFolder/SubFolderTestFile.txt")] - [InlineData(".\\SubFolder/SubFolderTestFile.txt", "./SubFolder/SubFolderTestFile.txt")] - // Traverse to the parent directory and back to the file system directory - [InlineData("..\\TestFiles/FilePathResultTestFile.txt", "../TestFiles/FilePathResultTestFile.txt")] - [InlineData("..\\TestFiles\\FilePathResultTestFile.txt", "../TestFiles/FilePathResultTestFile.txt")] - [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt", "../TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt", "../TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt", "../TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt", "../TestFiles/SubFolder/SubFolderTestFile.txt")] - // Absolute paths - [InlineData("C:\\Folder\\SubFolder\\File.txt", "C:/Folder/SubFolder/File.txt")] - [InlineData("C:/Folder/SubFolder/File.txt", "C:/Folder/SubFolder/File.txt")] - [InlineData("\\\\NetworkLocation\\Folder\\SubFolder\\File.txt", "//NetworkLocation/Folder/SubFolder/File.txt")] - [InlineData("//NetworkLocation/Folder/SubFolder/File.txt", "//NetworkLocation/Folder/SubFolder/File.txt")] - public void NormalizePath_ConvertsBackSlashes_IntoForwardSlashes(string path, string expectedPath) - { - // Arrange - var fileResult = new FilePathResult(path, "text/plain") - { - FileProvider = Mock.Of(), - }; - - // Act - var normalizedPath = fileResult.NormalizePath(path); - - // Assert - Assert.Equal(expectedPath, normalizedPath); - } - - [Theory] - // '~/' and '~\' mean the application root folder - [InlineData("~/FilePathResultTestFile.txt", "/FilePathResultTestFile.txt")] - [InlineData("~/SubFolder/SubFolderTestFile.txt", "/SubFolder/SubFolderTestFile.txt")] - [InlineData("~/SubFolder\\SubFolderTestFile.txt", "/SubFolder/SubFolderTestFile.txt")] - public void NormalizePath_ConvertsVirtualPaths_IntoRelativePaths(string path, string expectedPath) - { - // Arrange - var fileResult = new FilePathResult(path, "text/plain") - { - FileProvider = Mock.Of(), - }; - - // Act - var normalizedPath = fileResult.NormalizePath(path); - - // Assert - Assert.Equal(expectedPath, normalizedPath); - } - - [Theory] - // '~/' and '~\' mean the application root folder - [InlineData("~\\FilePathResultTestFile.txt")] - [InlineData("~\\SubFolder\\SubFolderTestFile.txt")] - [InlineData("~\\SubFolder/SubFolderTestFile.txt")] - public void NormalizePath_DoesNotConvert_InvalidVirtualPathsIntoRelativePaths(string path) - { - // Arrange - var fileResult = new FilePathResult(path, "text/plain") - { - FileProvider = Mock.Of(), - }; - - // Act - var normalizedPath = fileResult.NormalizePath(path); - - // Assert - Assert.Equal(path, normalizedPath); - } - - [ConditionalTheory] - // https://github.com/aspnet/Mvc/issues/2727 - [FrameworkSkipCondition(RuntimeFrameworks.Mono)] - [InlineData("C:\\Folder\\SubFolder\\File.txt")] - [InlineData("C:/Folder/SubFolder/File.txt")] - [InlineData("\\\\NetworkLocation\\Folder\\SubFolder\\File.txt")] - [InlineData("//NetworkLocation/Folder/SubFolder/File.txt")] - public void IsPathRooted_ReturnsTrue_ForAbsolutePaths(string path) - { - // Arrange - var fileResult = new FilePathResult(path, "text/plain") - { - FileProvider = Mock.Of(), - }; - - // Act - var isRooted = fileResult.IsPathRooted(path); - - // Assert - Assert.True(isRooted); - } - - [ConditionalTheory] - // https://github.com/aspnet/Mvc/issues/2727 - [FrameworkSkipCondition(RuntimeFrameworks.Mono)] - [InlineData("FilePathResultTestFile.txt")] - [InlineData("/FilePathResultTestFile.txt")] - [InlineData("\\FilePathResultTestFile.txt")] - // Paths with subfolders and mixed slash kinds - [InlineData("/SubFolder/SubFolderTestFile.txt")] - [InlineData("\\SubFolder\\SubFolderTestFile.txt")] - [InlineData("/SubFolder\\SubFolderTestFile.txt")] - [InlineData("\\SubFolder/SubFolderTestFile.txt")] - // '.' has no special meaning - [InlineData("./FilePathResultTestFile.txt")] - [InlineData(".\\FilePathResultTestFile.txt")] - [InlineData("./SubFolder/SubFolderTestFile.txt")] - [InlineData(".\\SubFolder\\SubFolderTestFile.txt")] - [InlineData("./SubFolder\\SubFolderTestFile.txt")] - [InlineData(".\\SubFolder/SubFolderTestFile.txt")] - // Traverse to the parent directory and back to the file system directory - [InlineData("..\\TestFiles/FilePathResultTestFile.txt")] - [InlineData("..\\TestFiles\\FilePathResultTestFile.txt")] - [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt")] - [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt")] - [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt")] - // '~/' and '~\' mean the application root folder - [InlineData("~/FilePathResultTestFile.txt")] - [InlineData("~\\FilePathResultTestFile.txt")] - [InlineData("~/SubFolder/SubFolderTestFile.txt")] - [InlineData("~\\SubFolder\\SubFolderTestFile.txt")] - [InlineData("~/SubFolder\\SubFolderTestFile.txt")] - [InlineData("~\\SubFolder/SubFolderTestFile.txt")] - public void IsPathRooted_ReturnsFalse_ForRelativePaths(string path) - { - // Arrange - var fileResult = new FilePathResult(path, "text/plain") - { - FileProvider = Mock.Of(), - }; - - // Act - var isRooted = fileResult.IsPathRooted(path); - - // Assert - Assert.False(isRooted); - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/PhysicalFileProviderResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/PhysicalFileProviderResultTest.cs new file mode 100644 index 0000000000..1451468bd5 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/PhysicalFileProviderResultTest.cs @@ -0,0 +1,207 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Http.Features; +using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.Routing; +using Microsoft.Net.Http.Headers; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class PhysicalFileProviderResultTest + { + [Fact] + public void Constructor_SetsFileName() + { + // Arrange + var path = Path.GetFullPath("helllo.txt"); + + // Act + var result = new PhysicalFileProviderResult(path, "text/plain"); + + // 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 TestPhysicalFileProviderResult(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 PhysicalFileProviderResult(path, "text/plain"); + var sendFileMock = new Mock(); + sendFileMock + .Setup(s => s.SendFileAsync(path, 0, null, CancellationToken.None)) + .Returns(Task.FromResult(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(); + } + + [Fact] + public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding() + { + // Arrange + var expectedContentType = "text/foo; charset=us-ascii"; + var path = Path.GetFullPath(Path.Combine(".", "TestFiles", "FilePathResultTestFile_ASCII.txt")); + var result = new TestPhysicalFileProviderResult(path, MediaTypeHeaderValue.Parse(expectedContentType)) + { + IsAscii = true + }; + var httpContext = new DefaultHttpContext(); + var memoryStream = new MemoryStream(); + httpContext.Response.Body = memoryStream; + var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + await result.ExecuteResultAsync(context); + + // Assert + var contents = Encoding.ASCII.GetString(memoryStream.ToArray()); + Assert.Equal("FilePathResultTestFile contents ASCII encoded", contents); + Assert.Equal(expectedContentType, httpContext.Response.ContentType); + } + + [Fact] + public async Task ExecuteResultAsync_WorksWithAbsolutePaths() + { + // Arrange + var path = Path.GetFullPath(Path.Combine(".", "TestFiles", "FilePathResultTestFile.txt")); + var result = new TestPhysicalFileProviderResult(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); + } + + [Theory] + [InlineData("FilePathResultTestFile.txt")] + [InlineData("./FilePathResultTestFile.txt")] + [InlineData(".\\FilePathResultTestFile.txt")] + [InlineData("~/FilePathResultTestFile.txt")] + [InlineData("..\\TestFiles/FilePathResultTestFile.txt")] + [InlineData("..\\TestFiles\\FilePathResultTestFile.txt")] + [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt")] + [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt")] + [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt")] + [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt")] + [InlineData("~/SubFolder/SubFolderTestFile.txt")] + [InlineData("~/SubFolder\\SubFolderTestFile.txt")] + public async Task ExecuteAsync_ThrowsFileNotFound_ForNonRootedPaths(string path) + { + // Arrange + var result = new TestPhysicalFileProviderResult(path, "text/plain"); + var context = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + var expectedMessage = "Could not find file: " + path; + + // Act + var ex = await Assert.ThrowsAsync(() => result.ExecuteResultAsync(context)); + + // Assert + Assert.Equal(expectedMessage, ex.Message); + Assert.Equal(path, ex.FileName); + } + + [Theory] + [InlineData("/SubFolder/SubFolderTestFile.txt")] + [InlineData("\\SubFolder\\SubFolderTestFile.txt")] + [InlineData("/SubFolder\\SubFolderTestFile.txt")] + [InlineData("\\SubFolder/SubFolderTestFile.txt")] + [InlineData("./SubFolder/SubFolderTestFile.txt")] + [InlineData(".\\SubFolder\\SubFolderTestFile.txt")] + [InlineData("./SubFolder\\SubFolderTestFile.txt")] + [InlineData(".\\SubFolder/SubFolderTestFile.txt")] + public void ExecuteAsync_ThrowsDirectoryNotFound_IfItCanNotFindTheDirectory_ForRootPaths(string path) + { + // Arrange + var result = new TestPhysicalFileProviderResult(path, "text/plain"); + var context = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + + // Act & Assert + Assert.ThrowsAsync(() => result.ExecuteResultAsync(context)); + } + + [Theory] + [InlineData("/FilePathResultTestFile.txt")] + [InlineData("\\FilePathResultTestFile.txt")] + public void ExecuteAsync_ThrowsFileNotFound_WhenFileDoesNotExist_ForRootPaths(string path) + { + // Arrange + var result = new TestPhysicalFileProviderResult(path, "text/plain"); + var context = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + + // Act & Assert + Assert.ThrowsAsync(() => result.ExecuteResultAsync(context)); + } + + private class TestPhysicalFileProviderResult : PhysicalFileProviderResult + { + public TestPhysicalFileProviderResult(string filePath, string contentType) + : base(filePath, contentType) + { + } + + public TestPhysicalFileProviderResult(string filePath, MediaTypeHeaderValue contentType) + : base(filePath, contentType) + { + } + + public bool IsAscii { get; set; } = false; + + protected override Stream GetFileStream(string path) + { + if (IsAscii) + { + return new MemoryStream(Encoding.ASCII.GetBytes("FilePathResultTestFile contents ASCII encoded")); + } + else + { + return new MemoryStream(Encoding.UTF8.GetBytes("FilePathResultTestFile contents¡")); + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/FilePathResultTestFile.txt b/test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/FilePathResultTestFile.txt deleted file mode 100644 index 1ae10032bf..0000000000 --- a/test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/FilePathResultTestFile.txt +++ /dev/null @@ -1 +0,0 @@ -FilePathResultTestFile contents \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/FilePathResultTestFile_ASCII.txt b/test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/FilePathResultTestFile_ASCII.txt deleted file mode 100644 index bde72f59aa..0000000000 --- a/test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/FilePathResultTestFile_ASCII.txt +++ /dev/null @@ -1 +0,0 @@ -FilePathResultTestFile contents ASCII encoded \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/SubFolder/SubFolderTestFile.txt b/test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/SubFolder/SubFolderTestFile.txt deleted file mode 100644 index 1ae10032bf..0000000000 --- a/test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/SubFolder/SubFolderTestFile.txt +++ /dev/null @@ -1 +0,0 @@ -FilePathResultTestFile contents \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/VirtualFileProviderResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/VirtualFileProviderResultTest.cs new file mode 100644 index 0000000000..5f5f58b20b --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/VirtualFileProviderResultTest.cs @@ -0,0 +1,326 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.FileProviders; +using Microsoft.AspNet.Hosting; +using Microsoft.AspNet.Http.Features; +using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.Routing; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Net.Http.Headers; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class VirtualFileProviderResultTest + { + [Fact] + public void Constructor_SetsFileName() + { + // Arrange + var path = Path.GetFullPath("helllo.txt"); + + // Act + var result = new VirtualFileProviderResult(path, "text/plain"); + + // Assert + Assert.Equal(path, result.FileName); + } + + [Fact] + public async Task ExecuteResultAsync_FallsBackToWebRootFileProvider_IfNoFileProviderIsPresent() + { + // Arrange + var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt"); + var result = new TestVirtualFileProviderResult(path, "text/plain"); + + var appEnvironment = new Mock(); + appEnvironment.Setup(app => app.WebRootFileProvider) + .Returns(GetFileProvider(path)); + + var httpContext = new DefaultHttpContext(); + httpContext.Response.Body = new MemoryStream(); + httpContext.RequestServices = new ServiceCollection() + .AddInstance(appEnvironment.Object) + .BuildServiceProvider(); + 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_FallsbackToStreamCopy_IfNoIHttpSendFilePresent() + { + // Arrange + var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt"); + var result = new TestVirtualFileProviderResult(path, "text/plain") + { + FileProvider = GetFileProvider(path), + }; + + 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.Combine("TestFiles", "FilePathResultTestFile.txt"); + var result = new TestVirtualFileProviderResult(path, "text/plain") + { + FileProvider = GetFileProvider(path), + }; + + var sendFileMock = new Mock(); + sendFileMock + .Setup(s => s.SendFileAsync(path, 0, null, CancellationToken.None)) + .Returns(Task.FromResult(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(); + } + + [Fact] + public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding() + { + // Arrange + var expectedContentType = "text/foo; charset=us-ascii"; + var result = new TestVirtualFileProviderResult( + "FilePathResultTestFile_ASCII.txt", MediaTypeHeaderValue.Parse(expectedContentType)) + { + FileProvider = GetFileProvider("FilePathResultTestFile_ASCII.txt"), + IsAscii = true, + }; + + var httpContext = new DefaultHttpContext(); + var memoryStream = new MemoryStream(); + httpContext.Response.Body = memoryStream; + var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + await result.ExecuteResultAsync(context); + + // Assert + var contents = Encoding.ASCII.GetString(memoryStream.ToArray()); + Assert.Equal("FilePathResultTestFile contents ASCII encoded", contents); + Assert.Equal(expectedContentType, httpContext.Response.ContentType); + } + + [Fact] + public async Task ExecuteResultAsync_ReturnsFileContentsForRelativePaths() + { + // Arrange + var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt"); + var result = new TestVirtualFileProviderResult(path, "text/plain") + { + FileProvider = GetFileProvider(path), + }; + + 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); + } + + [Theory] + [InlineData("FilePathResultTestFile.txt")] + [InlineData("TestFiles/FilePathResultTestFile.txt")] + [InlineData("TestFiles\\FilePathResultTestFile.txt")] + [InlineData("~/FilePathResultTestFile.txt")] + [InlineData("~/TestFiles/FilePathResultTestFile.txt")] + [InlineData("~/TestFiles\\FilePathResultTestFile.txt")] + public async Task ExecuteResultAsync_ReturnsFiles_ForDifferentPaths(string path) + { + // Arrange + var result = new TestVirtualFileProviderResult(path, "text/plain") + { + FileProvider = GetFileProvider(path), + }; + var httpContext = new DefaultHttpContext(); + var memoryStream = new MemoryStream(); + httpContext.Response.Body = memoryStream; + + var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + await result.ExecuteResultAsync(context); + httpContext.Response.Body.Position = 0; + + // Assert + var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync(); + 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(); + 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(); + nonDiskFileProvider.Setup(fp => fp.GetFileInfo(It.IsAny())).Returns(nonDiskFileInfo.Object); + + var filePathResult = new VirtualFileProviderResult("/SampleEmbeddedFile.txt", "text/plain") + { + 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] + // Root of the file system, forward slash and back slash + [InlineData("FilePathResultTestFile.txt")] + [InlineData("/FilePathResultTestFile.txt")] + [InlineData("\\FilePathResultTestFile.txt")] + // '.' has no special meaning + [InlineData("./FilePathResultTestFile.txt")] + [InlineData(".\\FilePathResultTestFile.txt")] + // Traverse to the parent directory and back to the file system directory + [InlineData("..\\TestFiles/FilePathResultTestFile.txt")] + [InlineData("..\\TestFiles\\FilePathResultTestFile.txt")] + [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt")] + [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt")] + [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt")] + [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt")] + [InlineData("~/FilePathResultTestFile.txt")] + [InlineData("~\\TestFiles\\FilePathResultTestFile.txt")] + public async Task ExecuteResultAsync_ThrowsFileNotFound_IfFileProviderCanNotFindTheFile(string path) + { + // Arrange + // Point the IFileProvider root to a different subfolder + var fileProvider = new PhysicalFileProvider(Path.GetFullPath("./Properties")); + var filePathResult = new VirtualFileProviderResult(path, "text/plain") + { + FileProvider = fileProvider, + }; + + var expectedMessage = "Could not find file: " + path; + var context = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + + // Act + var ex = await Assert.ThrowsAsync(() => filePathResult.ExecuteResultAsync(context)); + + // Assert + Assert.Equal(expectedMessage, ex.Message); + Assert.Equal(path, ex.FileName); + } + + [Theory] + [InlineData("/SubFolder/SubFolderTestFile.txt")] + [InlineData("\\SubFolder\\SubFolderTestFile.txt")] + [InlineData("/SubFolder\\SubFolderTestFile.txt")] + [InlineData("\\SubFolder/SubFolderTestFile.txt")] + [InlineData("./SubFolder/SubFolderTestFile.txt")] + [InlineData(".\\SubFolder\\SubFolderTestFile.txt")] + [InlineData("./SubFolder\\SubFolderTestFile.txt")] + [InlineData(".\\SubFolder/SubFolderTestFile.txt")] + [InlineData("~/SubFolder/SubFolderTestFile.txt")] + [InlineData("~/SubFolder\\SubFolderTestFile.txt")] + public void ExecuteResultAsync_ThrowsDirectoryNotFound_IfFileProviderCanNotFindTheDirectory(string path) + { + // Arrange + // Point the IFileProvider root to a different subfolder + var fileProvider = new PhysicalFileProvider(Path.GetFullPath("./Properties")); + var filePathResult = new VirtualFileProviderResult(path, "text/plain") + { + FileProvider = fileProvider, + }; + + var context = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + + // Act & Assert + Assert.ThrowsAsync(() => filePathResult.ExecuteResultAsync(context)); + } + + private IFileProvider GetFileProvider(string path) + { + var fileInfo = new Mock(); + fileInfo.SetupGet(fi => fi.Exists).Returns(true); + fileInfo.SetupGet(fi => fi.PhysicalPath).Returns(() => path); + var fileProvider = new Mock(); + fileProvider.Setup(fp => fp.GetFileInfo(It.IsAny())).Returns(fileInfo.Object); + + return fileProvider.Object; + } + + private class TestVirtualFileProviderResult : VirtualFileProviderResult + { + public TestVirtualFileProviderResult(string filePath, string contentType) + : base(filePath, contentType) + { + } + + public TestVirtualFileProviderResult(string filePath, MediaTypeHeaderValue contentType) + : base(filePath, contentType) + { + } + + public bool IsAscii { get; set; } = false; + + protected override Stream GetFileStream(IFileInfo fileInfo) + { + if (IsAscii) + { + return new MemoryStream(Encoding.ASCII.GetBytes("FilePathResultTestFile contents ASCII encoded")); + } + else + { + return new MemoryStream(Encoding.UTF8.GetBytes("FilePathResultTestFile contents¡")); + } + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/FilesWebSite/Controllers/DownloadFilesController.cs b/test/WebSites/FilesWebSite/Controllers/DownloadFilesController.cs index 192b495cc7..7733fdca21 100644 --- a/test/WebSites/FilesWebSite/Controllers/DownloadFilesController.cs +++ b/test/WebSites/FilesWebSite/Controllers/DownloadFilesController.cs @@ -20,13 +20,13 @@ namespace FilesWebSite public IActionResult DowloadFromDisk() { var path = Path.Combine(_appEnvironment.ApplicationBasePath, "sample.txt"); - return File(path, "text/plain"); + return PhysicalFile(path, "text/plain"); } public IActionResult DowloadFromDiskWithFileName() { var path = Path.Combine(_appEnvironment.ApplicationBasePath, "sample.txt"); - return File(path, "text/plain", "downloadName.txt"); + return PhysicalFile(path, "text/plain", "downloadName.txt"); } public IActionResult DowloadFromStream() diff --git a/test/WebSites/FilesWebSite/Controllers/EmbeddedFilesController.cs b/test/WebSites/FilesWebSite/Controllers/EmbeddedFilesController.cs index b22c4ce19a..7d2cc5ac68 100644 --- a/test/WebSites/FilesWebSite/Controllers/EmbeddedFilesController.cs +++ b/test/WebSites/FilesWebSite/Controllers/EmbeddedFilesController.cs @@ -11,7 +11,7 @@ namespace FilesWebSite { public IActionResult DownloadFileWithFileName() { - return new FilePathResult("/Greetings.txt", "text/plain") + return new VirtualFileProviderResult("/Greetings.txt", "text/plain") { FileProvider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, "FilesWebSite.EmbeddedResources"), FileDownloadName = "downloadName.txt"