1. 程式人生 > >ASP.NET Core靜態檔案中介軟體[5]: DirectoryBrowserMiddleware & DefaultFilesMiddleware

ASP.NET Core靜態檔案中介軟體[5]: DirectoryBrowserMiddleware & DefaultFilesMiddleware

對於NuGet包由“Microsoft.AspNetCore.StaticFiles”提供的3箇中間件來說,StaticFileMiddleware中介軟體旨在處理針對具體靜態檔案的請求,其他兩個中介軟體(DirectoryBrowserMiddleware和DefaultFilesMiddleware)處理的均是針對某個目錄的請求。

目錄
一、DirectoryBrowserMiddleware中介軟體
二、自定義IDirectoryFormatter
三、DefaultFilesMiddleware中介軟體

一、DirectoryBrowserMiddleware中介軟體

與StaticFileMiddleware中介軟體一樣,DirectoryBrowserMiddleware中介軟體本質上還定義了一個請求基地址與某個物理目錄之間的對映關係,而目標目錄體現為一個IFileProvider物件。當這個中介軟體接收到匹配的請求後,會根據請求地址解析出對應目錄的相對路徑,並利用這個IFileProvider物件獲取目錄的結構。目錄結構最終會以一個HTML文件的形式定義,而此HTML文件最終會被這個中介軟體作為響應的內容。

如下面的程式碼片段所示,DirectoryBrowserMiddleware型別的第二個建構函式有4個引數。其中,第二個引數是代表當前執行環境的IWebHostEnvironment物件;第三個引數提供一個HtmlEncoder物件,當目標目錄被呈現為一個HTML文件時,它被用於實現針對HTML的編碼,如果沒有顯式指定(呼叫第一個建構函式),預設的HtmlEncoder(HtmlEncoder.Default)會被使用;第四個型別為IOptions<DirectoryBrowserOptions>的引數用於提供表示配置選項的DirectoryBrowserMiddleware的DirectoryBrowserOptions物件。與前面介紹的StaticFileOptions一樣,DirectoryBrowserOptions是SharedOptionsBase的子類。

public class DirectoryBrowserMiddleware
{
    public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment env, IOptions<DirectoryBrowserOptions> options)
    public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, HtmlEncoder encoder, IOptions<DirectoryBrowserOptions> options);
    public Task Invoke(HttpContext context);
}

public class DirectoryBrowserOptions : SharedOptionsBase
{
    public IDirectoryFormatter Formatter { get; set; }

    public DirectoryBrowserOptions();
    public DirectoryBrowserOptions(SharedOptions sharedOptions);
}

DirectoryBrowserMiddleware中介軟體的註冊可以通過IApplicationBuilder介面的3個Use
DirectoryBrowser擴充套件方法來完成。在呼叫這些擴充套件方法時,如果沒有指定任何引數,就意味著註冊的中介軟體會採用預設配置。我們也可以顯式地執行一個DirectoryBrowserOptions物件來對註冊的中介軟體進行定製。如果我們只希望指定請求的路徑,就可以直接呼叫第三個方法過載。

public static class DirectoryBrowserExtensions
{    
    public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app)
        => app.UseMiddleware<DirectoryBrowserMiddleware>(Array.Empty<object>());

    public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, DirectoryBrowserOptions options)
    {          
        var args = new object[] { Options.Create<DirectoryBrowserOptions>(options) };
        return app.UseMiddleware<DirectoryBrowserMiddleware>(args);
    }

    public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, string requestPath)
    {
        var options = new DirectoryBrowserOptions
        {
            RequestPath = new PathString(requestPath)
        };
        return app.UseDirectoryBrowser(options);
    }
}

