WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

MVC でファイルのダウンロード

by WebSurfer 2018年12月12日 16:06

ASP.NET MVC アプリケーションでファイルをダウンロードするのはどうすればいいかということを備忘録として書いておきます。(Web Forms アプリの場合は先の記事「ダウンロードは HTTP ハンドラで」を見てください)

IE11 の通知バー

上の画像のように通知バーを表示し(IE11 の場合です)、ファイル名とファイルの種類をブラウザにきちんと認識させるのが条件です。

基本的には以下のアクションメソッド FileDownload のようにします。

public ActionResult FileDownload()
{
    string fileName = Server.MapPath("~/Files/test.pdf");
    return File(fileName, "application/pdf", "test.pdf");
}

上のコードの return File(...); のヘルパーメソッド File は、FileResult クラスを継承した FilePathResult, FileContentResult, FileStreamResult 派生メソッドのいずれかを呼び出します。

それらの使い分けは以下の通りです。

  1. FilePathResult: コンテンツが既存のファイルとして提供される。
  2. FileStreamResult: コンテンツがストリームとして提供される。
  3. FileContentResult: コンテンツがバイト配列として提供される。

上記のどれを呼び出すかは return File(...); の引数によります。上の記事の例では第一引数が文字列(ファイルの物理パス)なので FilePathResult が呼び出されます。

FilePathResult は Web Forms アプリでもファイルをダウンロードする際に使われている TransmitFile メソッドを呼び出します。TransmitFile メソッドは、FilePathResult の第 1 引数に指定されたファイル名を受けて、そのファイルをメモリにバッファリングせずに、HTTP 応答出力ストリームに直接書き込みます。

FileStreamResult は、第 1 引数に指定されるストリームから FileStream.Read メソッドでバイト列を取得し、それを HttpReponse.OutputStream.Write メソッドを使って出力ストリームに書き出すという操作を行います。

FileContentResult は、第 1 引数に指定されるバイト列を直接 HttpReponse.OutputStream.Write メソッドを使って出力ストリームに書き出すという操作を行います。

ファイル名とファイルの種類をブラウザにきちんと認識させ、上の画像のような通知バーを出させるには、第 2, 3 引数に順にファイルの MIME タイプ、拡張子を含むファイル名を設定します。そうすることにより、応答ヘッダに Content-Type と Content-Disposition が適切に設定されます。

なお、第 2 引数に設定するファイル名には US-ASCII 文字を使用しないと IE では文字化けするので注意してください。先の記事「ダウンロードファイル名の文字化け」に書いた方法でどのように対応できるかは未調査です。

キャッシュコントロールは Web Forms アプリと同様に HttpResponse.Cache で取得できる HttpCachePolicy オブジェクトを使って可能です。

例えば、上の FileDownload アクションメソッドに以下のコードを追加してやれば、

Response.Cache.SetCacheability(HttpCacheability.NoCache);
Response.Cache.SetExpires(DateTime.Now.ToUniversalTime());
Response.Cache.SetMaxAge(new TimeSpan(0, 0, 0, 0));

以下の画像の応答ヘッダのように Cache-Control: no-cache, Pragma: no-cache, Expires: -1 が設定されます。

応答ヘッダ

File メソッドの第 2, 3 引数に設定した MIME タイプとファイル名により、Content-Type と Content-Disposition が適切に設定されているところにも注目してください。

Tags:

Upload Download

ダウンロードは HTTP ハンドラで

by WebSurfer 2013年2月16日 16:50

画像、データ、ファイルなど、Page でないコンテンツをダウンロードするには、.aspx ページを利用するより、HTTP ハンドラ(.ashx)を利用したほうが良さそうだという話です。

.aspx ページを利用してファイルをダウンロードするサンプルは、MSDN ライブラリなどでもよく目にしますが、いろいろ問題がありそうです。

MSDN ライブラリの HttpResponse クラス にある画像をダウンロードするサンプルコードを例に取って問題点を説明します。

