ASP.NET Core应用针对静态文件请求的处理[4]: DirectoryBrowserMiddleware中间件如何呈现目录结构

和StaticFileMiddleware中间件一样,DirectoryBrowserMiddleware中间本质上还是定义了一个请求地址与某个物理目录之间的映射关系,而目标目录体现为一个FileProvider对象。当这个中间件接收到匹配的请求后,会根据请求地址解析出对应目录的相对路径,并利用这个FileProvider获取目录的内容。目录的内容最终会以一个HTML文档的形式被定义,而此HTML最终会被这个中间件作为响应的内容,“目录浏览器”的实现原理就这么简单。

目录 一、DirectoryBrowserMiddleware 二、DirectoryFormatter 三、具体请求处理逻辑 四、自定义DirectoryFormatter

一、DirectoryBrowserMiddleware

接下来我们来看看DirectoryBrowserMiddleware的定义。如下面的代码片段所示,DirectoryBrowserMiddleware的第二个构造函数具有四个参数,其中第二个参数是代表当前执行环境的HostingEnvironment。作为第三个参数的是一个HtmlEncoder对象,当目标目录被呈现为一个HTML文档的时候,它被用于实现针对HTML的编码,如果没有显式指定(调用第一个构造函数),默认的HtmlEncoder(HtmlEncoder.Default)会被使用。至于第四个类型为IOptions<DirectoryBrowserOptions>的参数,则承载了针对DirectoryBrowserMiddleware的配置选项,DirectoryBrowserOptions与前面介绍的StaticFileOptions一样,它们都是SharedOptionsBase的子类。

   1: public class DirectoryBrowserMiddleware
   2: {
   3:     public DirectoryBrowserMiddleware(RequestDelegate next, IHostingEnvironment env, IOptions<DirectoryBrowserOptions> options)
   4:     public DirectoryBrowserMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, HtmlEncoder encoder, IOptions<DirectoryBrowserOptions> options);
   5:     public Task Invoke(HttpContext context);
   6: }
   7:  
   8: public class DirectoryBrowserOptions : SharedOptionsBase
   9: {
  10:     public IDirectoryFormatter Formatter { get; set; }
  11:  
  12:     public DirectoryBrowserOptions();
  13:     public DirectoryBrowserOptions(SharedOptions sharedOptions);
  14: }

二、DirectoryFormatter

DirectoryBrowserMiddleware中间件的目的很明确,就是将目录下的内容(文件和子目录)格式化成一种可读的形式响应给客户端,针对目录内容的响应最终实现在一个DirectoryFormatter对象上。DirectoryFormatter是我们对所有实现了IDirectoryFormatter接口的类型与对应对象的统称,DirectoryBrowserOptions的Formatter属性设置和返回的就是这个一个对象。

如下面的代码片段所示,IDirectoryFormatter接口仅仅包含一个GenerateContentAsync方法。当实现这个方法的时候,我们可以利用第一个类型为HttpContext的参数获取当前请求上下文的信息。该方法的另一个参数返回一组FileInfo的集合,每个FileInfo代表目标下的某个以文件或者子目录。

   1: public interface IDirectoryFormatter
   2: {
   3:     Task GenerateContentAsync(HttpContext context, IEnumerable<IFileInfo> contents);
   4: }

我们知道默认情况下请求目录的内容在页面上是以一个表格的形式被呈现的,包含这个表格的HTML文档是默认使用的DirectoryFormatter生成的,它是一个类型为HtmlDirectoryFormatter的对象。如下面的代码片段所示,我们在构造一个HtmlDirectoryFormatter对象的时候需要指定一个HtmlEncoder对象,该对象最初来源于构造DirectoryBrowserMiddleware时指定的那个HtmlEncoder对象。

   1: public class HtmlDirectoryFormatter : IDirectoryFormatter
   2: {
   3:     public HtmlDirectoryFormatter(HtmlEncoder encoder);
   4:     public virtual Task GenerateContentAsync(HttpContext context, IEnumerable<IFileInfo> contents);
   5: }

三、具体请求处理逻辑

