ASP.NET Core靜態文件中間件[1]: 搭建文件伺服器

雖然ASP.NET Core是一款「動態」的Web服務端框架,但是由它接收並處理的大部分是針對靜態文件的請求,最常見的是開發Web站點使用的3種靜態文件(JavaScript腳本、CSS樣式和圖片)。ASP.NET Core提供了3個中間件來處理針對靜態文件的請求,利用它們不僅可以將物理文件發布為可以通過HTTP請求獲取的Web資源,還可以將所在的物理目錄的結構呈現出來。通過HTTP請求獲取的Web資源大部分來源於存儲在伺服器磁碟上的靜態文件。對於ASP.NET Core應用來說,如果將靜態文件存儲到約定的目錄下,絕大部分文件類型都是可以通過Web的形式對外發布的。基於靜態文件的請求由3個中間件負責處理,它們均定義在NuGet包「Microsoft.AspNetCore.StaticFiles」中,利用這3個中間件完全可以搭建一個基於Web的文件伺服器,下面做相關的實例演示。[更多關於ASP.NET Core的文章請點這裡]

目錄
一、發布物理文件
二、呈現目錄結構
三、顯示默認頁面
四、映射媒體類型

一、發布物理文件

我們創建的演示實例是一個簡單的ASP.NET Core應用,它的項目結構如下圖所示。在默認作為WebRoot的「wwwroot」目錄下,可以將JavaScript腳本文件、CSS樣式文件和圖片文件存放到對應的子目錄(js、css和img)下。WebRoot目錄下的所有文件將自動發布為Web資源,客戶端可以訪問相應的URL來讀取對應文件的內容。

1

針對具體某個靜態文件的請求是通過一個名為StaticFileMiddleware的中間件來處理的。如下面的程式碼片段所示,承載ASP.NET Core應用的程式中調用IApplicationBuilder介面的UseStaticFiles擴展方法註冊的就是這樣一個中間件。

public class Program
{
    public static void Main()
    {
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder=>builder.Configure(app => app.UseStaticFiles()))
            .Build()
            .Run();
    }
}

上述程式運行之後,就可以通過GET請求的方式來讀取對應文件的內容。請求採用的URL由目標文件的路徑決定。具體來說,目標文件相對於WebRoot目錄的路徑就是對應URL的路徑,如JPG圖片文件「~/wwwroot/img/dolphin1.jpg」對應的URL路徑為「/img/dolphin1.jpg」。如果直接利用瀏覽器訪問這個URL,目標圖片就會直接以下圖所示的形式顯示出來。

2

上面通過一個簡單的實例將WebRoot所在目錄下的所有靜態文件發布為Web資源,如果需要發布的靜態文件存儲在其他目錄下呢?下面將上面演示的應用程式的一些文檔存儲在下圖所示的「~/doc/」目錄下,那麼對應的程式又該如何編寫?

3

ASP.NET Core應用在大部分情況下都是利用一個IFileProvider對象來讀取文件的針對靜態文件的讀取請求也不例外。對於IApplicationBuilder介面的UseStaticFiles擴展方法註冊的StaticFileMiddleware中間件來說,它的內部維護著一個IFileProvider對象和請求路徑的映射關係。如果調用UseStaticFiles方法沒有指定任何參數,那麼這個映射關係的請求路徑就是應用的基地址(PathBase),對應的IFileProvider對象自然就是指向WebRoot目錄的PhysicalFileProvider對象。

上述需求可以通過顯式訂製這個映射關係的方式來實現。如下面的程式碼片段所示,我們在現有程式的基礎上額外添加了一次針對UseStaticFiles擴展方法的調用,在本次調用中指定一個對應的Options對象(一個類型為StaticFileOptions的對象)作為參數來訂製請求路徑(「/documents」)與對應IFileProvider對象(針對路徑「~/doc/」的PhysicalFileProvider對象)之間的映射關係。

public class Program
{
    public static void Main()
    {
        var path = Path.Combine(Directory.GetCurrentDirectory(), "doc");
        var options = new StaticFileOptions
        {
            FileProvider = new PhysicalFileProvider(path),
            RequestPath = "/documents"
        };
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder.Configure(app => app
                .UseStaticFiles()
                .UseStaticFiles(options)))
            .Build()
            .Run();
    }
}

