WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

異なるフォルダのファイルをアップロード

by WebSurfer 9. July 2020 21:33

ブラウザを使って自分の PC の複数のフォルダから複数のファイルを一度にアップロードするにはどうしたらよいかという話を書きます。

複数ファイルをアップロード

html の input type="file" 要素を使うことが前提です。ブラウザがサポートしていれば input type="file" 要素に multiple="multiple" を追加すれば、ユーザーは複数のファイルを選択して一度にアップロードすることができます。

ファイルを選択する際、上の画像の[ファイル選択]ボタン(画像は Chrome の例です。ブラウザによって異なります)をクリックすると下の画像のダイアログが表示されますので、Shift キーや Ctrl キーを使って複数のファイルを選択できます。

送信するファイルの選択

その後[開く(O)]ボタンをクリックすると送信準備完了となり、form を method="post" で submit してやればサーバーに選択された複数のファイルが送信されます。

ただし、上の操作で送信するファイルを選択できるのは一回だけです。この後、もう一度同じ操作を行って別のファイルを追加することはできません。それをすると、前の操作で選択したファイルは送信対象には含まれなくなり、後の操作で選択したファイルのみが送信されるようになります。それは同じフォルダで繰り返し行う場合も異なるフォルダに移動して行う場合も同じです。

ということは、フォルダが異なるファイルを選択するには各フォルダに移動して選択操作を行わざるを得ませんが、送信できるのは最後の操作で選択したファイルのみになります。なので、複数のフォルダにある複数のファイルを一度にアップロードするのは普通のやり方(form を submit する方法)ではできないようです。

代案は HTML5 File APIFormData を利用し 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);
        }
    }
}

注意した方がよさそうと思う点を以下に書いておきます。

  1. 複数回送信する場合、送信し終わったら View の JavaScript のコードにある fd をクリアして最初から始めないと、次に送信するとき前回選んだファイルも送信されダブってしまいます。上のコードはそこは未対応なので注意してください。
  2. ファイルを選択しないで送信した場合でも、上の Controller / Action Method のコードにある postedFiles は null にならないし、postedFiles.Count は 0 になりません(1 になる)。従い、上のコードでは "ファイルアップロードに失敗しました" というエラーメッセージは出ません。  
  3. View のコードにある fd.append("CustomField", "This is some extra data"); でデータを送信して Model の CustomField プロパティにバインドできます。テキストボックスを配置してユーザーに入力してもらい送信してモデルバインドすることも考えましたが、送信前に FormData オブジェクトに append するのが難しく、それは今後の検討課題です。
  4. 上の 2 に書いたファイルを選択しないで送信した場合でも postedFile が null にならず、postedFiles.Count は 1 になる理由は、内容が空のパート(下の Fiddler による要求のキャプチャ画像の 2 つ目のパート)が送信されてくるからです。普通にファイルを選択して input type="submit" ボタンクリックで送信すれば空のパートはなくなりますが、この記事のように File API と FormData を JavaScript で細工するとそれが残ってしまいます。

    想像ですが、var fd = new FormData(document.querySelector("form")); で下の画像の 2 つ目までのパートが取得され、その後の操作で 3 つ目以降が追加されていくからであろうと思います。空のパートを削除する方法は分かりませんでした。

要求ヘッダとコンテンツ

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

CORE

HttpClient でファイルアップロード

by WebSurfer 11. August 2019 14:00

HttpClient クラス を使った Windows Forms アプリで multipart/form-data 形式にてファイルをアップロードする方法を書きます。

HttpClient でファイルをアップロード

multipart/form-data 形式で送信するには MultipartFormDataContent クラスを利用します。

MultipartFormDataContent クラスのインスタンスを生成し、それに HttpContent クラスの派生クラスを multipart の各パートとして Add します。

この記事では文字列とファイルを別々のパートとして送信するサンプルを書きます。

その場合、使用する HttpContent クラスの派生クラスは、文字列を送信する場合は StringContent クラスを、ファイルを送信する場合は StreamContent クラスを使うのがよさそうです。

下のサンプルコードを見てください、StringContent クラスとStreamContent クラスを初期化してヘッダ情報を設定し、それぞれを MultipartFormDataContent オブジェクトに Add し、さらにそれを HttpRequestMessage オブジェクトの Content プロパティに設定しています。

