Response Download file for Android Browser
最近在開發一個ASP.NET程式要讓用戶可以直接下載檔案時,伺服器不必實際產生一個實體路徑,而是把資料庫中的二進位資料,
透過Response.BinaryWrite(或Response.OutStream)的方式,產生至HttpResponse中讓用戶直接下載下來。
這樣的作法,可以用於常見的檔案,例如:Word/PowerPoint檔、文字檔、圖片等。
然而,這樣的網頁程式在一般網頁上是可以正常運作的,但換到Mobile平台運作時,完全就不是這麼一回事了。
那到底差異在那些地方呢?跟著該篇文章下的標題,很清楚的說明就是在Android的內鍵瀏覽器出現了問題。
根據目前測試下來的結果,Windows Phone(含Windows Mobile)、Opera Mobile/Mini、iPhone與Android(Firefox)是可以正常運作的,
使用過去熟悉的Response.BinaryWrite是可以正常把檔案下載下來,但原生的Android瀏覽器卻沒有辦法正確下載檔案內容。
其主要原因在於Android內鍵瀏覽器針對Http Header有的處理方式:
(1) 當網頁程式把二進位檔透過Response與HttpHeader中加上(attachment;filename)送給Browser進行下載檔案時,
(2) Android瀏覽器根據接收到指定網址,再一次透過GET的方式同一個網址詢問資料內容,這樣會變成GET到整個網頁的HTML內容;
上面二點是根據網路上搜尋幾個開發論壇討論的歸類,大致上大家都是往Android Browser內鍵針對HTTP Header有特殊處理的部分,
來尋找可以解決的方法。如果用上述二點的說明不太了解的話,可以透過下圖示意它:
從上圖可以清楚了解,Android Browser在接收到要進行attchment任務時,會重新向同一個網址進行GET的Request,
那我們寫在.aspx檔裡面的後端程式,將沒有辦法進行,當然就變成了直接把.aspx檔直接下載下來,而不是下載真正要的檔案。
在了解原因之後,在介紹實作方法之前,先補充針對HTTP Header在Response情形下,透過那些特殊Tag的設定可以讓瀏覽器自動識
別目前Response的結果是一般的HTML、Office檔案(如:.doc, .xls, .ppt, .pdf)、圖片(.jpg/.jpeg, .gif, .png)或壓縮檔案等。
以下分別介紹二個針對檔案下載重要的Tag:
內容類型,這個Tag用於告知瀏覽器收到的內容為何種類型,其語法如:「Content-Type: [type]/[subtype]; parameter」,
其[type]可用的型式,詳細可以參考<MIME headers - 內容類型(Content-Type)>,以下舉常見的Tag,包括:
a. Text:用於標準化地表示的本文信息,用於呈現向XHTML、html或是多種字符集和不同的格式;
b. Application:用於傳輸應用程序數據或者二進制數據;
c. Image:傳輸靜態圖片數據;
其[subtype]的部分,是指定[type]的詳細類型,正是通常會撰寫的部分,其對應表可參考IIS的MIME類型表,
裡面有相當多的檔案類型,如下圖:
透過IIS裡針對MIME類型的定義,即可以理解你的Web Server針對那些檔案類型可以通知Browser現在要給Response到底是什麼類型。
另外,常見的「application/x-download」與Office 2007系列的「application/vnd.openxmlformats-officedocument.wordprocessingml.document」,
這些雖然尚未被正式http protocol所接受,但是可以使用的。
〉Content-Disposition:
表示MIME通訊協定內容配置標題。意思是指通常在讓Response到達Browser時能夠自動產生下載的對話框時,
就需要設定該Content-Disposition,讓Header告知Browser目前Response指定的MIME類型內容,是需要被下載下來的。
撰寫格式:「Content-Disposition:attachment;filename=test.txt」。特別要注意的是後段的filename是真正
告訴Browser下載檔案在Client上使用的名稱。
[注意]
設定了這個filename,在Android內鍵的瀏覽器上,並非全部都可以使用,透過Android模擬器也一樣不會產生指定好的filename。
〉Content-Length:
顧名思義指定整個Response裡所有內容的資料長度,在透過二進位檔案提供用戶下載資料時,建議要補上Content-Length,
這樣可以告知Browser它整個Response需要擷取的資料長度有多少。
〉Content-Transfer-Encoding:
內容傳輸編號,這個Tag指定Response回傳時,使用ASCII以外的字元編碼方式,可用的型式包括:
a. 7 bit /8 bit:指定使用7 bit或8 bit的ASCII編碼方式。
b. binary:指定使用二進位資料類型的編碼方式。
通常在發佈二進位資料到Response裡時,建議將Content-Transfer-Encoding設定為:binary。
===================
在了解Http Header之後,原本我是直接修改Content-Type與Content-Disposition的識別來調整下載的方式,都發現是不可行的。
並且在參考到<Android 2.1對於HTTP Header的限制>這一篇內容時,發現Android瀏覽器(底層是WebKit)而言,針對Content-Type與
Content-Disposition這些HTTP Header裡的管理,從Android 2.1開始變得嚴謹,它會根據Header中針對強迫Browser下載的工作,
再輾轉產生一個新的Request(GET)向原網址詢問下載的任務,此時,會變成直接把網頁(.aspx)當成實際要下載的檔案給下載下來。
由於Android瀏覽器會有重新產生一個新的Request(GET)的原因, 於是想了一個好玩的解決方法,如下圖:
主要實作二個網頁(.aspx),第一頁的Request.aspx提供用戶選擇想要下載的檔案清單,等著用戶送出第一頁的
Request(POST)向Server要求取得檔案時,Request.aspx將網頁導向(Response.Redirect)Download.aspx,至於
導向過去的用意是什麼呢?=>正是要解決Android瀏覽器會送出2次Request來取得檔案的問題,如步驟(3到4)。
然而,實際的作法,在Redirect過去Download.aspx時,你可以把一些關鍵資料,例如:檔案名稱或Content-Type,
夾進QueryString中送給Download.aspx,這樣Download.aspx即使被重覆下2次以上的Request也不會遺失要下載的資訊。
〉範例實作說明
(1) 實作透過二個Page的方式,結合Response.Redirect將Download檔案的功能,移轉到另一個.aspx來完成;
‧Request.aspx (畫面中一個ListBox放置要測試用的Content-Type、一個TextBox輸入要下載的檔案、一個按鈕準備下載)
1: <body>
2: <form id="form1" runat="server">
3: <div>
4: <table>
5: <tr>
6: <td>File Name:</td>
7: <td>
8: <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
9: </td>
10: </tr>
11: <tr>
12: <td>Content-Type:</td>
13: <td>
14: <asp:ListBox ID="ListBox1" runat="server" Width="208px">
15: <asp:ListItem>application/msword</asp:ListItem>
16: <asp:ListItem>application/vnd.ms-powerpoint</asp:ListItem>
17: <asp:ListItem>application/vnd.ms-excel</asp:ListItem>
18: <asp:ListItem>application/pdf</asp:ListItem>
19: <asp:ListItem>image/jpeg</asp:ListItem>
20: <asp:ListItem>image/png</asp:ListItem>
21: <asp:ListItem>image/gif</asp:ListItem>
22: <asp:ListItem>text/plain</asp:ListItem>
23: <asp:ListItem>text/xml</asp:ListItem>
24: <asp:ListItem>application/vnd.openxmlformats-officedocument.wordprocessingml.document</asp:ListItem>
25: <asp:ListItem>application/vnd.openxmlformats-officedocument.spreadsheetml.sheet</asp:ListItem>
26: <asp:ListItem>application/vnd.openxmlformats-officedocument.presentationml.presentation</asp:ListItem>
27: </asp:ListBox>
28: </td>
29: </tr>
30: <tr>
31: <td></td>
32: <td>
33: <asp:Button ID="btnDownload" runat="server" Text="Download"
34: onclick="btnDownload_Click" />
35: </td>
36: </tr>
37: </table>
38: </div>
39: </form>
40: </body>
‧Request.aspx.cs
1: protected void btnDownload_Click(object sender, EventArgs e)
2: {
3: //將要下載的檔案名稱由TextBox取出來,並且加上選擇要使用的Content-Type
4: //組合進去QueryString中,讓Android重覆送出的Reuqest都可以取得到要下載的檔案
5: string tFile = string.Format("Download.aspx?FileName={0}&Content={1}", TextBox1.Text, ListBox1.SelectedItem.Value);
6: Response.Redirect(tFile);
7: }
此頁主要使用的方式,是把用戶選擇要下載的檔案名稱與Content-Type當進參數放進QueryString傳到Download.aspx中,
讓Android瀏覽器在重新向Download.aspx下達Request時,都可以讓Download.aspx取得到在Request.aspx中的指定值。
‧Download.aspx.cs
Download.aspx其實是空白,不需要放進任何的HTML控件,因為該頁的用途只為了將資料庫中的二進位資料取得出來,並且放進
Response中,告知Browser準備下載檔案。
1: protected void Page_Load(object sender, EventArgs e)
2: {
3: if (Request.QueryString["FileName"] != string.Empty)
4: {
5: //取得檔案名稱與Content-Type
6: string tFile = Request.QueryString["FileName"].ToString();
7: string tContent = Request.QueryString["Content"].ToString();
8:
9: //使用ReadFile()取得二進位資料,配合CreateFile將二進位資料寫入Response中
10: CreateFile(ReadFile(tFile), tFile, tContent);
11: }
12: }
13:
14: private void CreateFile(byte[] pBytes, string pFileName, string pContent)
15: {
16: Response.Clear();
17: //設定用戶選擇的Content-Type
18: Response.ContentType = pContent;
19: Response.AddHeader("Content-Transfer-Encoding", "Binary");
20: //設定HttpHeader告知Browser要下載的任務
21: Response.AddHeader("Content-Disposition", "attachment;filename='" + pFileName + "'");
22: Response.AddHeader("Content-Length", pBytes.Length.ToString());
23: //將byte[]根據指定的檔名,寫入Response中。
24: Response.OutputStream.Write(pBytes, 0, pBytes.Length);
25: Response.End();
26: }
‧結果圖示
=>
透過這個方法可以讓檔案確實下載下來,進行測試的機型(HTC Desire HD與HTC Hero)二台裡面,HTC Desire HD可以
正常下載檔案(檔案也是正確的)下來,但在HTC Hero(或Android模擬器,如上圖3)下載下來的檔案雖然正確,但檔名卻是網頁檔案名稱。
舉例來說:
今天下載的檔案是123.jpg,而瀏覽器連結到的網址是/Donwload.aspx,雖然下載下來的檔案內容是對的,但檔名卻是Download.jpg。
(2) 透過實作IHttpHandler介面,讓檔名放進URL裡,讓自訂的HttpHandler處理URL裡的檔案,解決正確檔名下載;
即然知道某些Android瀏覽器會直接按照網頁檔案名稱做為下載檔案的名稱,那換個作法,把用戶要下載的檔案名稱,
直接放進URL中,例如:將/Download.aspx變成"123.aspx",但由於我們的網站中沒有123.aspx,這樣讓瀏覽器直接去連結
的話,一定會取得404的錯誤訊息。因此,我們需要客製一個HttpHandler,需要實作IHttpHandler介面,讓HttpHandler可以
針對我們指定的副檔名進行處理。
a. 實作一客製HttpHandler,命名為:AndroidDownload
該HttpHandler的任務是做什麼呢?其實就是把Downloasd.aspx要處理的任務,如:取得資料庫內容與檔案名稱放進Response中,
完全移植過來。
1: public class AndroidDownload : IHttpHandler, IRequiresSessionState
2: {
3: //暫存httpContext
4: private HttpContext gContext = null;
5:
6: #region IHttpHandler 成員
7:
8: public bool IsReusable
9: {
10: get { return true; }
11: }
12:
13: //專門處理接收到Request時要完成的任務
14: public void ProcessRequest(HttpContext context)
15: {
16: gContext = context;
17: if (gContext.Request.QueryString["FileName"] != string.Empty)
18: {
19: string tFile = gContext.Request.QueryString["FileName"].ToString();
20: string tContent = gContext.Request.QueryString["Content"].ToString();
21: CreateFile(ReadFile(tFile), tFile, tContent);
22: }
23: }
24:
25: #endregion
26:
27: private void CreateFile(byte[] pBytes, string pFileName, string pContent)
28: {
29: gContext.Response.Clear();
30: gContext.Response.ContentType = pContent;
31: gContext.Response.AddHeader("Content-Transfer-Encoding", "Binary");
32: gContext.Response.AddHeader("Content-Disposition", "attachment;filename='" + pFileName + "'");
33: gContext.Response.AddHeader("Content-Length", pBytes.Length.ToString());
34: gContext.Response.OutputStream.Write(pBytes, 0, pBytes.Length);
35: gContext.Response.End();
36: }
37: }
b. 設定Web.config與設定IIS支援客製的HttpHandler
b-0. 設定Web.config讓站台可以針對我們指定的副檔名,交由實作的HttpHandler來處理;
1: //新增一組httpHandler針對*.and副檔名的內容,配合實作的AndroidDownload類別進行處理;
2: <httpHandlers>
3: <add verb="*" path="*.and" type="WebBytesDownload.AndroidDownload" validate="false"/>
4: </httpHandlers>
b-1. 啟動IIS,選擇Web站台,選擇處理常式對應,如下圖:
b-2. 新增一組管理的處理常式,點選「新增Managed處理常式」;
‧結果圖示
如上圖可見,透過實作httpHandler,將指定的檔名放入url中,當作實際要下載的檔案名稱,確實可以解決檔名不正確的問題。
===================
以上提供的做法的確可以解決Android內鍵的Browser無法透過Respones.OutStream下載的問題,
但是仍然有些問題存在,例如:當檔名是中文時,由於把檔名放進了URL,理所當然中文會被Encoding,
這樣就會造成檔名變成編譯後的結果,這樣下載下來的檔案就不是當初所要的樣子了。
這個問題我還沒有想到怎麼解。如果大家看完覺得有其他方法可以解的話,也不妨給我些建議。
這篇文章分享給大家。希望對大家有所幫助。
References:
‧Asp.net Download file on mobile platform
‧http post request sent from andriod to ashx in asp.net. cannot recieve data
‧Best way to stream files in ASP.NET
‧移动项目开发笔记(asp.net中Response.WriteFile()实现文件下载) & 【转】Response.WriteFile无法下载大文件
‧MIME Reference & Content Type 大全 (必讀)
‧如何利用Response.OutputStream(vb)做到下載檔案時多點續傳的功能? & 有辦法以Response.WriteFile的方式開啟網路上的檔案嗎?
‧android phone download issue from asp.net website
‧Downloading and Uploading Files & File Download in ASP.Net With C#
‧Issue 1978: Browser: double download of dynamically generated content