通过HTTP请求获取的Web资源很多都来源于存储在服务器磁盘上的静态文件。对于ASP.NET应用来说,如果将静态文件存储到约定的目录下,绝大部分文件类型都是可以通过Web的形式对外发布的。“Microsoft.AspNetCore.StaticFiles” 这个NuGet包中提供了三个用来处理静态文件请求的中间件,我们可以用它们搭建一个文件服务器。[本文节选《ASP.NET Core 6框架揭秘》第18章]
[1901]以Web形式发布文件(图片)(源代码)
[1902]以Web形式发布文件(PDF)(源代码)
[1903]显式文件目录结构(源代码)
[1904]显示目录的默认页面(源代码)
[1905]定制目录的默认页面(源代码)
[1906]设置默认的媒体类型(源代码)
[1907]映射文件扩展名的媒体类型(源代码)
[1901]以Web形式发布文件(图片)
作为演示实例是ASP.NET应用具有如图1所示的项目结构。在默认作为WebRoot的“wwwroot”目录下,我们将JavaScript脚本文件、CSS样式文件和图片文件存放到对应的子目录(js、css和img)下。该目录下的所有文件将自动发布为Web资源,客户端可以访问相应的URL来读取对应它们的内容。
图1 静态文件发布的项目结构
针对具体某个静态文件的请求是通过StaticFileMiddleware中间件来处理。如下所示的演示程序中调用IApplicationBuilder接口的UseStaticFiles扩展方法注册的就是这个中间件。
var app = WebApplication.Create();
app.UseStaticFiles();
app.Run();
演示程序运行之后,就可以通过GET请求的方式来读取对应文件的内容,目标文件相对于WebRoot目录的路径就是对应URL的路径,如JPG图片文件“~/wwwroot/img/dolphin1.jpg”对应的URL路径为“/img/dolphin1.jpg”。如果直接利用浏览器访问这个URL,目标图片就会直接以图2所示的形式显示出来。
图2 以Web形式请求发布的图片文件
[1902]以Web形式发布文件(PDF)
上面通过一个简单的实例将WebRoot所在目录下的所有静态文件发布为Web资源,如果需要发布的静态文件存储在其他目录下呢?比如我们将上面演示的应用程序的一些文档存储在图3所示的“~/doc/”目录下,那么对应的程序又该如何编写呢?
图3 发布“~/doc/”和“~/wwwroot”目录下的文件
ASP.NET应用在大部分情况下都是利用一个IFileProvider对象来读取文件的,针对静态文件的读取请求处理也不例外。StaticFileMiddleware中间件内部维护着一个IFileProvider对象和请求路径的映射关系。如果调用UseStaticFiles方法没有指定任何参数,那么这个映射的路径就是应用的基地址(PathBase),采用的IFileProvider对象就是指向WebRoot目录的PhysicalFileProvider对象。上述需求可以通过定制这个映射关系来实现。如下面的代码片段所示,我们在现有程序的基础上额外添加了一次针对UseStaticFiles扩展方法的调用,并利用作为参数的StaticFileOptions配置选项添加请求路径(“/documents”)与对应IFileProvider对象(针对路径“~/doc/”的PhysicalFileProvider对象)之间的映射关系。
using Microsoft.Extensions.FileProviders;
var path = Path.Combine(
Directory.GetCurrentDirectory(), "doc");
var options = new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(path),
RequestPath = "/documents"
};
var app = WebApplication.Create();
app
.UseStaticFiles()
.UseStaticFiles(options);
app.Run();
按照上面这段程序指定的映射关系,对于存储在“~/doc/”目录下的这个PDF文件(checklist.pdf),请求URL采用的路径就应该是“/documents/checklist.pdf”。如果利用浏览器请求这个地址时,PDF文件的内容就会按照图4所示的形式显示在浏览器上。
图4 以Web形式请求发布的PDF文件
[1903]显示文件目录结构
StaticFileMiddleware中间件只会处理针对具体的某个静态文件的请求,如果利用浏览器发送一个针对目录路径的请求(比如“/img”),我们将得到状态为“404 Not Found”的响应。如果希望浏览器呈现出目标目录的结构,就可以注册DirectoryBrowserMiddleware中间件。这个中间件会返回一个HTML页面,请求目录下的结构会以表格的形式显示在这个页面中。我们演示的程序按照如下方式调用IApplicationBuilder接口的UseDirectoryBrowser扩展方法注册了这个中间件。
using Microsoft.Extensions.FileProviders;
var path = Path.Combine(
Directory.GetCurrentDirectory(), "doc");
var fileProvider = new PhysicalFileProvider(path);
var fileOptions = new StaticFileOptions
{
FileProvider = fileProvider,
RequestPath = "/documents"
};
var diretoryOptions = new DirectoryBrowserOptions
{
FileProvider = fileProvider,
RequestPath = "/documents"
};
var app = WebApplication.Create();
app
.UseStaticFiles()
.UseStaticFiles(fileOptions)
.UseDirectoryBrowser()
.UseDirectoryBrowser(diretoryOptions);
app.Run();
当上面的应用启动之后,如果利用浏览器向针对某个目录的URL(如“/”或者“/img”)发起请求,目标目录的内容(包括子目录和文件)就会以图5所示的形式显示在一个表格中。可以看出在呈现的表格中,当前目录的子目录和文件均会显示为链接。
图5 显示目录内容
[1904]显示目录的默认页面
UseDirectoryBrowser中间件会将整个目标目录的结构和所有文件全部暴露出来,所以这个中间件需要根据自身的安全策略谨慎使用。对于针对目录的请求,更加常用的处理策略就是显示一个保存该目录下的默认页面。默认页面文件一般采用如下四种命名约定(default.htm、default.html、index.htm和index.html)。默认页面的呈现实现DefaultFilesMiddleware中间件中,我们演示的这个应用可以按照如下方式调用IApplicationBuilder接口的UseDefaultFiles扩展方法来注册这个中间件。
using Microsoft.Extensions.FileProviders;
var path = Path.Combine(Directory.GetCurrentDirectory(), "doc");
var fileProvider = new PhysicalFileProvider(path);
var fileOptions = new StaticFileOptions
{
FileProvider = fileProvider,
RequestPath = "/documents"
};
var diretoryOptions = new DirectoryBrowserOptions
{
FileProvider = fileProvider,
RequestPath = "/documents"
};
var defaultOptions = new DefaultFilesOptions
{
RequestPath = "/documents",
FileProvider = fileProvider,
};
var app = WebApplication.Create();
app
.UseDefaultFiles()
.UseDefaultFiles(defaultOptions)
.UseStaticFiles()
.UseStaticFiles(fileOptions)
.UseDirectoryBrowser()
.UseDirectoryBrowser(diretoryOptions);
app.Run();
下面在“~/wwwroot/img/”和“~/doc”目录下分别创建一个名为index.html的默认页面,并且在该.html文件的主体部分指定一段简短的文字(This is an index page!)。我们在应用启动之后利用浏览器访问这两个目录(“/img”和“/documents”),默认页面就会以图6的形式显示出来。
图6 显示默认页面
[1905]定制目录的默认页面
我们须将DefaultFilesMiddleware中间件放在StaticFileMiddleware和DirectoryBrowserMiddleware中间件之前。这是因为DirectoryBrowserMiddleware和DefaultFilesMiddleware中间件处理的均是针对目录的请求,如果先注册DirectoryBrowserMiddleware中间件,那么显示的总是目录的结构。如果先注册用于显示默认页面的DefaultFilesMiddleware中间件,那么在默认页面不存在的情况下它会将请求分发给后续中间件,此时DirectoryBrowserMiddleware中间件将当前目录的结构呈现出来。要先于StaticFileMiddleware中间件之前注册DefaultFilesMiddleware中间件是因为后者是通过采用URL重写的方式实现的。这个中间件会将针对目录的请求改写成针对默认页面的请求,而最终针对默认页面的请求还需要依赖StaticFileMiddleware中间件来完成。
图7 重命名默认页面
DefaultFilesMiddleware中间件在默认情况下总是以约定的名称在当前请求的目录下定位默认页面。如果作为默认页面的文件没有采用这样的约定命名,比如我们如图7所示的方式将默认页面命名为readme.html,就需要按照如下方式显式指定默认页面的文件名(S1905)。
using Microsoft.Extensions.FileProviders;
var path = Path.Combine(
Directory.GetCurrentDirectory(), "doc");
var fileProvider = new PhysicalFileProvider(path);
var fileOptions = new StaticFileOptions
{
FileProvider = fileProvider,
RequestPath = "/documents"
};
var diretoryOptions = new DirectoryBrowserOptions
{
FileProvider = fileProvider,
RequestPath = "/documents"
};
var defaultOptions1 = new DefaultFilesOptions();
var defaultOptions2 = new DefaultFilesOptions
{
RequestPath = "/documents",
FileProvider = fileProvider,
};
defaultOptions1.DefaultFileNames.Add("readme.html");
defaultOptions2.DefaultFileNames.Add("readme.html");
var app = WebApplication.Create();
app
.UseDefaultFiles(defaultOptions1)
.UseDefaultFiles(defaultOptions2)
.UseStaticFiles()
.UseStaticFiles(fileOptions)
.UseDirectoryBrowser()
.UseDirectoryBrowser(diretoryOptions);
app.Run();
[1906]设置默认的媒体类型
通过上面演示的实例可以看出,浏览器能够准确地将请求的目标文件的内容正常呈现出来。对HTTP协议具有基本了解的读者应该都知道,响应文件能够在浏览器上被正常显示的基本前提是响应报文通过Content-Type报头携带的媒体类型必须与内容一致。我们的实例演示了针对两种文件类型的请求,一种是JPG文件,另一种是PDF文件,对应的媒体类型分别是“image/jpg”和“application/pdf”,那么用来处理静态文件请求的StaticFileMiddleware中间件是如何解析出对应的媒体类型的呢?
StaticFileMiddleware中间件针对媒体类型的解析是通过一个IContentTypeProvider对象来完成的, FileExtensionContentTypeProvider是对该接口的默认实现。FileExtensionContentTypeProvider根据文件的扩展命名来解析媒体类型。它在内部预定了数百种常用文件扩展名与对应媒体类型之间的映射关系,所以如果发布的静态文件具有标准的扩展名,StaticFileMiddleware中间件就能为对应的响应赋予正确的媒体类型。
图8 重命名默认页面
如果某个文件的扩展名没有在预定义的映射之中,或者需要某个预定义的扩展名匹配不同的媒体类型,那又应该如何解决呢?同样是针对我们演示的这个实例,如果我们以图8所示的方式将“~/wwwroot/img/ dolphin1.jpg”文件的扩展名改成.img,那么StaticFileMiddleware中间件将无法为针对该文件的请求解析出正确的媒体类型。这个问题具有若干不同的解决方案,第一种方案就是按照如下方式让StaticFileMiddleware中间件支持不能识别的文件类型,并为设置一个默认的媒体类型。
var options = new StaticFileOptions
{
ServeUnknownFileTypes = true,
DefaultContentType = "image/jpg"
};
var app = WebApplication.Create();
app.UseStaticFiles(options);
app.Run();
[1907]映射文件扩展名的媒体类型
上述解决方案只能设置一种默认媒体类型,如果具有多种需要映射成不同媒体类型的文件类型,这种方案就无能为力了,所以最根本的解决方案还是需要将不能识别的文件类型和对应的媒体类型进行映射。由于StaticFileMiddleware中间件使用的IContentTypeProvider对象是可以定制的,所以可以按照如下方式显式地为该中间件指定一个FileExtensionContentTypeProvider对象,然后将缺失的映射添加到这个对象上即可。
using Microsoft.AspNetCore.StaticFiles;
var contentTypeProvider
= new FileExtensionContentTypeProvider();
contentTypeProvider.Mappings.Add(".img", "image/jpg");
var options = new StaticFileOptions
{
ContentTypeProvider = contentTypeProvider
};
var app = WebApplication.Create();
app.UseStaticFiles(options);
app.Run();