通過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();