按照上面這段程式指定的映射關係,對於存儲在「~/doc/」目錄下的這個PDF文件(checklist.pdf),對應URL的路徑就應該是「/documents/checklist.pdf」。如果利用瀏覽器請求這個地址時,PDF文件的內容就會按照下圖所示的形式顯示在瀏覽器上。

4

二、呈現目錄結構

上面的演示實例註冊的StaticFileMiddleware中間件只會處理針對具體的某個靜態文件的請求,如果利用瀏覽器發送一個針對目錄的請求(如「//localhost:5000/img/」),得到的將是一個狀態為「404 Not Found」的響應。如果希望瀏覽器呈現出目標目錄的結構,就可以註冊另一個名為DirectoryBrowserMiddleware的中間件。這個中間件會返回一個HTML頁面,請求目錄下的結構會以表格的形式顯示在這個頁面中。我們演示的程式可以按照如下方式調用IApplicationBuilder介面的UseDirectoryBrowser擴展方法來註冊DirectoryBrowserMiddleware中間件。

public class Program
{
    public static void Main()
    {
        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"
        };

        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder.Configure(app => app
                .UseStaticFiles()
                .UseStaticFiles(fileOptions)
                .UseDirectoryBrowser()
                .UseDirectoryBrowser(diretoryOptions)))
            .Build()
            .Run();
    }
}

當上面的應用啟動之後,如果利用瀏覽器向針對某個目錄的URL(如「//localhost:5000/」或者「//localhost:5000/img/」)發起請求,目標目錄的內容(包括子目錄和文件)就會以圖14-5所示的形式顯示在一個表格中。可以看出,在呈現的表格中,當前目錄的子目錄和文件均會顯示為鏈接。

5

三、顯示默認頁面

從安全的角度來講,利用註冊的UseDirectoryBrowser中間件會將整個目標目錄的結構和所有文件全部暴露出來,所以這個中間件需要根據自身的安全策略謹慎使用。對於針對目錄的請求,更加常用的處理策略就是顯示一個保存在這個目錄下的默認頁面。默認頁面文件一般採用如下4種命名約定:default.htm、default.html、index.htm和index.html。針對默認頁面的呈現實現在一個名為DefaultFilesMiddleware的中間件中,我們演示的這個應用就可以按照如下方式調用IApplicationBuilder介面的UseDefaultFiles擴展方法來註冊這個中間件。

public class Program
{
    public static void Main()
    {
        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,
        };

        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder.Configure(app => app
                .UseDefaultFiles()
                .UseDefaultFiles(defaultOptions)
                .UseStaticFiles()
                .UseStaticFiles(fileOptions)
                .UseDirectoryBrowser()
                .UseDirectoryBrowser(diretoryOptions)))
            .Build()
            .Run();
    }
}

下面在「~/wwwroot/img/」目錄和「~/doc」目錄下分別創建一個名為index.html的默認頁面,並且在該.html文件的主體部分指定一段簡短的文字(This is an index page!)。在應用啟動之後,可以利用瀏覽器訪問這兩個目錄對應的URL(「//localhost:5000/img/」和「//localhost:5000/documents/」),下圖顯示的就是這個默認頁面的內容。

6

必須在註冊StaticFileMiddleware中間件和DirectoryBrowserMiddleware中間件之前註冊DefaultFilesMiddleware中間件,否則它無法發揮作用。這是因為DirectoryBrowserMiddleware中間件和DefaultFilesMiddleware中間件處理的均是針對目錄的請求,如果先註冊DirectoryBrowserMiddleware中間件,那麼顯示的總是目錄的結構;如果先註冊用於顯示默認頁面的DefaultFilesMiddleware中間件,那麼在默認頁面不存在的情況下它會將請求分發給後續中間件,而DirectoryBrowserMiddleware中間件會接收請求的處理並將當前目錄的結構呈現出來。

要先於StaticFileMiddleware中間件之前註冊DefaultFilesMiddleware中間件是因為後者是通過採用URL重寫的方式實現的,也就是說,這個中間件會將針對目錄的請求改寫成針對默認頁面的請求,而最終針對默認頁面的請求還需要依賴StaticFileMiddleware中間件來完成。DefaultFilesMiddleware中間件在默認情況下總是以約定的名稱(default.htm、default.html、index.htm和index.html)在當前請求的目錄下定位默認頁面。如果作為默認頁面的文件沒有採用這樣的約定命名(如我們將默認頁面命名為readme.html),就需要按照如下方式顯式指定默認頁面的文件名。

public class Program
{
    public static void Main()
    {
        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");

        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder.Configure(app => app
                .UseDefaultFiles(defaultOptions1)
                .UseDefaultFiles(defaultOptions2)
                .UseStaticFiles()
                .UseStaticFiles(fileOptions)
                .UseDirectoryBrowser()
                .UseDirectoryBrowser(diretoryOptions)))
            .Build()
            .Run();
    }
}

