WebSurfer's Home

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

MVC でチャンク形式でダウンロード

by WebSurfer 2018年12月20日 11:27

MVC のアクションメソッドを使ってファイルをチャンク形式でエンコーディングしてブラウザにダウンロードする方法を書きます。

チャンク形式でダウンロード

チャンク形式エンコーディングせず、普通に(Content-Length を設定して)ダウンロードする方法については先の記事「MVC でファイルのダウンロード」に書きましたのでそちらを見てください。

チャンク形式エンコーディングでダウンロードするには、その記事で紹介したようなヘルパーメソッド File は使えませんので別の手段を考えることになると思います。

結局は、(1) HttpResponse オブジェクトを取得、(2) それから OutputStream プロパティを使って出力ストリームを取得、(3) コンテンツをチャンクに分割して Write メソッドでストリームに書き込む、(4) Flush メソッドでクライアントに送信する、(5) 全チャンクを送信するまで (3) と (4) の操作を繰り返す・・・ということになると思います。

MVC のアクションメソッドでは Controller.Response プロパティで HttpResponse オブジェクトを取得できますので、それを使って上記 (1) ~ (5) の操作を行うことができます。

そのコードを以下に書いておきます。結局は Web Forms アプリの記事「チャンク形式でダウンロード」のコードとほぼ同じですが・・・

public void ChunkedDownload()
{
  string folder = "~/Files/";
  string filename = "Sig552T8.jpg";
  string path = Server.MapPath(folder + filename);
  FileInfo fileInfo = new FileInfo(path);

  if (fileInfo.Exists)
  {
    int chunkSize = 10000;
    Byte[] buffer = new Byte[chunkSize];
    Response.Clear();

    using (FileStream stream = System.IO.File.OpenRead(path))
    {
      long length = stream.Length;
      Response.ContentType = "image/jpeg";
      Response.AddHeader("Content-Disposition",
              "attachment; filename=" + fileInfo.Name);

      while (length > 0 && Response.IsClientConnected)
      {
        int lengthRead = stream.Read(buffer, 0, chunkSize);
        Response.OutputStream.Write(buffer, 0, lengthRead);

        Response.Flush();
        length -= lengthRead;
      }
    }
  }            
}

アクションメソッドの戻り値は ActionResult である必要はなく void にできることに注意してください。

また、上のコードで最後を示す長さ 0 のチャンクも送信されます。(Fiddler で最後のバイト列が ... 0D 0A 30 0D 0A 0D 0A となっているのを確認しました)

もう一つ、MVC アプリでも HTTP ジェネリックハンドラ(.ashx ファイル)は使えますので、アクションメソッドを使わなくても、HTTP ジェネリックハンドラに同様な機能を実装することは可能です。

Tags:

Upload Download

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

チャンク形式でダウンロード

by WebSurfer 2016年12月14日 23:49

ASP.NET Web Forms アプリにおいて、ファイルをチャンク形式エンコーディングしてブラウザにダウンロードするサンプルを備忘録として書いておきます。

Fiddler で見た応答

チャンク形式エンコーディングとは HTTP/1.1 で定義されている方式で、送信したいデータを任意のサイズのチャンク(塊)に分割し、各々のチャンクにサイズ情報を付与するエンコード方式です。

メリットは、送信するデータを動的に作成していて、作成中は全体のサイズが分からないが、部分的にでも作成でき次第送信を始められるというところにあるようです。(一旦全データをバッファして全体のサイズを調べ、Content-Length に設定するということをしなくても済みます)

サーバーにある既存のファイルをダウンロードするような場合はファイルのサイズは分かっていますので、チャンク形式エンコーディングせずに Content-Length を設定してダウンロードする方がよさそうです。(なので、自分的にはチャンク形式エンコーディングの使い道はあまりなさそうですが、せっかく調べたので書いておきます)

ASP.NET Web Forms アプリでは、HttpResponse.OutputStream にチャンクを書き込んだら Flush することでそのチャンクがクライアント(ブラウザ)に送信されます。

具体的には以下のコードのようにします。Sig552T8.jpg は 30,903 バイトの jpeg 画像ファイルで、以下の .aspx ページをブラウザから要求すると、その画像データを 10,000 バイトずつチャンクに分けて送信するようになっています。

この記事の一番上の画像を見てください。Fiddler を使ってサーバーからの応答をキャプチャしたものですが、応答ヘッダの赤線で示した部分にチャンク形式エンコーディングになっていること、コンテンツの最初の部分 32 37 31 30(UTF-8 で 2710 ⇒ 10 進数に直すと 10000)に最初に送信されたチャンクのサイズが示されていることが分かります。

<%@ Page Language="C#" %>
<%@ Import Namespace="System.IO" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
  protected void Page_Load(object sender, EventArgs e)
  {
    string folder = "~/images/";
    string filename = "Sig552T8.jpg";
    string path = Server.MapPath(folder + filename);
    FileInfo fileInfo = new FileInfo(path);

    if (fileInfo.Exists)
    {
      int chunkSize = 10000;
      Byte[] buffer = new Byte[chunkSize];
      Response.Clear();   

      using (FileStream stream = File.OpenRead(path))
      {
        long length = stream.Length;
        Response.ContentType = "image/jpeg";
        Response.AddHeader("Content-Disposition", 
                "attachment; filename=" + fileInfo.Name);

        // ここで Flush しても通知バーは表示されない。以下のコ
        // ードのコメントアウトを外すとそれが確認できます。
        // Response.Flush();
        // System.Threading.Thread.Sleep(10000);

        while (length > 0 && Response.IsClientConnected)
        {
          int lengthRead = stream.Read(buffer, 0, chunkSize);
          Response.OutputStream.Write(buffer, 0, lengthRead);

          // ここでの最初の Flush で通知バーが表示される
          Response.Flush();
          length -= lengthRead;

          // chunked ダウンロードされていることを確認するため
          // 試験的に入れたコード。コメントアウトを外すとここ
          // で 5 秒待つ。
          // System.Threading.Thread.Sleep(5000);
        }
      }

      // <!DOCTYPE html ... 以下の html ソースをダウンロード
      // させないために設定
      Response.SuppressContent = true;
    }        
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    </form>
</body>
</html>

ブラウザ上ではどういう動きになるかと言うと、IE9 を使った場合ですが、上記コードの中の while ループの最初の Flush で以下の画像のように通知バーが表示されます。

IE9 の通知バー

上の通知バーの[ファイルを開く(O)]または[保存(S)]をクリックすると通知バーは以下の画像のように変わります。(注:Thread.Sleep(5000) のコメントアウトを外して試してください)

ダウンロード中の通知バー

その後、最初に表示された通知バーで[ファイルを開く(O)]をクリックしていた場合はダウンロードが完了すると画像 Sig552T8.jpg がブラウザ上に表示されます。

最初に表示された通知バーで[保存(S)]をクリックしていた場合はダウンロードが完了すると通知バーは以下の画像のように変わります。

ダウンロード完了の通知バー

[ファイルを開く(O)]または[フォルダーを開く(P)]でダウンロードした画像ファイルを確認できます。

Tags:

Upload Download

About this blog

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

Calendar

<<  2019年1月  >>
303112345
6789101112
13141516171819
20212223242526
272829303112
3456789

View posts in large calendar