最近在測試 Cordova-App-Loader,他可以透過 manifest.json 去判斷 App 是否有檔案需要更新,再向遠端的 Server 要下載資料,原本套件的做法是可以透過 HTTP 去檢查遠端的 manifest.json 是否和 App 內部所記錄的有差異,然後再透過同一組 HTTP URL 去抓取要更新的檔案。

而我打算透過 WebAPI 提供這樣的資源服務,因此寫了一個 UpdateController ,去檢查 manifest.json ,以其之後可以增加權限控制,避免任何知道此連結的人都可以下載的到相關的檔案,也避免暴露網站資料夾結構。

//GET api/Update/Manifest
[Route("api/Update/Manifest")]
[HttpGet]
public HttpResponseMessage GetManifest()
{
    var filePath = HostingEnvironment.MapPath(@"~/Update/manifest.json");
    if (File.Exists(filePath))
    {
        var fileJson = File.ReadAllText(filePath);
        response = Request.CreateResponse(HttpStatusCode.OK);
        response.Content = new StringContent(fileJson, Encoding.UTF8, "application/json");
        return response;
    }
    response = Request.CreateResponse(HttpStatusCode.NotFound);
    return response;
}

另外,在下載更新檔案的動作也用類似的作法,但 filename 可能包含了資料夾路徑,且會回應正確 MIME 型別的檔案

//GET api/Update/js/app.js
[Route("api/Update/{*filename}")]
[HttpGet]
public HttpResponseMessage GetUpdateFile(string filename)
{
    var filePath = HostingEnvironment.MapPath(@"~/Update/" + filename);
    var mimeType = System.Web.MimeMapping.GetMimeMapping(filename);
    if (File.Exists(filePath))
    {
        var fs = new FileStream(filePath, FileMode.Open);
        response = Request.CreateResponse(HttpStatusCode.OK);
        response.Content = new StreamContent(fs);
        response.Content.Headers.ContentType = new MediaTypeHeaderValue(mimeType);
        return response;
    }
    response = Request.CreateResponse(HttpStatusCode.NotFound);
    return response;
}

在這兩個下載檔案的動作中,有兩點需要特別注意:

  1. 檔案 MIME 型別
  2. 路由參數的參數包含 /.

檔案 MIME 型別

每個檔案類型都有自己對應的 MIME 型別,如果你不指定回傳的 MIME 型別,只想說丟一串 string 回傳,那麼需求發起者會預設的型別 text/html ,而不是預期的 JSON 格式(雖然那串字串長得跟 JSON 一模一樣),就不遑論其他的檔案類型了。

所幸 .NET Framework 4.5 的 System.Web.dll 中,有可以幫我們輕鬆找出對應的 MIME 型別的功能 MimeMapping.GetMimeMapping Method,透過下面這段範例程式碼,即可輕鬆取得該檔案的 MIME 類型。

string mimeType = System.Web.MimeMapping.GetMimeMapping(fileName);

如果你使用的是 IIS 整合管線模式(integrated pipeline,大部分的人都是用此模式吧),那麼你可以比對到 IIS 中所有的 MIME 清單,若是傳統模式(classic mode),則是會參考內建的對照檔,約可以比對出常用的300多筆 MIME 型別。

路由參數的參數包含 /.

另外一個注意事項「路由參數的參數包含 /.」,我們都知道 WebAPI 會將 / 視為資源 URI 的分隔符號,而 . 則是 URI 的檔案副檔名符號。當我們想要將 js/app.js 當作是參數傳給 Controller 時,就必須要特別注意了。

首先解決 / 的問題

解法很簡單,只要在參數前面加一個萬用字元 * 就可以了。

[Route("api/Update/{*filename}")]

但這樣的用法是因為這次 Cordova-App-Loader 才會這樣用,因為使用萬用字元了話,上面這段就會被解釋成:api/Update/ 之後接的字串都當成參數值傳給 filename,因此就不能在後面接其他路由設定了。

另一個問題 .

這問題主要是因為我們預期這段 api/Update/js/app.js URL 應該會經過 WebAPI 做檢查,但以執行周期來說,IIS 會先對此 URL 做檢查,檢查是否有這個檔案資源,如果沒有了話,就吐 404 錯誤。這樣的執行週期,就不是我們預期的了。

比較通用的解法是設定這個 Web 應用程式,任何的 request 都經由 ASP.NET 去做處理,這代表所有靜態的檔案(圖片、PDF…)都會經過 ASP.NET 去處理,當然這樣的動作其實是不必要的,因此會損失一些系統效能。

這通用的做法很輕鬆,在 web.config 的 <system.webServer> 加入一行設定即可。

<configuration>
    <system.webServer>
        <modules runAllManagedModulesForAllRequests="true" />

另外還有一種解法,就比較精確,透過修改 HTTP handler 指定特定路徑被請求時,轉由 ASP.NET 去處理。

這種不修改 RAMMFAR (runAllManagedModulesForAllRequests)的方式是比較優秀的解法,可以避免效能的損失,也可精確的針對特定 URI 做控制。

<add name="ApiURIs-ISAPI-Integrated-4.0"
     path="/people/*"
     verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS"
     type="System.Web.Handlers.TransferRequestHandler"
     preCondition="integratedMode,runtimeVersionv4.0" />

參考資料:


Poy Chang

Trial and Error