既然最复杂的工作(呈现目录内容)都已经交给DirectoryFormatter来完成了,DirectoryBrowserMiddleware自身的工作其实就没有多少了。为了更好的说明这个中间件在处理请求是具体做了些什么,我们采用一种比较好理解的方式对DirectoryBrowserMiddleware类型进行了重新定义,具体的实现体现在如下所示的代码片段中。

   1: public class DirectoryBrowserMiddleware
   2: {
   3:     private RequestDelegate _next;
   4:     private DirectoryBrowserOptions _options;
   5:  
   6:     public DirectoryBrowserMiddleware(RequestDelegate next, IHostingEnvironment env, IOptions<DirectoryBrowserOptions> options) : this(next, env, HtmlEncoder.Default,options)
   7:     { }
   8:  
   9:     public DirectoryBrowserMiddleware(RequestDelegate next, IHostingEnvironment env, HtmlEncoder encoder, IOptions<DirectoryBrowserOptions> options)
  10:     {
  11:         _next                      = next;
  12:         _options                   = options.Value;
  13:         _options.FileProvider      = _options.FileProvider ?? env.WebRootFileProvider;
  14:         _options.Formatter         = _options.Formatter ?? new HtmlDirectoryFormatter(encoder);
  15:     }
  16:  
  17:     public async Task Invoke(HttpContext context)
  18:     {
  19:         //只处理GET和HEAD请求
  20:         if (!new string[] { "GET", "HEAD" }.Contains(context.Request.Method, StringComparer.OrdinalIgnoreCase))
  21:         {
  22:             await _next(context);
  23:             return;
  24:         }
  25:  
  26:        //检验当前路径是否与注册的请求路径相匹配
  27:         PathString path = new PathString(context.Request.Path.Value.TrimEnd('/') + "/");
  28:         PathString subpath;
  29:         if (!path.StartsWithSegments(_options.RequestPath, out subpath))
  30:         {
  31:             await _next(context); 
  32:             return;
  33:         }
  34:  
  35:         //检验目标目录是否存在
  36:         IDirectoryContents directoryContents = _options.FileProvider.GetDirectoryContents(subpath);
  37:         if (!directoryContents.Exists)
  38:         {
  39:             await _next(context); 
  40:             return;
  41:         }
  42:  
  43:         //如果当前路径不以"/"作为后缀,会响应一个针对“标准”URL的重定向
  44:         if (!context.Request.Path.Value.EndsWith("/"))
  45:         {
  46:             context.Response.StatusCode = 302;
  47:             context.Response.GetTypedHeaders().Location = new Uri(path.Value + context.Request.QueryString);
  48:             return;
  49:         }
  50:  
  51:         //利用DirectoryFormatter响应目录内容
  52:         await _options.Formatter.GenerateContentAsync(context, directoryContents);
  53:     }
  54: }

如上面的代码片段所示,当DirectoryBrowserMiddleware最终利用注册的DirectoryFormatter来响应目标目录的内容之前,它会做一系列的前期工作。比如它会验证当前请求是否是GET或者HEAD请求,以及当前的URL是否与注册的请求路径相匹配,在匹配的情况下还需要验证目标目录是否存在。除此之外,这个中间件要求访问目录的请求路劲必须以字符“/”作为后缀,否则会在目前的路径上添加这个后缀并针对最终的路径发送一个重定向。所以我们利用浏览器发送针对某个目录的请求的时候,URL明明没有指定“/”作为后缀,这个后缀会自动给我们加上,这就是重定向的作用。

四、自定义DirectoryFormatter

