The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

認識 ASP․NET Core 檔案提供者與透過 Web API 下載實體檔案

昨天有位學員問到如何透過 ASP․NET Core Web API 下載檔案,原本認為這只是個簡單的問題,畢竟就只是抓個檔案嘛,哪有什麼難的,隨便 Google 兩下就有答案了。但是這個簡單的問題,卻激起我的好奇心,為什麼以往在 ASP․NET MVC 5 非常簡單的寫法,竟然到了 ASP․NET Core 就不能用了?欲知詳情,請繼續看下去!

錯誤的代碼

如果想要透過一個 Action 下載檔案,在 ASP․NET MVC 5 的年代,我們會這樣寫:

public ActionResult GetFile()
{
    var filePath = Server.MapPath("~/IMG_0215.jpg");
    return File(filePath, "image/jpeg");
}

換到 ASP․NET Core 之後,由於並沒有 Server 靜態類別可用,所以很直覺的,你可能會直接先測試看看寫死「絕對路徑」嘗試看看:

public IActionResult GetFile()
{
    return File("G:\\IMG_0215.jpg", "image/jpeg");
}

雖然可以成功建置,但是執行時卻會得到以下結果:

image

InvalidOperationException: No file provider has been configured to process the supplied file.

Microsoft.AspNetCore.Mvc.Infrastructure.VirtualFileResultExecutor.GetFileInformation(VirtualFileResult result)

很明顯的,這是一份可以建置,但卻無法執行的代碼!

循線追查原凶

將錯誤訊息到 Google/Stackoverflow 搜尋,立刻就可以找到各種建議,每一種建議都可以解決無法下載檔案的問題,但老實說,是我每個答案都不滿意,因為感覺不對!

我們就來看看網路上查到的各種答案:

  1. 由於 ControllerBase.File 總共有 24 個多型,有人就建議,當 File(String, String) 不能用的時候,換一個不就好了!

    public IActionResult GetFile()
    {
        IFileProvider provider = new PhysicalFileProvider("G:\\");
        IFileInfo fileInfo = provider.GetFileInfo("IMG_0215.jpg");
        return File(fileInfo.CreateReadStream(), "image/jpeg");
    }
    

    說實在的,原本 1 行就能寫完的代碼,現在要我寫 3 行,這感覺太怪了,不合理啊!

    我後來找到,這段程式碼的建議,完全可以改寫成以下:

    public IActionResult GetFile()
    {
        return PhysicalFile("G:\\IMG_0215.jpg", "image/jpeg");
    }
    
  2. 接著有另外一位老兄,提出了一個更加複雜的建議,雖然也真的能解決問題,但就非常不討喜,完全不可取的寫法!

    他提到了關於 File Providers 的觀念,要你先到 Startup.ConfigureServices 註冊一個 IFileProvider 到 DI 容器中:

    services.AddSingleton<IFileProvider>(new PhysicalFileProvider(Directory.GetCurrentDirectory()));
    

    然後到控制器類別的建構式注入 IFileProvider 物件:

    private readonly IFileProvider fileProvider;
    
    public CoursesController(IFileProvider fileProvider)
    {
        this.fileProvider = fileProvider;
    }
    

    接著先將圖片複製到專案根目錄下,然後透過 IFileProvider.GetFileInfo(String) 方法,取得 IFileInfo 物件,並執行 IFileInfo.CreateReadStream 方法,最後傳入 File(Stream, String) 即可:

    public IActionResult GetFile()
    {
        IFileInfo fileInfo = fileProvider.GetFileInfo("IMG_0215.jpg");
        return File(fileInfo.CreateReadStream(), "image/jpeg");
    }
    

    我的天啊!這是人寫的 Code 嗎?還我簡單且無負擔的程式碼!

研究 virtualPath 的核心原理

你從 File(String, String) 文件可以看到,他的第一個參數是 virtualPath,文件說明是 The virtual path of the file to be returned.

image

嗯!有看沒有懂!而且相關資料、範例都非常難找到!

接著,我認真的把 File Providers in ASP.NET Core 文件看完看懂後,我發現還是完全無法理解 File ProvidersvirtualPath 之間的關係。

當你在無法理解一個抽象概念的時候,你可能連想搜尋的關鍵字都不知道該怎麼輸入!

後來我真的氣到了,決定直接去翻查 ASP.NET Core 的原始碼,直接從 ControllerBase 類別的 File(string virtualPath, string contentType) 開始追查起。然後循線找到 VirtualFileResult 類別。接著再找到 VirtualFileResultExecutor 類別的 ExecuteAsync 方法。而這裡,正是可以讓我看出發生問題的主因!

畢竟這個多型,是傳入所謂的 virtualPath 進去,這是一個字串,然後執行到這裡,他最終還是要取得檔案資訊 (IFileInfo)。接著我就發現到,他有一段 GetFileProvider 方法,用來取得 virtualPath 的「預設提供者」在哪,這才發現到,原來 VirtualFileResult 的所謂的「虛擬路徑」(virtualPath)指的就是用這個檔案提供者 (File Provider) 所提供的檔案啊!

而且從原始碼可以看出,預設的檔案提供者物件為 _hostingEnvironment.WebRootFileProvider,也就是說,透過 File(String, String) 執行時,所載入的實體檔案,必須在所謂 web root 目錄下,才能正常載入檔案!

請注意: 預設 ASP․NET Core 的 web root 是位於專案根目錄下的 wwwroot 資料夾!

總結

在理解了這一切背景知識之後,我終於知道,原來透過 ControllerBase.File 輔助方法,傳入 virtualPath 的時候,是以 web root 路徑為起點 (wwwroot),我只要將需要下載的檔案,放到 wwwroot 目錄下 (或任意子目錄),就可以非常放心的寫出以下代碼,用 wwwroot 的相對路徑輸出檔案內容:

public IActionResult GetFile()
{
    return File("IMG_0215.jpg", "image/jpeg");
}

當然,這種設計可以大幅提高安全性,避免被駭客掃描作業系統檔案的風險!

雖然最終的代碼,是那麼的簡單,但是官方文件完全沒有提及這些重要的背景知識,要能理解這行簡單的代碼,花了我好幾個小時的時間,但這過程卻幫我更加理解 ASP․NET Core 的其他部分,原來 File Provider 的概念,貫穿整個 ASP․NET Core 框架,有很多 Middleware 都有用到。如此一來,未來看文件與原始碼都會更有感覺了!

相關連結

留言評論