WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

Ajax でファイルダウンロード

by WebSurfer 5. February 2019 18:38

Ajax を使ってファイルをダウンロードするにはどうすれば良いかということを調べましたので備忘録として書いておきます。

Ajax でファイルダウンロード

例えば、先の記事「MVC でファイルのダウンロード」で書いたようなアクションメソッドがあるとして、Ajax を使ってその URL に非同期要求をかけたとします。

そうすると、同期要求した場合と全く同様に、応答ヘッダには Content-Type と Content-Disposition が適切に設定され、コンテンツ(ファイルのバイナリデータ)も正しく返ってきます。

しかしながら、上の画像のような通知バーは表示されませんし、ファイルは PC のディスクには保存されません。

では、ファイルを PC のディスクに保存するにはどうすればいいかと言うと、応答コンテンツを Blob(生データ)として取得し、それを保存するためのスクリプトを書くことになります。

jQuery ajax を使う場合、バージョン 3.0 以降であれば Blob を取得できるそうですが(未検証・未確認です)、バージョン 2.x 以前では Blob を取得できないので、ネイティブの XMLHttpRequest を使うことになるそうです。

詳しくは stackoverflow の記事 Using jQuery's ajax method to retrieve images as a blob にある回答を読んでください。

取得した Blob をファイルとして PC に保存するには、IE, Edge の場合は msSaveBlob method を使います。

Chrome, Firefox, Opera の場合は URL.createObjectURL を使って Blob の URL を取得し、それを html の a 要素の href 属性に設定し、download 属性にファイル名を設定してスクリプトでクリックするようにします。

具体的には、ネイティブの XMLHttpRequest を使った例ですが、以下のコードの通りです。

function download() {
  var url = "/Home/FileDownload";
  var filename = "testfile";

  var xhr = new XMLHttpRequest();
  xhr.open('GET', url, true);
  xhr.responseType = 'blob';
  xhr.onreadystatechange = function (e) {
    if (this.readyState == 4 && this.status == 200) {
      var blob = this.response;

      //IE, Edge とその他で処理の切り分け
      if (window.navigator.msSaveBlob) {
        window.navigator.msSaveBlob(blob, filename + ".pdf");
      } else {
        var a = document.createElement("a");
        // IE11 は URL API をサポートしてない
        var url = window.URL;
        a.href = url.createObjectURL(new Blob([blob],
                                     { type: blob.type }));
        document.body.appendChild(a);
        a.style = "display: none";
        a.download = filename + ".pdf";
        a.click();
      }
    }
  };
  xhr.send();
}

2, 3 注意点を書いておきます。

IE11 は window.URL をサポートしてないのでファイルとして保存するには msSaveBlob メソッドを使う以外に手はなさそうです。

msSaveBlob の第 2 引数を設定しないと、ファイル名は IE の場合 1A31A31A-1F57-4D8D-8C70-150839D02536.pdf のように、Edge の場合は (1) となってしまいます。

MDN のドキュメントには、Content-Disposition ヘッダーで download 属性の指定と異なるファイル名が与えられた場合は、この属性より HTTP ヘッダーが優先するということが書いてありますが、実際試すと download 属性の指定通りとなります。"" を設定したりすると aae9adeb-1005-407f-a3a6-046fa79c4351.pdf のようになります。

Tags: ,

Upload Download

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

by WebSurfer 12. December 2018 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 メソッドでバイト列を取得し、それを HttpResponse.OutputStream.Write メソッドを使って出力ストリームに書き出すという操作を行います。

FileContentResult は、第 1 引数に指定されるバイト列を直接 HttpResponse.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 16. February 2013 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

About this blog

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

Calendar

<<  April 2019  >>
MoTuWeThFrSaSu
25262728293031
1234567
891011121314
15161718192021
22232425262728
293012345

View posts in large calendar