by WebSurfer
19. January 2020 12:33
ASP.NET Core 3.1 MVC アプリ(注:.NET Framework の ASP.NET MVC ではありません)でファイルをアップロードする方法について書きます。
.NET Framework ベースの(Core ではない)MVC5 アプリでファイルをアップロードする方法は、先の記事「MVC でファイルのアップロード」に書きましたのでそちらを見てください。
上のリンクの MVC5 アプリの記事と同様に、普通に form を submit して POST 送信する場合と、jQuery Ajax を利用して非同期で送信する場合の両方の例を紹介します。ちなみに、上の画像は jQuery Ajax を使ってアップロードした結果です。
本題に入る前に、Microsoft の記事「ASP.NET Core でファイルをアップロードする」の「セキュリティの考慮事項」のセクションを一読することをお勧めします。
この記事では上に紹介した Microsoft の記事に書かれたセキュリティ関する配慮がされていませんので注意してください。例えば「アプリと同じディレクトリツリーに、アップロードしたファイルを保持しないでください」とありますが、この記事ではアプリケーションルート直下の UploadedFiles というフォルダに、アップロードされたファイルをチェックせず、ユーザーによって指定されたファイル名でそのまま保存するコードになっています。
この記事ではセキュリティの話はちょっと置いといて、単純にファイルをアップロードするにはどうするかということを書きます。気をつけるべき点は以下の通りです。
-
View では form 要素の enctype 属性に "multipart/form-data" が設定されるようにする。
-
Controller のアクションメソッドでは、アップロードされたファイルがバインドされるパラメータまたはクラスのプロパティは IFormFile 型であること。(MVC5 の HttpPostedFileBase 型ではなくて)
-
上で述べたバインドされるパラメータまたはクラスのプロパティの名前は、html ソースの <input type="file" ... /> の name 属性と一致させる。
-
Internet Explorer (IE) でファイルをアップロードすると、クライアント PC でのフルパスがファイル名として送信されることがある(先の記事「IE でアップロードする際のファイル名」を参照)。その場合、IFormFile.FileName でファイル名を取得するとクライアント PC でのフルパスになるので、必ず Path.GetFileName を使うこと。
-
ワーカープロセスがアップロードするホルダに対する「書き込み」権限を持っていること。(IIS でホストする場合です。Kestrel の場合も権限が必要なのは同じだと思いますが詳しいことは未調査で分かりません)
-
IIS でも Kestrel も最大要求本文サイズに 30,000,000 バイトの制限がある。詳しくは上に紹介した Microsoft の記事の「IIS の内容の長さの制限」または「Kestrel の最大要求本文サイズ」のセクションを見てください。変更方法も書いてあります。
jQuery Ajax を使ってファイルをアップロードする場合は、上記に加えて以下の点に注意してください。
-
XMLHttpRequest を使用して送信するためのキーと値のペアのセットを取得するために FormData オブジェクトを利用する。詳しくは MDN の記事「FormData オブジェクトの利用」にありますのでそちらを参照してください。
-
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();
}
}
}
}