DirectoryBrowserMiddleware中介軟體的目的很明確,就是將目錄下的內容(檔案和子目錄)格式化成一種可讀的形式響應給客戶端。針對目錄內容的響應最終實現在一個IDirectoryFormatter物件上,DirectoryBrowserOptions的Formatter屬性設定和返回的就是這樣的一個物件。如下面的程式碼片段所示,IDirectoryFormatter介面僅包含一個GenerateContentAsync方法。當實現這個方法的時候,我們可以利用第一個引數獲取當前HttpContext上下文。該方法的另一個引數返回一組IFileInfo的集合,每個IFileInfo代表目標目錄下的某個檔案或者子目錄。

public interface IDirectoryFormatter
{
    Task GenerateContentAsync(HttpContext context, IEnumerable<IFileInfo> contents);
}

在預設情況下,請求目錄的內容在頁面上是以一個表格的形式來呈現的,包含這個表格的HTML文件正是預設使用的IDirectoryFormatter物件生成的,該物件的型別為HtmlDirectory
Formatter。如下面的程式碼片段所示,我們在構造一個HtmlDirectoryFormatter物件時需要指定一個HtmlEncoder物件,它就是在構造DirectoryBrowserMiddleware物件時提供的那個Html
Encoder物件。

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

既然最複雜的工作(呈現目錄內容)由IDirectoryFormatter完成,那麼DirectoryBrowserMiddleware中介軟體自身的工作其實就會很少。為了更好地說明這個中介軟體在處理請求時具體做了些什麼,可以採用一種比較容易理解的方式對DirectoryBrowserMiddleware型別重新定義。

public class DirectoryBrowserMiddleware
{
    private readonly RequestDelegate _next;
    private readonly DirectoryBrowserOptions _options;

    public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment env, IOptions<DirectoryBrowserOptions> options) : this(next, env, HtmlEncoder.Default, options)
    { }

    public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment env, HtmlEncoder encoder, IOptions<DirectoryBrowserOptions> options)
    {
        _next = next;
        _options = options.Value;
        _options.FileProvider = _options.FileProvider ?? env.WebRootFileProvider;
        _options.Formatter = _options.Formatter ?? new HtmlDirectoryFormatter(encoder);
    }

    public async Task InvokeAsync(HttpContext context)
    {
        //只處理GET請求和HEAD請求
        if (!new string[] { "GET", "HEAD" }.Contains(context.Request.Method, StringComparer.OrdinalIgnoreCase))
        {
            await _next(context);
            return;
        }

        //檢驗當前路徑是否與註冊的請求路徑相匹配
        PathString path = new PathString(context.Request.Path.Value.TrimEnd('/') + "/");
        PathString subpath;
        if (!path.StartsWithSegments(_options.RequestPath, out subpath))
        {
            await _next(context);
            return;
        }

        //檢驗目標目錄是否存在
        IDirectoryContents directoryContents = _options.FileProvider.GetDirectoryContents(subpath);
        if (!directoryContents.Exists)
        {
            await _next(context);
            return;
        }

        //如果當前路徑不以“/”作為字尾,會響應一個針對“標準”URL的重定向
        if (!context.Request.Path.Value.EndsWith("/"))
        {
            context.Response.StatusCode = 302;
            context.Response.GetTypedHeaders().Location = new Uri(path.Value + context.Request.QueryString);
            return;
        }

        //利用DirectoryFormatter響應目錄內容
        await _options.Formatter.GenerateContentAsync(context, directoryContents);
    }
}

如上面的程式碼片段所示,在最終利用註冊的IDirectoryFormatter物件來響應目標目錄的內容之前,DirectoryBrowserMiddleware中介軟體會做一系列的前期工作:驗證當前請求是否是GET請求或者HEAD請求;當前的URL是否與註冊的請求路徑相匹配,在匹配的情況下還需要驗證目標目錄是否存在。

這個中介軟體要求訪問目錄的請求路徑必須以“/”作為字尾,否則會在目前的路徑上新增這個字尾,並針對修正的路徑傳送一個302重定向。所以,利用瀏覽器傳送針對某個目錄的請求時,雖然URL沒有指定“/”作為字尾,但瀏覽器會自動將這個字尾補上,這就是重定向導致的結果。