このコードでは、まず bmp.Save(Response.OutputStream, ImageFormat.Jpeg) メソッドによって画像データが jpeg 形式で出力ストリームに保存され、その後 Response.Flush メソッドによって保存された画像データが chunked コーディングされてクライアントに送信されます。

そのあと直ちに、データの終了を示す 0 が送信されればいいのですが、サーバーは <!DOCTYPE ... で始まる html コードも生成し、それもクライアントに送信してから終了します。

従って、jpeg 形式のデータとしては余計な html コードが含まれてしまいます。IE9 で試した限りでは画像は表示されましたが、すべてのブラウザで問題ないかは保障の限りではありません。

以下のような解決策を考えましたが、.aspx ページを利用する限り決定打はなさそうです。

  1. Response.Flush の後、Response.Close する ⇒ 時々そのようなサンプルを目にしますが、これは解決策というよりは改悪です。Response.Close は "クライアントへのソケット接続を閉じます" ということなので、 chunked コーディングでデータの終了を示す 0 が送信されないまま終了してしまいます。IE9 でしか試してませんが、画像は表示されません。
  2. Response.Flush に替えて Response.End を使う ⇒ 応答ヘッダに Content-Length が指定されて(chunked コーディングされず)、データはまとめて送信されます。送信されるデータの内容も OK です。ただし、最近知ったのですが、.NET 4 以降の MSDN ライブラリの説明で、End メソッドの使用は非推奨になっています。理由は、End メソッドでスローされる ThreadAbortException がパフォーマンスに悪影響を及ぼすからだそうです。代わりに HttpApplication.CompleteRequest メソッドを呼び出せとのことです。
  3. Response.Flush に替えて HttpApplication.CompleteRequest を呼び出す ⇒ やはり <!DOCTYPE ... で始まる html コードも送信されてしまいます。Response.Flush と異なる点は、chunked コーディングされず、Content-Length が指定されてデータが(html コードも含めて)まとめて送信されることです。
  4. html コードを全部削除し、Flush メソッドも End メソッドも使わない ⇒ これは見かけよさそうです。余計な html コードはくっついてきません。
  5. Response.Flush の後 Response.SuppressContent を true に設定する(2016/6/28 追記) ⇒ そうすると画像のみが送信され、<!DOCTYPE html... 以下は送信されません。ただし、Response.Flush の前で true に設定するとコンテンツは一切送信されない(ヘッダと chunked の終わりを示す 0 のみ送信される)ので注意してください。

.aspx をページを使う場合は上記 4 または 5 の方法がよさそうですが、普通と違うことをして思わぬところで副作用が出る可能性が否定しきれません。.aspx をページを使って余計な心配をするより、代わりに http ハンドラ(.ashx)を使った方が良さそうです。http ハンドラなら、Response.End メソッドで強制する等の処置は必要なく、HTTP パイプラインの最後まで普通に実行させれば済みます。

http ハンドラを使ったサンプルを書いておきます。コメントに注意事項を書いたので参考にしてください。

(2014/3/2 追記:IE の場合は UrlEncode を使ってファイル名をエンコードしていますが、半角空白は "+" に変換されるので、ブラウザ側ではそのまま "+" になってしまいます。それが気に入らない場合は、ダウンロードファイル名の文字化け に紹介したサンプルコードを参考に対処してください)

<%@ WebHandler Language="C#" Class="Handler" %>

using System;
using System.Web;