using System;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;

namespace WindowsFormsApplication2
{
  public partial class Form3 : Form
  {
    // socket 浪費問題対応
    private static HttpClient httpClient;

    public Form3()
    {
      InitializeComponent();
      this.textBox1.Text = "";
      this.label1.Text = "";
    }

    // ファイル選択に OpenFileDialog 利用
    private void button1_Click(object sender, EventArgs e)
    {
      OpenFileDialog openFileDialog1 = new OpenFileDialog();

      openFileDialog1.InitialDirectory = 
                               @"C:\Users\surfe\Pictures";
      openFileDialog1.Filter = 
           "jpeg files (*.jpg)|*.jpg|All files (*.*)|*.*";
      openFileDialog1.FilterIndex = 1;
      openFileDialog1.RestoreDirectory = true;

      if (openFileDialog1.ShowDialog() == DialogResult.OK)
      {
        this.textBox1.Text = openFileDialog1.FileName;
      }
    }

    // HttpWebRequest を利用
    private void button2_Click(object sender, EventArgs e)
    {
      // ・・・コードは省略・・・
    }

    // HttpClient を利用
    private async void button3_Click(object s, EventArgs e)
    {
      if (string.IsNullOrEmpty(this.textBox1.Text)) return;

      this.label1.Text = "";

      if (httpClient == null)
      {
        httpClient = new HttpClient();
      }

      //送信するファイルへのパス
      string filePath = this.textBox1.Text;
      string fileName = Path.GetFileName(filePath);

      string url = @"送信先 Web サーバーの URL";

      MultipartFormDataContent content = 
                              new MultipartFormDataContent();
            
      // ファイルのみでなく文字列も送信してみる
      string strData = "これは、テストです。";
      StringContent stringContent = new StringContent(strData);
      stringContent.Headers.ContentDisposition = 
               new ContentDispositionHeaderValue("form-data")
      {
        Name = "comment"
      };
      content.Add(stringContent);

      // アップロードするファイル
      using (FileStream fs = new FileStream(filePath, 
                                            FileMode.Open, 
                                            FileAccess.Read))
      {
        StreamContent streamContent = new StreamContent(fs);
        streamContent.Headers.ContentDisposition = 
               new ContentDispositionHeaderValue("form-data")
        {
          Name = "upfile",
          FileName = fileName
        };                
        streamContent.Headers.ContentType = 
                    new MediaTypeHeaderValue("image/jpeg");
        content.Add(streamContent);

        // メソッド (POST) と送信先の URL 指定
        HttpRequestMessage request = 
                 new HttpRequestMessage(HttpMethod.Post, url);
        request.Content = content;

        // ここでファイルを HTTP ストリームに書き込むので、
        // 以下は using の { } 内にないとファイルが読めな
        // いというエラーになる
        HttpResponseMessage response = 
                           await httpClient.SendAsync(request);

        // 応答のコンテンツを Stream として取得
        using (Stream responseStream = 
                   await response.Content.ReadAsStreamAsync())
        {
          using (StreamReader sr = 
              new StreamReader(responseStream, Encoding.UTF8))
          {
            this.label1.Text = sr.ReadToEnd();
          }
        }
      }            
    }
  }
}

上記のコードを実行して、HttpClient を使ってファイルを送信し、返ってきた応答を表示したのが上に表示した画像です。

下の画像は Fiddler でのキャプチャ結果で、送信ヘッダとコンテンツを表示しています。送信ヘッダに指定されている boundary で StringContent (文字列) とStreamContent (ファイル) の部分が分けられ、各パートに Content-Disposition などのヘッダ情報が付与されているのが分かるでしょうか?

送信ヘッダとコンテンツ

最後になりましたが HttpClient を使う際の注意点として重要なことを書いておきます。それは、using 句を使うなどしてHttpClient の初期化と Dispose を繰り返すと socket が浪費されるという問題があるということです。詳しくは以下の記事を見てください。static にして使い回すのが良いとのことです。

YOU'RE USING HTTPCLIENT WRONG AND IT IS DESTABILIZING YOUR SOFTWARE

Tags: ,

Upload Download

About this blog

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

Calendar

<<  November 2020  >>
MoTuWeThFrSaSu
2627282930311
2345678
9101112131415
16171819202122
23242526272829
30123456

View posts in large calendar