WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

ASP.NET Core MVC でファイルダウンロード

by WebSurfer 20. January 2020 12:09

ASP.NET Core 3.1 MVC アプリでファイルをダウンロードする方法を書きます。下の画像のようにファイル名とファイルの種類をブラウザにきちんと認識させるのが条件です。(画像は IE11 の例です。通知バーが正しく出ていることに注目してください)

IE11 の通知バー

.NET Framework MVC5 アプリでファイルをダウンロードする方法は、先の記事「MVC でファイルのダウンロード」に書きましたのでそちらを見てください。

ASP.NET Core 3.1 MVC の場合も基本的なことは .NET Framework MVC5 とほぼ同じで、FileResult Class を継承する VirtualFileResult, FileContentResult, FileStreamResult, PhysicalFileResult オブジェクトのいずれかを Controller のアクションメソッドで生成して返してやることになります。

VirtualFileResult, FileContentResult, FileStreamResult オブジェクトを生成するには File メソッドが使えます。File メソッドの第 1 引数の型に応じて生成されるオブジェクトが異なり、以下のようになります。

  1. string 型: VirtualFileResult - コンテンツが既存のファイルとして提供される場合
  2. byte[] 型: FileContentResult - コンテンツがストリームとして提供される場合
  3. stream 型: FileStreamResult - コンテンツがバイト配列として提供される場合

上の 1 番目の「コンテンツが既存のファイルとして提供される場合」は File メソッドの第 1 引数にファイルの仮想バスを設定します(注:物理パスを指定する .NET Framework MVC5 の File メソッドと異なります)。ファイルはアプリケーションルート直下の wwwroot フォルダ下に置きます(そこ以外では見つからないというサーバーエラーになります)。例えば wwwroot/Files/test.pdf をダウンロードする場合は File メソッドの第 1 引数は "/Files/test.pdf" とします。

物理パスを指定したい場合は PhysicalFileResult クラスを使います。File メソッドを使わず明示的に PhysicalFileResult クラスを初期化して返してやる必要があるようです。

File メソッドの第 2 引数にはファイルの MIME タイプ、第 3 引数には拡張子を含むファイル名を設定します。そうすることにより応答ヘッダに Content-Type と Content-Disposition が適切に設定されます。ブラウザによって Content-Type, Content-Disposition のどちらでファイル名とファイルの種類を判断するかが異なりますので両方をきちんと設定するのは必須です。

.NET Framework MVC5 の File メソッドの場合、第 3 引数に設定するファイル名には US-ASCII 文字を使用しないと IE では文字化けしましたが、Core の File メソッドは RFC 6266 (RFC 2231/RFC 5987) に準拠した Content-Disposition ヘッダを生成するので文字化けの問題は回避できるようになりました。例えば第 3 引数に "日本語.pdf" という文字列を設定すると以下の画像にあるような Content-Disposition ヘッダが生成されます。

応答ヘッダ

キャッシュコントロールは ResponseCacheAttribute で設定します。例えば [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] とすると、上の応答ヘッダの画像のとおり Cache-Control: no-store, no-cache / Pragma: no-cache となります。(注: .NET Framework MVC5 の Response.Cache は使えません)

以下に、順に PhysicalFileResult, VirtualFileResult, FileContentResult, FileStreamResult クラスを使った場合のサンプルコードを書いておきます。

PhysicalFileResult

任意のフォルダにある既存のファイルを物理パスを指定してダウンロードする場合です。File メソッドは使えないようなので PhysicalFileResult クラスを直接初期化して return します。Content-Disposition ヘッダは自力でコードを書いて設定します(RFC 6266 に準拠したい場合はそれも自力でコーディング要)。

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Hosting;
using MvcCoreApp.Models;
using Microsoft.AspNetCore.Http;
using System.IO;

namespace MvcCoreApp.Controllers
{
  public class DownloadController : Controller
  {
    // Core では Server.MapPath が使えないことの対応
    private readonly IWebHostEnvironment _hostingEnvironment;

    public UploadController(
                      IWebHostEnvironment hostingEnvironment)
    {
      _hostingEnvironment = hostingEnvironment;
    }

    [HttpGet("/downloadfile")]
    [ResponseCache(Duration = 0, 
                   Location = ResponseCacheLocation.None, 
                   NoStore = true)]
    public IActionResult DownloadFile()
    {            
      // Content-Disposition ヘッダを設定
      // (RFC 6266 対応してないので注意)
      Response.Headers.Append("Content-Disposition", 
                            "attachment;filename=test.pdf");

      // アプリケーションルートの物理パスを取得
      // wwwroot の物理パスは WebRootPath プロパティを使う
      string contentRootPath = 
                       _hostingEnvironment.ContentRootPath;

      // ダウンロードするファイルの物理パス。アプリケーション
      // ルート直下の Files フォルダの test.pdf とする
      string physicalPath = contentRootPath + "\\" + 
                            "Files\\" + "test.pdf";
      return new PhysicalFileResult(physicalPath, 
                                    "application/pdf");
    }
  }
}

VirtualFileResult

wwwroot フォルダ下にある既存のファイルを仮想パスを指定してダウンロードする場合です。File メソッドが利用できます。

[HttpGet("/downloadfile")]
[ResponseCache(Duration = 0, 
               Location = ResponseCacheLocation.None, 
               NoStore = true)]
public IActionResult DownloadFile()
{
  // wwwroot/Files フォルダ内の test.pdf を指定
  string virtualPath = "/Files/test.pdf";

  // RFC 6266 に準拠した Content-Disposition ヘッダを生成する
  // ので第 3 引数のファイル名には日本語が使えます
  return File(virtualPath, "application/pdf", "日本語.pdf");
}

FileContentResult

ダウンロードするコンテンツがバイト配列として提供される場合です。コンテンツを SQL Server からバイト配列として取得するような時に利用できそうです。

[HttpGet("/downloadfile")]
[ResponseCache(Duration = 0, 
               Location = ResponseCacheLocation.None, 
               NoStore = true)]
public IActionResult DownloadFile()
{
  // 以下はバイト配列を取得するための単なるサンプル
  string contentRootPath = 
                    _hostingEnvironment.ContentRootPath;
  string physicalPath = contentRootPath + "\\" + 
                        "Files\\" + "test.pdf";
  byte[] data = System.IO.File.ReadAllBytes(physicalPath);

  return File(data, "application/pdf", "日本語.pdf");
}

FileStreamResult

ダウンロードするコンテンツがストリームとして提供される場合です。コンテンツを Web API から HttpClient を利用して取得するような時に利用できるでしょうか。

[HttpGet("/downloadfile")]
[ResponseCache(Duration = 0, 
               Location = ResponseCacheLocation.None, 
               NoStore = true)]
public IActionResult DownloadFile()
{
  // 以下は stream を取得するための単なるサンプル
  string contentRootPath = 
                    _hostingEnvironment.ContentRootPath;
  string physicalPath = contentRootPath + "\\" + 
                        "Files\\" + "test.pdf";
  FileStream stream = 
            new FileStream(physicalPath, FileMode.Open);

  return File(stream, "application/pdf", "日本語.pdf");
}

Tags: , ,

CORE

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(...); の引数によります。詳しくは Microsoft のドキュメント Controller.File Method を見てください。

上のコード例では第一引数が文字列(ファイルの物理パス)なので FilePathResult が呼び出されます。

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

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

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

ファイル名とファイルの種類をブラウザに認識させ、上の画像のような通知を表示させるには、上のサンプルコードで return File(...); の第 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

About this blog

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

Calendar

<<  April 2020  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910

View posts in large calendar