public class Handler : IHttpHandler 
{
    public void ProcessRequest (HttpContext context) 
    {
        HttpResponse response = context.Response;
        HttpRequest request = context.Request;
        
        string fileName = "日本語.txt";        
        
        // IE の場合、日本語ファイル名の文字化け対策が必要。
        // Firefox, Chrome, Safari, Opera の場合は不用。

        // 2014/3/3 修正
        // IE11 では Browser プロパティは "Mozilla" (.NET 2.0)
        // または "InternetExplorer" (.NET 4) になる。IE の場合
        // User Agent には必ず "Trident" と言う文字列が入ってい
        // るらしいので、そちらで判定した方がよさそう。
        if (request.Browser.Browser.ToUpper().IndexOf("IE") >= 0
            || request.UserAgent.Contains("Trident"))
        {
            fileName = context.Server.UrlEncode(fileName);
        }
        
        // キャッシュを許可するか否か、許可する場合は有効期限を
        // 指定しておくべき。
        // 以下のコードはキャッシュを許可しない場合の例。応答ヘ
        // ッダーは次のようになる。
        //    Cache-Control: no-cache
        //    Pragma: no-cache
        //    Expires: -1
        response.Cache.SetCacheability(HttpCacheability.NoCache);
        response.Cache.SetExpires(DateTime.Now.ToUniversalTime());
        response.Cache.SetMaxAge(new TimeSpan(0, 0, 0, 0));

        // ブラウザによってファイルの種類を判断する方法が異なる。
        // IE は、Content-Disposition: ヘッダが存在する場合は、
        // filename パラメータで設定されたファイル名の拡張子を
        // 優先的に使う。Content-Type: ヘッダの指定は無視される。
        // 逆に、Opera など、Content-Type: ヘッダの指定を優先的
        // に使うものもある。なので、Content-Disposition: ヘッダ
        // と Content-Type: ヘッダの両方を正しく指定しておいた方
        // がよさそう。        
        response.AppendHeader("Content-Disposition",
            "attachment;filename=" + fileName);
        response.ContentType = "text/plain";

        // 文字列 "Hello World" を応答 HTTP ストリームに書込み。
        // ファイルの場合は TransmitFile メソッドを使うのがお勧
        // め(WriteFile メソッドは大きなファイルは扱えないので)
        response.Write("Hello World");
        
        // Flush, End, Close メソッド等は使用しないこと。
    }
 
    public bool IsReusable 
    {
        get 
        {
            return false;
        }
    }

}

呼び出し方は、a 要素の href 属性に HTTP ハンドラの URL を設定するのがよさそうです。

Tags: ,

Upload Download

ダウンロードファイル名の文字化け

by WebSurfer 2011年3月20日 12:33

2014/3/2 改版
IE9 より RFC 6266 (RFC 2231/RFC 5987) がサポートされたということで、その点を考慮してこの記事を全面書き換えました。

ASP.NET Web アプリでファイルをダウンロードする際、AppendHeader メソッドを使って Content-Disposition ヘッダフィールドの attachment; filename= に日本語のファイル名を設定し、受信側のブラウザに IE を使用すると、ファイル名が以下の画像に示すように文字化けします。(以下の画像の例では、オリジナルのファイル名を "日本語のファイル名.xls" としています)

ダウンロードファイル名の文字化け

その理由は MSDN Blog の記事 Downloads and International Filenames をそのまま引用すると "Early versions of Internet Explorer worked around this limitation by assuming that any non-ASCII characters within HTTP headers were encoded using the local system's Windows codepage" ということだそうです。

上記の英文を具体的に説明すると次の通りです。

例えば以下のコードのよう AppendHeader メソッドを使ってファイル名を設定すると、応答ヘッダで "日本語" の部分は UTF-8 のバイト列 e6 97 a5 e6 9c ac e8 aa 9e となります。(Fiddler2 の HexView で見ると分かります。ASP.NET Web アプリではデフォルトでは応答ヘッダはすべて UTF-8 になりますので、ファイル名の部分も UTF-8 のバイト列として送信されます)

Response.AppendHeader("Content-Disposition",
      "attachment;filename=日本語.xls");

上の MSDN Blog に書いてあるように、応答ヘッダに ASCII コード以外の文字(上の例では e6 97 a5 e6 9c ac e8 aa 9e)がある場合、その部分は自分の PC の Windows codepage で、即ち日本語 OS の場合は Shift_JIS として解釈されます。

IE は e6 97 a5 e6 9c ac e8 aa 9e を Shift_JIS コードとして解釈しようとするので、正しくデコードできず文字化けするというわけです。(ちなみに、Shift_JIS では "日本語" は 93 fa 96 7b 8c ea になります)