二、自定義IDirectoryFormatter

目錄結構的呈現方式完全由IDirectoryFormatter物件完成,如果預設註冊的HtmlDirectoryFormatter物件的呈現方式無法滿足需求(如我們需要這個頁面與現有網站保持相同的風格),就可以通過註冊一個自定義的DirectoryFormatter來解決這個問題。下面通過一個簡單的例項來演示如何定義一個IDirectoryFormatter實現型別。我們將自定義的IDirectoryFormatter實現型別命名為ListDirectoryFormatter,因為它僅僅將所有檔案或者子目錄顯示為一個簡單的列表。

public class ListDirectoryFormatter : IDirectoryFormatter
{
    public async Task GenerateContentAsync(HttpContext context,
        IEnumerable<IFileInfo> contents)
    {
        context.Response.ContentType = "text/html";
        await context.Response.WriteAsync("<html><head><title>Index</title><body><ul>");
        foreach (var file in contents)
        {
            string href = $"{context.Request.Path.Value.TrimEnd('/')}/{file.Name}";
            await context.Response.WriteAsync($"<li><a href='{href}'>{file.Name}</a></li>");
        }
        await context.Response.WriteAsync("</ul></body></html>");
    }
}

public class Program
{
    public static void Main()
    {
        var options = new DirectoryBrowserOptions
        {
            Formatter = new ListDirectoryFormatter()
        };
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder.Configure(app => app.UseDirectoryBrowser(options)))
            .Build()
            .Run();
    }
}

如上面的程式碼片段所示,ListDirectoryFormatter最終響應的是一個完整的HTML文件,它的主體部分只包含一個通過<ul></ul>表示的無序列表,列表元素(<li>)是一個針對檔案或者子目錄的連結。在呼叫UseDirectoryBrowser擴充套件方法註冊DirectoryBrowserMiddleware中介軟體時,需要將一個ListDirectoryFormatter物件設定為指定配置選項的Formatter屬性。目錄內容最終以下圖所示的形式呈現在瀏覽器上。

三、DefaultFilesMiddleware中介軟體

DefaultFilesMiddleware中介軟體的目的在於將目標目錄下的預設檔案作為響應內容。如果直接請求的就是這個預設檔案,那麼前面介紹的StaticFileMiddleware中介軟體就會將這個檔案響應給客戶端。如果能夠將針對目錄的請求重定向到這個預設檔案上,一切問題就會迎刃而解。實際上,DefaultFilesMiddleware中介軟體的實現邏輯很簡單,它採用URL重寫的形式修改了當前請求的地址,即將針對目錄的URL修改成針對預設檔案的URL。

下面先介紹DefaultFilesMiddleware型別的定義。與其他兩個中介軟體類似,DefaultFilesMiddleware中介軟體的構造由一個IOptions<DefaultFilesOptions>型別的引數來指定相關的配置選項。由於DefaultFilesMiddleware中介軟體本質上依然體現了請求路徑與某個物理目錄的對映,所以DefaultFilesOptions依然派生於SharedOptionsBase。DefaultFilesOptions的DefaultFileNames屬性包含預定義的預設檔名,由此可以看到它預設包含4個名稱(default.htm、default.html、index.htm和index.html)。

public class DefaultFilesMiddleware
{
    public DefaultFilesMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<DefaultFilesOptions> options);
    public Task Invoke(HttpContext context);
}

public class DefaultFilesOptions : SharedOptionsBase
{
    public IList<string> DefaultFileNames { get; set; }

    public DefaultFilesOptions() : this(new SharedOptions()) { }
    public DefaultFilesOptions(SharedOptions sharedOptions) : base(sharedOptions)
    {
        this.DefaultFileNames = new List<string> {"default.htm", "default.html", "index.htm", "index.html" };
    }
}