四、映射媒體類型

通過上面演示的實例可以看出,瀏覽器能夠準確地將請求的目標文件的內容正常呈現出來。對HTTP協議具有基本了解的讀者應該都知道:響應文件能夠在瀏覽器上被正常顯示的基本前提是響應報文通過Content-Type報頭攜帶的媒體類型必須與內容一致。我們的實例演示了針對兩種文件類型的請求,一種是JPG文件,另一種是PDF文件,對應的媒體類型分別是image/jpg和application/pdf,那麼用來處理靜態文件請求的StaticFileMiddleware中間件是如何解析出對應的媒體類型的?

StaticFileMiddleware中間件針對媒體類型的解析是通過一個IContentTypeProvider對象來完成的,默認採用的是該介面的實現類型FileExtensionContentTypeProvider。顧名思義,FileExtensionContentTypeProvider根據文件的擴展命名來解析媒體類型。FileExtensionContentTypeProvider內部預定了數百種常用文件擴展名與對應媒體類型之間的映射關係,所以如果發布的靜態文件具有標準的擴展名,那麼StaticFileMiddleware中間件就能為對應的響應賦予正確的媒體類型。

如果某個文件的擴展名沒有在預定義的映射之中,或者需要某個預定義的擴展名匹配不同的媒體類型,那麼應該如何解決?同樣是針對我們演示的這個實例,筆者將~/wwwroot/img/ dolphin1.jpg文件的擴展名改成.img,毫無疑問,StaticFileMiddleware中間件將無法為針對該文件的請求解析出正確的媒體類型。這個問題具有若干不同的解決方案,第一種方案就是按照如下方式讓StaticFileMiddleware中間件支援不能識別的文件類型,並為它們設置一個默認的媒體類型。

public class Program
{
    public static void Main()
    {
        var options = new StaticFileOptions
        {
            ServeUnknownFileTypes = true,
            DefaultContentType = "image/jpg"
        };

        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder.Configure(app => app.UseStaticFiles(options)))
            .Build()
            .Run();
    }
}

上述解決方案只能設置一種默認媒體類型,如果具有多種需要映射成不同媒體類型的文件類型,採用這種方案就達不到目的,所以最根本的解決方案還是需要將不能識別的文件類型和對應的媒體類型進行映射。由於StaticFileMiddleware中間件使用的IContentTypeProvider對象是可以訂製的,所以可以按照如下方式顯式地為該中間件指定一個FileExtensionContentTypeProvider對象,然後將缺失的映射添加到這個對象上。

public class Program
{
    public static void Main()
    {
        var contentTypeProvider = new FileExtensionContentTypeProvider();
        contentTypeProvider.Mappings.Add(".img", "image/jpg");
        var options = new StaticFileOptions
        {
            ContentTypeProvider = contentTypeProvider
        };
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder.Configure(app => app.UseStaticFiles(options)))
            .Build()
            .Run();
    }
}

ASP.NET Core靜態文件中間件[1]: 搭建文件伺服器
ASP.NET Core靜態文件中間件[2]: 條件請求以提升性能
ASP.NET Core靜態文件中間件[3]: 區間請求以提供部分內容
ASP.NET Core靜態文件中間件[4]: StaticFileMiddleware
ASP.NET Core靜態文件中間件[5]: DirectoryBrowserMiddleware & DefaultFilesMiddleware