解決策は、マイクロソフト サポートオンラインの記事 ファイルをダウンロードする ASP.NET ページで日本語ファイル名が文字化けする に以下の 3 つの方法が紹介されています。

  1. HttpServerUtility.UrlEncode メソッドを使ってファイル名をエンコードする。
  2. HttpResponse.HeaderEncoding プロパティを使って Shift_JIS のヘッダを送信する。
  3. RFC 6266 に準拠して attachment; filename*= UTF-8''%e2%82%ac%20rates のようにする。

1 番目の方法は、一部のブラウザが対応していないという点が問題です。IE, Firefox, Chrome, Safari, Opera で検証した結果は以下の表の通りでした。

ブラウザ UrlEncode
あり なし
IE OK NG
文字化けする
Firefox NG
エンコードされた文字になる
OK
Chrome OK OK
Safari NG
エンコードされた文字になる
OK
Opera OK OK

上記の結果は、最初にこの記事を書いた 2011 年 3 月 20 日の時点で検証に使ったブラウザ IE8, Firefox 3.6.15, Chrome 10.0.648.151, Safari 5.0.4, Opera 11.01 でも、この記事を改版した 2014 年 3 月 2 日の時点で検証に使ったブラウザ IE9, Firefox 27.0.1, Chrome 33.0.1750.117, Safari 5.1.7, Opera 12.16 でも同じでした。

Firefox と Safari の場合は、UrlEncode するのはダメで、%e6%97%a5%e6%9c%ac%e8%aa%9e.xls のようなエンコードされた名前になってしまいます。Chrome と Opera は UrlEncode 有り/無しどちらも OK でした。

また、IE の場合でも、ファイル名に半角空白が含まれているとうまく行きません。UrlEncode メソッドは、半角空白を + に変換するので、例えば "file name.txt"(file と name の間に半角空白)" は "file+name.txt" になってしまいます。なので、半角空白は URL エンコードせず、そのままにしておく必要があります。

半角空白を + ではなく %20 に変換(パーセントエンコーディング)しても問題があります。IE 側で開く「ファイルのダウンロード」ダイアログで[開く(O)]をクリックするとメモ帳が開いて中身が表示されますが(もちろん拡張子 txt がメモ帳に関連付けられているとして)、メモ帳のウィンドウの左上に表示されるファイル名の空白は %20 のままになります。(上記の例では "file%20name.txt" となる) まぁ、これは大した問題ではないのかもしれませんが、気分がよくないです。(笑)

2 番目の方法(Shift_JIS のヘッダを送信)は IE ではうまく行きますが、当然、IE 以外のブラウザは対応できません。

また、ファイル名を Shift_JIS にすると、IE 自体は対応できても、それ以前にプロキシサーバーで文字化けして、結局、文字化けは回避できないという可能性があります。それに、単にファイル名が文字化けするだけでなく、他に予期しない好ましからざる副作用が出る恐れもあります。従って、2 番目の方法は、使用するブラウザが IE に限定される場合でも避けた方が賢明だと思います。

3 番目の方法は RFC 6266 をサポートしていないブラウザ(IE8-、Safari 5.1.7 など)には使えないという問題があります。また、これも、UrlEncode メソッドで、半角空白が + に変換されることにより、"file name.txt"(file と name の間に半角空白)" が "file+name.txt" になってしまうという問題があります。半角空白はパーセントエンコーディング("+" ではなくて "%20" に変換)しないとうまく行きません。

結局、現状ではファイル名には ASCII 文字以外は使わないというのが一番の解決策だと思います。

どうしても日本語のファイル名を使って、かつ IE, Firefox, Chrome 等の複数のブラウザに対応するなら、以下のようにブラウザによって応答ヘッダを切り替える他方法はなさそうです。

(注)別の記事 ダウンロードは HTTP ハンドラで で書きましたように、ファイルなどをダウンロードするのに .aspx ページを利用するといろいろ問題がありそうなので、以下のサンプルコードでは HTTP ハンドラを使用しています。

<%@ WebHandler Language="C#" Class="DownloadHandler" %>

using System;
using System.Web;