由于目录的内容在浏览器中的呈现方式完全由DirectoryFormatter完成,如果实现在HtmlDirectoryFormatter的默认呈现方式不能满足需求(比如我们需要这个页面与现有网站保持相同的风格),这可以通过注册一个自定义的DirectoryFormatter来完成。接下来我们通过一个简单的实例来演示如何定义这么一个DirectoryFormatter。我们将自定义的DirectoryFormatter命名为ListDirectoryFormatter,应为它仅仅将所有文件或者子目录显示为一个简单的列表。

   1: public class ListDirectoryFormatter : IDirectoryFormatter
   2: {
   3:     public async Task GenerateContentAsync(HttpContext context, IEnumerable<IFileInfo> contents)
   4:     {
   5:         context.Response.ContentType = "text/html";
   6:         await context.Response.WriteAsync("<html><head><title>Index</title><body><ul>");
   7:         foreach (var file in contents)
   8:         {
   9:             string href = $"{context.Request.Path.Value.TrimEnd('/')}/{file.Name}";
  10:             await context.Response.WriteAsync($"<li><a href='{href}'>{file.Name}</a></li>");
  11:         }
  12:         await context.Response.WriteAsync("</ul></body></html>");
  13:     }
  14: }
  15:  
  16: public class Program
  17: {
  18:     public static void Main()
  19:     {
  20:         new WebHostBuilder()
  21:             .UseContentRoot(Directory.GetCurrentDirectory())
  22:             .UseKestrel()
  23:             .Configure(app => app.UseDirectoryBrowser(new DirectoryBrowserOptions {Formatter = new ListDirectoryFormatter()}))
  24:             .Build()
  25:             .Run();
  26:     }
  27: }

如上面的代码片段,ListDirectoryFormatter最终响应的是一个完整的HTML文档,它的主体部分只包含一个通过<ul>…</ul>表示的无序列表。列表元素(<li>)是一个针对文件或者子目录的链接。在调用扩展方法UseDirectoryBrowser注册DirectoryBrowserMiddleware中间件的时候,我们为将一个ListDirectoryFormatter对象设置为DirectoryBrowserOptions的Formatter属性。目录内容最终将会采用如图9所示的形式呈现在浏览器上。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏安恒网络空间安全讲武堂

writeup分享 | 近期做的比较好的web

0x01猫头鹰嘤嘤嘤 http://124.128.55.5:30829/index.php 首先分析一下功能,随便上传一张jpg图片上传,跳转到 http...

5908
来自专栏落影的专栏

为何百兆静态库能打进数兆的可执行文件?

前言 第三方库是工程开发必不可少的部分,而第三方库可以是.a和.framework的静态库,也可以是.framework的动态库,其中静态库是最常用的方式。 ...

5068
来自专栏青青天空树

jersey之get,put,post,delete简单使用

  要使用jersey首先要有相应的依赖包,获取方法有很多,本地下载依赖文件或maven获取,这里假设你的环境已经搭建好了。要使用jersey首先要初始化一个c...

932
来自专栏分布式系统和大数据处理

.Net Remoting(远程方法回调) - Part.4

根据这三点的变化,我们可以看出:客户端含有客户端对象,但它还需要远程服务对象的元数据来构建代理;服务端含有服务对象,但它还需要客户端对象的元数据来构建代理。因此...

1092
来自专栏技术博客

Asp.Net Web API 2第五课——Web API路由

    Asp.Net Web API第一课——入门 http://www.cnblogs.com/aehyok/p/3432158.html

1345
来自专栏芋道源码1024

消息队列中间件 RocketMQ 源码分析 —— Message 存储

1、概述 2、CommitLog 结构 3、CommitLog 存储消息 MappedFile#落盘 FlushRealTimeService CommitR...

43413
来自专栏大内老A

WCF技术剖析之十七:消息(Message)详解(中篇)

在上篇中大体上围绕着Message的两个话题进行讲述:消息版本(Message Version)和采用五种不同的方式创建Message。本篇文章将会详细介绍Me...

1959
来自专栏你不就像风一样

[原创]一款小巧、灵活的Java多线程爬虫框架(AiPa)

AiPa 只需要使用者提供网址集合,即可在多线程下自动爬取,并对一些异常进行处理。

1373
来自专栏移动开发面面观

React Native的HTTP请求

1513
来自专栏码农阿宇

Asp.Net WebApi 调试利器“单元测试”

当我们编辑好一个WebApi应用程序后,需要对该Api接口进行调试,传统的调试办法是在方法内设置断点,然后用PostMan等http工具模拟访问进行查看WebA...

3125

扫码关注云+社区

领取腾讯云代金券