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

ASP.NET Core MVC でファイルアップロード

by WebSurfer 19. January 2020 12:33

ASP.NET Core 3.1 MVC アプリ(注:.NET Framework の ASP.NET MVC ではありません)でファイルをアップロードする方法について書きます。

ASP.NET Core MVC でファイルのアップロード

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

上のリンクの MVC5 アプリの記事と同様に、普通に form を submit して POST 送信する場合と、jQuery Ajax を利用して非同期で送信する場合の両方の例を紹介します。ちなみに、上の画像は jQuery Ajax を使ってアップロードした結果です。

本題に入る前に、Microsoft の記事「ASP.NET Core でファイルをアップロードする」の「セキュリティの考慮事項」のセクションを一読することをお勧めします。

この記事では上に紹介した Microsoft の記事に書かれたセキュリティ関する配慮がされていませんので注意してください。例えば「アプリと同じディレクトリツリーに、アップロードしたファイルを保持しないでください」とありますが、この記事ではアプリケーションルート直下の UploadedFiles というフォルダに、アップロードされたファイルをチェックせず、ユーザーによって指定されたファイル名でそのまま保存するコードになっています。

この記事ではセキュリティの話はちょっと置いといて、単純にファイルをアップロードするにはどうするかということを書きます。気をつけるべき点は以下の通りです。

  1. View では form 要素の enctype 属性に "multipart/form-data" が設定されるようにする。
  2. Controller のアクションメソッドでは、アップロードされたファイルがバインドされるパラメータまたはクラスのプロパティは IFormFile 型であること。(MVC5 の HttpPostedFileBase 型ではなくて)  
  3. 上で述べたバインドされるパラメータまたはクラスのプロパティの名前は、html ソースの <input type="file" ... /> の name 属性と一致させる。
  4. Internet Explorer (IE) でファイルをアップロードすると、クライアント PC でのフルパスがファイル名として送信されることがある(先の記事「IE でアップロードする際のファイル名」を参照)。その場合、IFormFile.FileName でファイル名を取得するとクライアント PC でのフルパスになるので、必ず Path.GetFileName を使うこと。  
  5. ワーカープロセスがアップロードするホルダに対する「書き込み」権限を持っていること。(IIS でホストする場合です。Kestrel の場合も権限が必要なのは同じだと思いますが詳しいことは未調査で分かりません)
  6. IIS でも Kestrel も最大要求本文サイズに 30,000,000 バイトの制限がある。詳しくは上に紹介した Microsoft の記事の「IIS の内容の長さの制限」または「Kestrel の最大要求本文サイズ」のセクションを見てください。変更方法も書いてあります。

