Response Download file for Android Browser

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有特殊處理的部分,

來尋找可以解決的方法。如果用上述二點的說明不太了解的話,可以透過下圖示意它:

image

從上圖可以清楚了解,Android Browser在接收到要進行attchment任務時,會重新向同一個網址進行GET的Request

那我們寫在.aspx檔裡面的後端程式,將沒有辦法進行,當然就變成了直接把.aspx檔直接下載下來,而不是下載真正要的檔案。

 

在了解原因之後,在介紹實作方法之前,先補充針對HTTP Header在Response情形下,透過那些特殊Tag的設定可以讓瀏覽器自動識

別目前Response的結果是一般的HTML、Office檔案(如:.doc, .xls, .ppt, .pdf)、圖片(.jpg/.jpeg, .gif, .png)或壓縮檔案等。

以下分別介紹二個針對檔案下載重要的Tag:

Content-Type

內容類型,這個Tag用於告知瀏覽器收到的內容為何種類型,其語法如:「Content-Type: [type]/[subtype]; parameter」,

其[type]可用的型式,詳細可以參考<MIME headers - 內容類型(Content-Type)>,以下舉常見的Tag,包括:

a. Text:用於標準化地表示的本文信息,用於呈現向XHTML、html或是多種字符集和不同的格式;

b. Application:用於傳輸應用程序數據或者二進制數據;

c. Image:傳輸靜態圖片數據;

其[subtype]的部分,是指定[type]的詳細類型,正是通常會撰寫的部分,其對應表可參考IIS的MIME類型表,

裡面有相當多的檔案類型,如下圖:

image

透過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)的原因, 於是想了一個好玩的解決方法,如下圖:

image

主要實作二個網頁(.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: }

 

‧結果圖示

圖1.00 圖2.HTC Desire HD 圖3. Android Enumlator

=>

透過這個方法可以讓檔案確實下載下來,進行測試的機型(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站台,選擇處理常式對應,如下圖:

02

b-2. 新增一組管理的處理常式,點選「新增Managed處理常式」;

03

 

‧結果圖示

圖1.00圖2.05

如上圖可見,透過實作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 Response Object

移动项目开发笔记(asp.net中Response.WriteFile()实现文件下载) & 【转】Response.WriteFile无法下载大文件

HttpResponse.OutputStream 屬性

Android實現下載文件SD Card的寫入

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

Test Cases for HTTP Content-Disposition header field and the Encodings defined in RFC 2047 and RFC 2231/5987

Mozilla/5.0 (Linux; U; Android 1.0; en-us; dream) AppleWebKit/525.10+ (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2