ブラウザを使って自分の PC の複数のフォルダから複数のファイルを一度にアップロードするにはどうしたらよいかという話を書きます。
html の input type="file" 要素を使うことが前提です。ブラウザがサポートしていれば input type="file" 要素に multiple="multiple" を追加すれば、ユーザーは複数のファイルを選択して一度にアップロードすることができます。
ファイルを選択する際、上の画像の[ファイル選択]ボタン(画像は Chrome の例です。ブラウザによって異なります)をクリックすると下の画像のダイアログが表示されますので、Shift キーや Ctrl キーを使って複数のファイルを選択できます。
その後[開く(O)]ボタンをクリックすると送信準備完了となり、form を method="post" で submit してやればサーバーに選択された複数のファイルが送信されます。
ただし、上の操作で送信するファイルを選択できるのは一回だけです。この後、もう一度同じ操作を行って別のファイルを追加することはできません。それをすると、前の操作で選択したファイルは送信対象には含まれなくなり、後の操作で選択したファイルのみが送信されるようになります。それは同じフォルダで繰り返し行う場合も異なるフォルダに移動して行う場合も同じです。
ということは、フォルダが異なるファイルを選択するには各フォルダに移動して選択操作を行わざるを得ませんが、送信できるのは最後の操作で選択したファイルのみになります。なので、複数のフォルダにある複数のファイルを一度にアップロードするのは普通のやり方(form を submit する方法)ではできないようです。
代案は HTML5 File API と FormData を利用し Ajax で送信することです。
以下に .NET Framework 版の ASP.NET MVC5 アプリの例を書いておきます。これは一番上の画像を表示したものです。詳しい説明はコードの中のコメントに書きましたのでそれを見てください。手抜きでスミマセン。
Model
MultipleUploadModels クラスをアクションメソッドの引数に設定すれば、送信されてきた複数ファイルは PostedFiles プロパティにモデルバインドされます。CustomField プロパティはファイル以外の追加情報を一緒に送信するためのものです。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Mvc5App.Models
{
public class MultipleUploadModels
{
public string CustomField { get; set; }
public IList<HttpPostedFileBase> PostedFiles { get; set; }
}
}
View
input type="file" 要素と input type="button" 要素(ボタン)を配置します。form 要素を生成するための html ヘルパー Html.BeginForm の引数(特に enctype = "multipart/form-data" とすること)に注意してください。
@section Scripts { ... } 内の JavaScript / jQuery のコードが HTML5 File API と FormData を利用し Ajax で複数フォルダ内の複数ファイルを送信するものです。このスクリプトがこの記事のキモです。
@model Mvc5App.Models.MultipleUploadModels
@{
ViewBag.Title = "MultipleUpload2";
}
<h2>MultipleUpload2</h2>
@using (Html.BeginForm("MultipleUpload2", "File",
FormMethod.Post, new { enctype = "multipart/form-data" }))
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>MultipleUploadModels</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(model => model.PostedFiles,
htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
<input id="mutiplefileupload" type="file"
name="postedfiles" multiple="multiple" />
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="button" id="ajaxUpload" value="Ajax Upload"
class="btn btn-default" />
<br />
<div id="result"></div>
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
<script type="text/javascript">
//<![CDATA[
$(function () {
// 複数回送信する場合、送信し終わったら fd をクリアして最初から
// 始めないと、次に送信するとき前回選んだファイルも送信されダブ
// ってしまう。このコードはそこは未対応なので注意。
// ブラウザの HTML5 File API サポートを確認
if (window.File && window.FileReader && window.FileList) {
// CSRF 用のトークンを含んだ FormData オブジェクトを取得
var fd = new FormData(document.querySelector("form"));
// input type="file" 要素のオブジェクトを取得
var fileUpload = document.getElementById("mutiplefileupload");
// input type="file" 要素でダイアログを開いてファイルを選択
// し[開く]ボタンをクリックすると、その都度 change イベン
// トが発生する。それにリスナ(下のコードの function )を
// アタッチして FormData オブジェクトを操作する
fileUpload.addEventListener('change', function (e) {
// files プロパティで FileList オブジェクトを取得
var filelist = fileUpload.files;
// 一回の操作で複数のファイルを選択できるので、以下
// のようにループを回して
for (let i = 0; i < filelist.length; i++) {
// File オブジェクトを FormData に追加していく
fd.append("postedfiles", filelist[i]);
}
});
// [Ajax Upload] ボタンクリックの処置
$('#ajaxUpload').on('click', function (e) {
// 追加データを以下のようにして送信できる。フォーム
// データの一番最後に追加されて送信される
fd.append("CustomField", "This is some extra data");
$.ajax({
url: '/file/multipleupload2',
method: 'post',
data: fd,
processData: false, // jQuery にデータを処理させない
contentType: false // contentType を設定させない
}).done(function (response) {
$("#result").empty;
$("#result").html(response);
}).fail(function (jqXHR, textStatus, errorThrown) {
$("#result").empty;
$("#result").text('textStatus: ' + textStatus +
', errorThrown: ' + errorThrown);
});
});
} else {
$("#result").empty;
$("#result").text('File API がサポートされてません。');
}
});
//]]>
</script>
}
Controller / Action Method
Ajax 呼び出しのみ可能としていますが、普通に input type="submit" ボタンをクリックして post した場合でもファイルのアップロードはできます。ただしその場合は最後のファイル選択操作で選んだファイルのみが送信されます。
using System.Web;
using System.Web.Mvc;
using Mvc5App.Models;
using System.IO;
using System.Collections.Generic;
namespace Mvc5App.Controllers
{
public class FileController : Controller
{
public ActionResult MultipleUpload2()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult MultipleUpload2(MultipleUploadModels model)
{
if (!Request.IsAjaxRequest())
{
return Content("Ajax 呼び出しのみ可能");
}
string result = "";
string customFiled = model.CustomField;
IList<HttpPostedFileBase> postedFiles = model.PostedFiles;
// ファイルが選択されてない場合でも postedFiles は null にならないし、
// postedFiles.Count は 0 にならない(1 になる)。従い、以下のコード
// では制御が else に飛ぶことはないので注意
if (postedFiles != null && postedFiles.Count > 0)
{
foreach (HttpPostedFileBase postedFile in postedFiles)
{
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 アップロード完了<br />";
}
}
result += "CustomField の文字列: " + customFiled;
}
else
{
result = "ファイルアップロードに失敗しました";
}
return Content(result);
}
}
}
注意した方がよさそうと思う点を以下に書いておきます。
-
複数回送信する場合、送信し終わったら View の JavaScript のコードにある fd をクリアして最初から始めないと、次に送信するとき前回選んだファイルも送信されダブってしまいます。上のコードはそこは未対応なので注意してください。
-
ファイルを選択しないで送信した場合でも、上の Controller / Action Method のコードにある postedFiles は null にならないし、postedFiles.Count は 0 になりません(1 になる)。従い、上のコードでは "ファイルアップロードに失敗しました" というエラーメッセージは出ません。
-
View のコードにある fd.append("CustomField", "This is some extra data"); でデータを送信して Model の CustomField プロパティにバインドできます。テキストボックスを配置してユーザーに入力してもらい送信してモデルバインドすることも考えましたが、送信前に FormData オブジェクトに append するのが難しく、それは今後の検討課題です。
-
上の 2 に書いたファイルを選択しないで送信した場合でも postedFile が null にならず、postedFiles.Count は 1 になる理由は、内容が空のパート(下の Fiddler による要求のキャプチャ画像の 2 つ目のパート)が送信されてくるからです。普通にファイルを選択して input type="submit" ボタンクリックで送信すれば空のパートはなくなりますが、この記事のように File API と FormData を JavaScript で細工するとそれが残ってしまいます。
想像ですが、var fd = new FormData(document.querySelector("form")); で下の画像の 2 つ目までのパートが取得され、その後の操作で 3 つ目以降が追加されていくからであろうと思います。空のパートを削除する方法は分かりませんでした。