DefaultFilesMiddleware中介軟體的註冊可以通過呼叫IApplicationBuilder介面的如下3個名為UseDefaultFiles的擴充套件方法來完成。從如下所示的程式碼片段可以看出,它們與用於註冊DirectoryBrowserMiddleware中介軟體的UseDirectoryBrowser擴充套件方法具有一致的定義和實現方式。

public static class DefaultFilesExtensions
{
    public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app) => app.UseMiddleware<DefaultFilesMiddleware>(Array.Empty<object>());

    public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, DefaultFilesOptions options)
    {
        var args = new object[] {Options.Create<DefaultFilesOptions>(options) };
        return app.UseMiddleware<DefaultFilesMiddleware>(args);
    }

    public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, string requestPath)
    {
        var options = new DefaultFilesOptions
        {
            RequestPath = new PathString(requestPath)
        };
        return app.UseDefaultFiles(options);
    }
}

下面採用一種易於理解的形式重新定義DefaultFilesMiddleware型別,以便於讀者理解它的處理邏輯。如下面的程式碼片段所示,與前面介紹的DirectoryBrowserMiddleware中介軟體一樣,DefaultFilesMiddleware中介軟體會對請求做相應的驗證。如果當前目錄下存在某個預設檔案,那麼它會將當前請求的URL修改成指向這個預設檔案的URL。值得注意的是,DefaultFiles
Middleware中介軟體同樣要求訪問目錄的請求路徑必須以“/”作為字尾,否則會在目前的路徑上新增這個字尾並針對最終的路徑傳送一個重定向。

public class DefaultFilesMiddleware
{
    private RequestDelegate _next;
    private DefaultFilesOptions _options;

    public DefaultFilesMiddleware(RequestDelegate next, IWebHostEnvironment env, IOptions<DefaultFilesOptions> options)
    {
        _next = next;
        _options = options.Value;
        _options.FileProvider = _options.FileProvider ?? env.WebRootFileProvider;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        //只處理GET請求和HEAD請求
        if (!new string[] { "GET", "HEAD" }.Contains(context.Request.Method, StringComparer.OrdinalIgnoreCase))
        {
            await _next(context);
            return;
        }

        //檢驗當前路徑是否與註冊的請求路徑相匹配
        PathString path = new PathString(context.Request.Path.Value.TrimEnd('/') + "/");
        PathString subpath;
        if (!path.StartsWithSegments(_options.RequestPath, out subpath))
        {
            await _next(context);
            return;
        }

        //檢驗目標目錄是否存在
        if (!_options.FileProvider.GetDirectoryContents(subpath).Exists)
        {
            await _next(context);
            return;
        }

        //檢驗當前目錄是否包含預設檔案
        foreach (var fileName in _options.DefaultFileNames)
        {
            if (_options.FileProvider.GetFileInfo($"{subpath}{fileName}").Exists)
            {
                //如果當前路徑不以“/”作為字尾,會響應一個針對“標準”URL的重定向
                if (!context.Request.Path.Value.EndsWith("/"))
                {
                    context.Response.StatusCode = 302;
                    context.Response.GetTypedHeaders().Location = new Uri(path.Value + context.Request.QueryString);
                    return;
                }
                //將針對目錄的URL更新為針對預設檔案的URL
                context.Request.Path = new PathString($"{context.Request.Path}{fileName}");
            }
        }
        await _next(context);
    }
}

由於DefaultFilesMiddleware中介軟體採用URL重寫的方式來響應預設檔案,預設檔案的內容其實還是通過StaticFileMiddleware中介軟體予以響應的,所以針對後者的註冊是必需的。也正是這個原因,DefaultFilesMiddleware中介軟體需要優先註冊,以確保URL重寫發生在StaticFileMiddleware響應檔案之前。

靜態檔案中介軟體[1]: 搭建檔案伺服器
靜態檔案中介軟體[2]: 條件請求以提升效能
靜態檔案中介軟體[3]: 區間請求以提供部分內容
靜態檔案中介軟體[4]: StaticFileMiddleware
靜態檔案中介軟體[5]: DirectoryBrowserMiddleware & DefaultFilesMi