jQuery Ajax を使ってファイルをアップロードする場合は、上記に加えて以下の点に注意してください。

  1. XMLHttpRequest を使用して送信するためのキーと値のペアのセットを取得するために FormData オブジェクトを利用する。詳しくは MDN の記事「FormData オブジェクトの利用」にありますのでそちらを参照してください。
  2. ASP.NET Core MVC 組み込みの CSRF 防止機能は Ajax でもそのまま使えるので、Controller のアクションメソッドへの [ValidateAntiForgeryToken] を忘れずに設定する。(ASP.NET Core 2.0 以降では FormTagHelperが HTML フォームの要素に偽造防止トークンを挿入するので、View で明示的に @Html.AntiForgeryToken() を書く必要はないそうです。詳しくは Microsoft の記事「ASP.NET Core でのクロスサイト要求偽造 (XSRF/CSRF) 攻撃を防ぐ」を見てください。

上の画像を表示するのに使ったコードを以下に書いておきます。

Model

using Microsoft.AspNetCore.Http;

namespace MvcCoreApp.Models
{
    public class UploadModels
    {
        public string CustomField { get; set; }
        public IFormFile PostedFile { get; set; }
    }
}

View

@model MvcCoreApp.Models.UploadModels

@{
    ViewData["Title"] = "Upload";
}

<h1>Upload</h1>

<hr />
<div class="row">
  <div class="col-md-4">
    <form method="post" enctype="multipart/form-data" 
        asp-controller="Upload" asp-action="Index">
      <div class="form-group">
        <div class="col-md-10">
          <p>Upload file using this form:</p>
          @* name 属性はモデルのクラスのプロパティ名と同じ
             にしないとサーバー側でモデルバインディングさ
             れないので注意。大文字小文字は区別しない。*@
          <input type="file" name="postedfile" />
        </div>
      </div>
      <div class="form-group">
        <div class="col-md-10">
          <input type="submit" value="Upload by Submit" 
                 class="btn btn-primary" />
          <div>@ViewBag.Result</div>
        </div>
      </div>
    </form>

    <div class="form-group">
      <div class="col-md-10">
        <input type="button" id="ajaxUpload" 
            value="Ajax Upload" class="btn btn-primary" />
        <div id="result"></div>
      </div>
    </div>
  </div>
</div>

@section Scripts {
<script type="text/javascript">
  //<![CDATA[
  $(function () {
    $('#ajaxUpload').on('click', function (e) {
      // FormData オブジェクトの利用
      var fd = new FormData(document.querySelector("form"));

      // 追加データを以下のようにして送信できる。フォーム
      // データの一番最後に追加されて送信される
      fd.append("CustomField", "This is some extra data");

      $.ajax({
        url: '/fileupload',
        method: 'post',
        data: fd,
        processData: false, // jQuery にデータを処理させない
        contentType: false  // contentType を設定させない
        }).done(function(response) {
          $("#result").empty;
          $("#result").text(response);
        }).fail(function( jqXHR, textStatus, errorThrown ) {
          $("#result").empty;
          $("#result").text('textStatus: ' + textStatus +
                          ', errorThrown: ' + errorThrown);
        });
    });
  });
  //]]>
</script>
}

Controler / Action Method

Core では Server.MapPath メソッドと Request.IsAjaxRequest メソッドが使えない点に注意してください。それに代わる手段は以下のコードに書いてあります。

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

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

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

    [HttpGet("/fileupload")]
    public IActionResult Index()
    {
        return View();
    }

    [HttpPost("/fileupload")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Index(UploadModels model)
    {
      string result = "";
      IFormFile postedFile = model.PostedFile;
      if (postedFile != null && postedFile.Length > 0)
      {
        // アップロードされたファイル名を取得。ブラウザが IE 
        // の場合 postedFile.FileName はクライアント側でのフ
        // ルパスになることがあるので Path.GetFileName を使う
        string filename = 
                      Path.GetFileName(postedFile.FileName);

        // アプリケーションルートの物理パスを取得。Core では
        // Server.MapPath は使えないので以下のようにする
        string contentRootPath = 
                        _hostingEnvironment.ContentRootPath;
        string filePath = contentRootPath + "\\" + 
                          "UploadedFiles\\" + filename;

        using (var stream = 
                    new FileStream(filePath, FileMode.Create))
        {
          await postedFile.CopyToAsync(stream);
        }

        result = filename + " (" + postedFile.ContentType + 
                 ") - " + postedFile.Length + 
                 " bytes アップロード完了";
      }
      else
      {
        result = "ファイルアップロードに失敗しました";
      }

      // Core では Request.IsAjaxRequest() は使えない
      if (Request.Headers["X-Requested-With"] == 
                                          "XMLHttpRequest")
      {
        return Content(result);
      }
      else
      {
        ViewBag.Result = result;
        return View();
      }
    }
  }
}

Tags: , , , ,

CORE

ASP.NET Core 3

by WebSurfer 15. December 2019 20:53

ASP.NET Core 3 の本

ASP.NET Core の勉強を始めようと思って左の画像の本を買って PC に Visual Studio Community 2019 をインストールしました。

自分が今まで使ってきた Visual Studio 2015 にはすでに ASP.NET Core の開発環境が提供されていたのですが、先の記事に書いた「ASP.NET Core で文字化け」という問題があったのです。

まだ枯れてないフレームワークには他にもいろいろ不具合がありそうな気がしましたし、勉強を始めたはいいが途中で開発中止になってしまう可能性もないわけではありませんでしたし、手を出すのは時期尚早と思ってました。(言い訳です(笑))

どうやら開発中止になるどころか Microsoft も Core への移行を推奨するそうですし、2019 年 11 月にバージョン 3.1 がリリースされて安定してきたそうなので、時代に取り残されてはいけないと思って本を買ってきた次第です。

実は、上に紹介した時事を書いた頃少しだけ勉強したのですが、サーバー周りが旧来の ASP.NET とは大幅に異なっていて、今の知識が役に立たなそうなのにビビってました。

Model, View, Controller のコードの基本的なところにはそんなに違いはないようですが、それでもやはり従来の MVC の知識が通用しないところがありそうです。

.NET Framework ベースの ASP.NET の知識も十分とは言えない自分が余計なものに手を出すべきではないのかもしれません。

でも、ソフト開発でメシを食っているわけではない自分にとっては所詮趣味の問題に過ぎないので、自己満足できればいいと思うことにします。(笑)

Tags:

CORE

About this blog

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

Calendar

<<  January 2020  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
272829303112
3456789

View posts in large calendar