public class DownloadHandler : IHttpHandler 
{    
  public void ProcessRequest (HttpContext context) 
  {
    HttpResponse response = context.Response;
    HttpRequest request = context.Request;

    string fileName = "日 本 語 (+japanese+).txt";

    // + は %2b に変換され、空白は + に変換される。
    string encodedFileName = context.Server.UrlEncode(fileName); 
    encodedFileName = encodedFileName.Replace("+", "%20");
        
    response.Clear();

    response.ContentType = "text/plain";
        
    // キャッシュを許可しない
    response.Cache.SetCacheability(HttpCacheability.NoCache);
    response.Cache.SetExpires(DateTime.Now.ToUniversalTime());
    response.Cache.SetMaxAge(new TimeSpan(0, 0, 0, 0));

    HttpBrowserCapabilities browser = request.Browser;

    // IE の場合。
    // IE11 では Browser プロパティは "Mozilla" (.NET 2.0) 
    // または "InternetExplorer" (.NET 4) になる。IE の場合
    // User Agent には必ず "Trident" と言う文字列が入ってい
    // るらしいので、そちらで判定した方がよさそう。
    if (browser.Browser.ToUpper().IndexOf("IE") >= 0 ||
        request.UserAgent.Contains("Trident"))            
    {
      // IE8 以下(RFC 6266 未サポート)
      // 空白を %20 のままにしておくと処理がうまくいかない。
      // 「ファイルのダウンロード」ダイアログで[開く(O)]を
      // クリックするとメモ帳が開いて中身が表示されるが %20 
      // がそのままウィンドウの左上に表示されるファイル名に
      // 含まれる。それが気になる場合は、以下のように %20 を
      // 空白に置き換える。(注:「ファイルのダウンロード」
      // ダイアログで[保存(S)]をクリックすれば何故か %20 
      // は空白に変換されて保存される)
      if (browser.MajorVersion < 9)
      {
        response.AppendHeader("Content-Disposition", 
            "attachment;filename=\"" + 
            encodedFileName.Replace("%20", " ") +  "\"");
      }
      // IE9 以上(RFC 6266 サポート)
      else
      {
        response.AppendHeader("Content-Disposition", 
            "attachment;filename*=utf-8''" + encodedFileName);
      }
    }
    // IE 以外
    else
    {
      // Safari 5.1.7 がまだ RFC 6266 未対応らしいので 
      // filename="xxxxx" の併記が必要。 
      response.AppendHeader("Content-Disposition", 
          "attachment;filename=\"" + fileName + 
          "\";filename*=utf-8''" + encodedFileName);
    }

    response.Write("こんにちは世界!");
  }
 
  public bool IsReusable 
  {
    get 
    {
      return false;
    }
  }
}

なお、IIS7 で試した限りですが(他のサーバーは不明)、以下のコードのように HyperLink で直リンクすれば、どのブラウザでも文字化けはありませんでした。

<asp:HyperLink 
    ID="HyperLink2" 
    runat="server" 
    NavigateUrl="~/日 本 語 (japanese).zip">
    日 本 語 (japanese).zip
</asp:HyperLink>

その理由は、ブラウザから GET 要求する url のファイル名はパーセントエンコーディング(半角空白は "+" ではなく "%20")され、応答ヘッダには以下のように正しく Content-Type が指定されると共に Content-Disposition フィールドが含まれないからです。

HTTP/1.1 200 OK
Content-Type: application/x-zip-compressed
Last-Modified: Fri, 21 Feb 2014 14:39:36 GMT
Accept-Ranges: bytes
ETag: "752e46be122fcf1:0"
Server: Microsoft-IIS/7.0
X-Powered-By: ASP.NET
Date: Sun, 02 Mar 2014 07:42:01 GMT
Content-Length: 1956

なお、直リンクする場合、NavigateUrl に指定するファイル名に "+" を入れると、IIS7 では HTTP エラー 404.11(ダブルエスケープシーケンスを含む要求の拒否)になるので注意してください。

Tags:

Upload Download

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  2018年12月  >>
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

View posts in large calendar