WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

Razor Pages アプリでファイルアップロード

by WebSurfer 6. May 2024 13:10

ASP.NET Core Razor Pages アプリでファイルをアップロードする方法について書きます。先の記事「ASP.NET Core MVC でファイルアップロード」の Razor Pages 版です。MVC ⇒ Razor Pages にしたこと以外で先の記事と違うのは、ターゲットプラットフォームを .NET 8.0 にしたことと、jQuery ajax に代えて fetch API を使ったことです。

Razor Pages でファイルアップロード

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

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

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

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

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

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

  1. fetch API を使用して送信するフォームデータを取得するために FormData オブジェクトを利用する。詳しくは MDN の記事「FormData オブジェクトの利用」にありますのでそちらを参照してください。FormData オブジェクトには CSRF 防止のための隠しフィールドのトークンも含まれます。同時にクッキーの CSRF 防止のためのトークンも送信されるので、サーバー側で検証が可能になります。
  2. fetch API を使う場合、デフォルトでは要求ヘッダに X-Requested-With: XMLHttpRequest は含まれない。サーバー側での判定のためなどにそのヘッダが必要な場合、クライアントスクリプトにそのヘッダを追加するコードを書く必要があります。

サンプルコードを以下に載せておきます。この記事の一番上の画像は下のサンプルコードの実行結果です。

Model

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

cshtml

@page
@model RazorPages1.Pages.File.UploadModel

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

<style type="text/css">
    /*Bootstrap5 には form-group は無いので 4 と同じものを追加*/
    .form-group {
        margin-bottom: 1rem;
    }
</style>

<h1>Upload</h1>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post" enctype="multipart/form-data">
            <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="Submit Form"
                           class="btn btn-primary" />
                    <div>@Model.Message</div>
                </div>
            </div>
        </form>

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

@section Scripts {
    <script type="text/javascript">
        //<![CDATA[

        document.getElementById("ajaxUpload")
            .addEventListener('click', async () => {
                const fd = new FormData(document.querySelector("form"));
                const result = document.getElementById("result");

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

                const param = {
                    method: "POST",
                    body: fd,
                    // jQuery ajax と違って X-Requested-With ヘッダは
                    // 自動的には送られないので以下の設定で対応
                    headers: { 'X-Requested-With': 'XMLHttpRequest' }
                }

                const response = await fetch("/file/upload", param);
                if (response.ok) {
                    const data = await response.text();
                    result.innerText = data;
                } else {
                    result.innerText = "response.ok が false";
                }
            });

        //]]>
    </script>
}

cshtml.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPages1.Models;

namespace RazorPages1.Pages.File
{
    public class UploadModel : PageModel
    {
        // Core では Server.MapPath が使えないことの対応
        private readonly IWebHostEnvironment _hostingEnvironment;

        public UploadModel(IWebHostEnvironment hostingEnvironment)
        {
            this._hostingEnvironment = hostingEnvironment;
        }
        
        public void OnGet()
        {
        }

        public string? Message { get; set; }

        [BindProperty]
        public UploadModels Model { get; set; } = default!;

        public async Task<IActionResult> OnPostAsync()
        {
            string result;
            IFormFile? postedFile = Model.PostedFile;
            if (postedFile != null && postedFile.Length > 0)
            {
                // アップロードされたファイル名を取得。ブラウザによっ
                // ては postedFile.FileName はクライアント側でのフル
                // パスになることがあるので Path.GetFileName を使う
                string filename = Path.GetFileName(postedFile.FileName);

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

                // アプリケーションルートの UploadedFiles フォルダに
                // ファイルを保存する
                string filePath = $"{rootPath}\\UploadedFiles\\{filename}";
                using (var fs = new FileStream(filePath, FileMode.Create))
                {
                    await postedFile.CopyToAsync(fs);
                }

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

            // クライアントスクリプトによるアップロードか否かを判定。
            // fetch API を使う場合は X-Requested-With: XMLHttpRequest
            // ヘッダを送るようクラ��アントスクリプト側でコーディング
            // が必要
            if (Request.Headers.XRequestedWith == "XMLHttpRequest")
            {
                return Content(result);
            }
            else
            {
                Message = result;
                return Page();
            }
        }
    }
}

Tags: , , ,

Upload Download

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: , , , ,

Upload Download

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  June 2024  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

View posts in large calendar