.NET Framework 版の MVC5 アプリでファイルをアップロードする方法について書きます。(Core 3.1 版は別の記事「ASP.NET Core MVC でファイルアップロード」に書きましたのでそちらを見てください。)
普通に form を submit して POST 送信する場合と、jQuery Ajax を利用して非同期で送信する場合の両方の例を紹介します。ちなみに、上の画像は jQuery Ajax を使ってアップロードした結果です。
本題に入る前に、Microsoft の記事「ASP.NET Core でファイルをアップロードする」の「セキュリティの考慮事項」のセクションを一読することをお勧めします。
この記事では上に紹介した Microsoft の記事に書かれたセキュリティ関する配慮がされていませんので注意してください。例えば「アプリと同じディレクトリツリーに、アップロードしたファイルを保持しないでください」とありますが、この記事ではアプリケーションルート直下の UploadedFiles というフォルダに、アップロードされたファイルをチェックせず、ユーザーによって指定されたファイル名でそのまま保存するコードになっています。
この記事ではセキュリティの話はちょっと置いといて、単純に技術的にどうするかということを書きます。気をつけるべき点は以下の通りです。
-
View では form 要素の enctype 属性に "multipart/form-data" が設定されるようにする。
-
Controller のアクションメソッドでは、アップロードされたファイルがバインドされるパラメータまたはクラスのプロパティは HttpPostedFileBase 型であること。
-
上で述べたバインドされるパラメータまたはクラスのプロパティの名前は、html ソースの <input type="file" ... /> の name 属性と一致させる。
-
Internet Explorer (IE) でファイルをアップロードすると、クライアント PC でのフルパスがファイル名として送信されることがある(先の記事「IE でアップロードする際のファイル名」を参照)。その場合、HttpPostedFileBase.FieName でファイル名を取得するとクライアント PC でのフルパスになるので、必ず Path.GetFileName を使うこと。
-
ワーカープロセスがアップロードするホルダに対する「書き込み」権限を持っていること。
-
ASP.NET では、デフォルト設定では 4MB を超えるリクエストは送信できないので注意。4MB を超える場合は、web.config の <httpRuntime> セクションの maxLengthRequest の設定で調整できる。
jQuery Ajax を使ってファイルをアップロードする場合は、上記に加えて以下の点に要注目です。
-
XMLHttpRequest を使用して送信するためのキーと値のペアのセットを取得するために FormData オブジェクトを利用する。詳しくは MDN の記事「FormData オブジェクトの利用」にありますのでそちらを参照してください。
-
ASP.NET MVC 組み込みの CSRF 防止機能は Ajax でもそのまま使えますので、View での @Html.AntiForgeryToken() と Controller のアクションメソッドへの [ValidateAntiForgeryToken] を忘れずに設定する。
上の画像を表示するのに使ったコードを以下に書いておきます。
Model
public class UploadModels
{
public string CustomField { get; set; }
public HttpPostedFileBase PostedFile { get; set; }
}
View
@model Mvc5App.Controllers.UploadModels
@{
ViewBag.Title = "Upload";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Upload</h2>
@using (Html.BeginForm("Upload", "Home", FormMethod.Post,
new { enctype = "multipart/form-data" }))
{
// form 内の隠しフィールドは Ajax でも送信される。
// なので以下に設定したトークンは送信される。もちろん
// クッキーのトークンも送信されるので、アクションメソ
// ッドに [ValidateAntiForgeryToken] を付与すれば
// CSRF の検証はできる
@Html.AntiForgeryToken()
// name 属性はモデルのクラスのプロパティ名と同じにしない
// とサーバー側でモデルバインディングされないので注意。
// 大文字小文字は区別しない。
<input type="file" name="postedfile" />
<button type="submit">Upload by Submit</button>
<br />
@ViewBag.Result
}
<br />
<input type="button" id="ajaxUpload" value="Ajax Upload" />
<br />
<div id="result"></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: '/home/upload',
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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.ComponentModel.DataAnnotations;
using Mvc5App.Models;
using System.IO;
namespace Mvc5App.Controllers
{
public class HomeController : Controller
{
public ActionResult Upload()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Upload(UploadModels model)
{
string result = "";
HttpPostedFileBase postedFile = model.PostedFile;
if (postedFile != null &&
postedFile.ContentLength > 0)
{
// アップロードされたファイル名を取得。ブラウザが IE
// の場合 postedFile.FileName はクライアント側でのフ
// ルパスになることがあるので Path.GetFileName を使う
string filename =
Path.GetFileName(postedFile.FileName);
// 保存先のホルダの物理パス\ファイル名
string path = Server.MapPath("~/UploadedFiles") +
"\\" + filename;
// アップロードされたファイルを保存
postedFile.SaveAs(path);
result = filename +
" (" + postedFile.ContentType + ") - " +
postedFile.ContentLength.ToString() +
" bytes アップロード完了";
}
else
{
result = "ファイルアップロードに失敗しました";
}
if (Request.IsAjaxRequest())
{
return Content(result);
}
else
{
ViewBag.Result = result;
return View();
}
}
}
}
上の例は単一ファイルをアップロードする場合のものです。複数のファイルをアップロードする場合は、例えば html ソースで以下のように name 属性の値に連番で index を付与できれば、
<input type="file" name="postedfiles[0]" />
<input type="file" name="postedfiles[1]" />
・・・中略・・・
<input type="file" name="postedfiles[n]" />
または、ブラウザが multiple 属性をサポートしていれば以下のようにして、
<input type="file" name="postedfiles" multiple="multiple" />
Model のクラスを以下のようにすれば PostedFiles にモデルバインドできます。
public class UploadModels
{
public string CustomField { get; set; }
public IList<HttpPostedFileBase> PostedFiles { get; set; }
}