由ASP.NET Core根據路徑下載檔案異常引發的探究

2022-06-29 12:01:42

前言

    最近在開發新的專案,使用的是ASP.NET Core6.0版本的框架。由於專案中存在檔案下載功能,沒有使用類似MinIOOSS之類的分散式檔案系統,而是下載本地檔案,也就是根據本地檔案路徑進行下載。這其中遇到了一個問題,是關於如何提供檔案路徑的,通過本文記錄一下相關總結,希望能幫助更多的同學避免這個問題。

使用方式

由於我們的系統沒有公司內部使用的也沒有做負載均衡之類的,所以檔案是儲存在當前伺服器中的,所以我們直接使用檔案絕對路徑的方式來進行下載的,使用的是ASP.NET Core自帶的File方法,使用的是如下方法(實際上檔案的路徑是儲存在資料庫中的)

[HttpGet]
[Produces("application/msword", Type = typeof(FileResult))]
public FileResult Virtual()
{
    // AppContext.BaseDirectory用來獲取當前執行程式的基目錄
    // 結果為絕對路徑,比如 D:\CodeProject\MyTest.WebApi\bin\Debug\net6.0\
    var filePath = Path.Combine(AppContext.BaseDirectory, "files/疫情防控規範說明.docx");
    return File(filePath, "application/msword", "疫情防控規範說明.docx");
}

這是比較常用的方式沒太在意會有什麼問題,不過,等自測的時候發現報了一個System.InvalidOperationException異常,大致內容如下所示

 System.InvalidOperationException: No file provider has been configured to process the supplied file.
         at Microsoft.AspNetCore.Mvc.Infrastructure.VirtualFileResultExecutor.GetFileInformation(VirtualFileResult result, IWebHostEnvironment hostingEnvironment)
         at Microsoft.AspNetCore.Mvc.Infrastructure.VirtualFileResultExecutor.ExecuteAsync(ActionContext context, VirtualFileResult result)
         at Microsoft.AspNetCore.Mvc.VirtualFileResult.ExecuteResultAsync(ActionContext context)

看異常內容問題是出在VirtualFileResultExecutor.GetFileInformation()方法,它的意思大概是沒有提供檔案提供來處理檔案,對於檔案提供程式如果瞭解過ASP.NET Core靜態檔案相關的話應該是瞭解這個的。如果想存取ASP.NET Core中的靜態檔案,預設是不可以直接存取的,這也是一種安全機制,想使用的話必須開啟靜態檔案存取機制,且預設的靜態檔案要儲存在wwwroot路徑下。如果想在其它路徑提供靜態檔案則必須要提供檔案處理程式,我們常用的方式則是

var fileProvider = new PhysicalFileProvider($"{env.ContentRootPath}/staticfiles");
app.UseStaticFiles(new StaticFileOptions {
    RequestPath="/staticfiles",
    FileProvider = fileProvider
});

同樣的在這裡我們也需要提供IFileProvider範例,因為我們是使用的本地檔案系統,所以要提供PhysicalFileProvider範例,通過下面方法解決了這個問題

[HttpGet]
[Produces("application/msword", Type = typeof(FileResult))]
public FileResult Virtual()
{
    var filePath = "files/疫情防控規範說明.docx";
    return new VirtualFileResult(filePath, "application/msword")
    {
        // 提供指定目錄的檔案存取程式
        FileProvider = new PhysicalFileProvider(AppContext.BaseDirectory),
        FileDownloadName = "疫情防控規範說明.docx"
    };
}

亦或者是通過原始的方式,比如讀取檔案的Stream或者byte[]的方式

[HttpGet]
[Produces("application/msword", Type = typeof(FileResult))]
public FileResult Virtual()
{
    //讀取byte[]方式
    var filePath = Path.Combine(AppContext.BaseDirectory, @"files\疫情防控規範說明.docx");
    var fileBytes = System.IO.File.ReadAllBytes(filePath);
    return File(fileBytes, "application/msword", "疫情防控規範說明.docx");

    //讀取Stream的方式
    //var filePath = Path.Combine(AppContext.BaseDirectory, @"files\疫情防控規範說明.docx");
    //var fileStream = System.IO.File.OpenRead(filePath);
    //return File(fileStream, "application/msword", "疫情防控規範說明.docx");
}

通過這些方式雖然可以解決問題,但是看起來不是很優雅,而且如果提供不同路徑的檔案還得要有許多的PhysicalFileProvider範例,或者自己封裝方法去解決問題。
當時就想微軟不至於連讀取自定義物理路徑的方法都不提供吧,於是就在ControllerBase基礎類別中查詢相關方法,終於看到了一個叫PhysicalFile的方法,看名字就知道是提供物理檔案用的,不知道行不行寫程式碼試了試,程式如下

[HttpGet]
[Produces("application/msword", Type = typeof(FileResult))]
public FileResult Physical()
{
    var filePath = Path.Combine(AppContext.BaseDirectory, "files/疫情防控規範說明.docx");
    return PhysicalFile(filePath, "application/msword", "疫情防控規範說明.docx");
}

結果還真的是可以,這個方法呢提供的檔案路徑可以是檔案的絕對路徑,而不需要提供別的檔案提供程式。這就勾起了我的好奇心,為啥兩個操作還不一樣呢,為啥有這樣的區別?

原始碼探究

通過上面遇到的問題知道了如果想提供絕對路徑的檔案下載需要使用PhysicalFile方法去下載,而預設的File方法則不能直接下載絕對路徑的檔案,懷揣著好奇心,大概看了一下這兩個方法的相關原始碼實現。

VirtualFileResult

接下來咱們就來看一下方法的定義在ControllerBase類中,現在來看一下File(string virtualPath, string contentType, string? fileDownloadName)方法的定義[點選檢視原始碼