WebSurfer's Home

Filter by APML

Web API でファイルアップ/ダウンロード (CORE)

by WebSurfer 17. January 2021 17:41

ファイルをアップロード/ダウンロードする相手が ASP.NET Core 3.1 Web API の場合はどのようにすれば良いかについて書きます。

Web API へファイルアップロード

コードを書いてみましたが、先の記事「ASP.NET Core 3.1 Web API」に書いた、(1) Controller は ControllerBase クラスを継承、(2) ApiControllerAttribute 属性を付与、(3) ルーティングは RouteAttibute 属性を付与して設定、アクションメソッドに [HttpGet], [HttpPost] 属性を付与する以外は MVC の場合とほとんど変わりませんでした。

(MVC の場合は、先の記事「ASP.NET Core MVC でファイルアップロード」と「ASP.NET Core MVC でファイルダウンロード」を見てください)

それでこの記事の話は終わってしまうのですが、それではちょっとブログの記事としては寂しいし、今後の参考になるかもしれないので検証に使ったコードを下にアップしておきます。

Web API コントローラー/アクションメソッド

using Microsoft.AspNetCore.Mvc;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using System.IO;

namespace WebAPI.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class FileUpDownloadController : ControllerBase
    {
        // 物理パスの取得用
        private readonly IWebHostEnvironment _hostingEnvironment;

        public FileUpDownloadController(IWebHostEnvironment hostingEnvironment)
        {
            this._hostingEnvironment = hostingEnvironment;
        }

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

                // アプリケーションルートの物理パスを取得
                // wwwroot の物理パスは WebRootPath プロパティを使う
                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 = "ファイルアップロードに失敗しました";
            }

            return Content(result);
        }

        [HttpGet]
        [ResponseCache(Duration = 0, 
            Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult GetFile(string filename = "sample1.jpg")
        {
            if (string.IsNullOrEmpty(filename))
            {
                return NotFound("引数が null または空");
            }

            // アプリケーションルートの物理パスを取得
            string contentRootPath = _hostingEnvironment.ContentRootPath;

            // ダウンロードするファイルの物理パス
            string physicalPath = contentRootPath + "\\" + 
                                  "Files\\" + filename;

            if (!System.IO.File.Exists(physicalPath))
            {
                return NotFound("指定されたパスにファイルが無い");
            }

            // Content-Disposition ヘッダを設定(RFC 6266 対応してない)
            Response.Headers.Append("Content-Disposition",
                "attachment;filename="+filename);
            
            return new PhysicalFileResult(physicalPath, "image/jpeg");
        }
    }
}

アップロードの検証に使った View

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

<h1>Upload</h1>

<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post" enctype="multipart/form-data">
            <div>
                <div>
                    <p>Upload file using this form:</p>
                    @* name 属性はモデルのクラスのプロパティ名と同じ
                       にしないとサーバー側でモデルバインディングさ
                       れないので注意。大文字小文字は区別しない。*@
                    <input type="file" name="postedfile" />
                </div>
            </div>

        </form>
        <div>
            <div>
                <input type="button" id="ajaxUpload" value="Upload" />
                <div id="result"></div>
            </div>
        </div>
    </div>
</div>

<script src="~/Scripts/jquery.js"></script>
<script type="text/javascript">
    //<![CDATA[
    $(function () {
        $('#ajaxUpload').on('click', function (e) {
            // FormData オブジェクトの利用
            var fd = new FormData(document.querySelector("form"));

            $.ajax({
                url: '/FileUpDownload',
                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>

ダウンロードの検証はブラウザのアドレスバーにコントローラーの URL を入力して FileUpDownload を GET 要求すれば可能です。ファイル名はデフォルトで "sample1.jpg" となっていますが、クエリ文字列で別のファイルを指定できます。

Tags: , , ,

Upload Download

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

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 20. January 2020 12:09

ASP.NET Core 3.1 MVC アプリでファイルをダウンロードする方法を書きます。下の画像のようにファイル名とファイルの種類をブラウザにきちんと認識させるのが条件です。(画像は IE11 の例です。通知バーが正しく出ていることに注目してください)

IE11 の通知バー

.NET Framework MVC5 アプリでファイルをダウンロードする方法は、先の記事「MVC でファイルのダウンロード」に書きましたのでそちらを見てください。

ASP.NET Core 3.1 MVC の場合も基本的なことは .NET Framework MVC5 とほぼ同じで、FileResult Class を継承する VirtualFileResult, FileContentResult, FileStreamResult, PhysicalFileResult オブジェクトのいずれかを Controller のアクションメソッドで生成して返してやることになります。

VirtualFileResult, FileContentResult, FileStreamResult オブジェクトを生成するには File メソッドが使えます。File メソッドの第 1 引数の型に応じて生成されるオブジェクトが異なり、以下のようになります。

  1. string 型: VirtualFileResult - コンテンツが既存のファイルとして提供される場合
  2. byte[] 型: FileContentResult - コンテンツがバイト配列として提供される場合
  3. stream 型: FileStreamResult - コンテンツがストリームとして提供される場合

上の 1 番目の「コンテンツが既存のファイルとして提供される場合」は File メソッドの第 1 引数にファイルの仮想バスを設定します(注:物理パスを指定する .NET Framework MVC5 の File メソッドと異なります)。ファイルはアプリケーションルート直下の wwwroot フォルダ下に置きます(そこ以外では見つからないというサーバーエラーになります)。例えば wwwroot/Files/test.pdf をダウンロードする場合は File メソッドの第 1 引数は "/Files/test.pdf" とします。

物理パスを指定したい場合は PhysicalFileResult クラスを使います。File メソッドを使わず明示的に PhysicalFileResult クラスを初期化して返してやる必要があるようです。

File メソッドの第 2 引数にはファイルの MIME タイプ、第 3 引数には拡張子を含むファイル名を設定します。そうすることにより応答ヘッダに Content-Type と Content-Disposition が適切に設定されます。ブラウザによって Content-Type, Content-Disposition のどちらでファイル名とファイルの種類を判断するかが異なりますので両方をきちんと設定するのは必須です。

.NET Framework MVC5 の File メソッドの場合、第 3 引数に設定するファイル名には US-ASCII 文字を使用しないと IE では文字化けしましたが、Core の File メソッドは RFC 6266 (RFC 2231/RFC 5987) に準拠した Content-Disposition ヘッダを生成するので文字化けの問題は回避できるようになりました。例えば第 3 引数に "日本語.pdf" という文字列を設定すると以下の画像にあるような Content-Disposition ヘッダが生成されます。

応答ヘッダ

キャッシュコントロールは ResponseCacheAttribute で設定します。例えば [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] とすると、上の応答ヘッダの画像のとおり Cache-Control: no-store, no-cache / Pragma: no-cache となります。(注: .NET Framework MVC5 の Response.Cache は使えません)

以下に、順に PhysicalFileResult, VirtualFileResult, FileContentResult, FileStreamResult クラスを使った場合のサンプルコードを書いておきます。

PhysicalFileResult

任意のフォルダにある既存のファイルを物理パスを指定してダウンロードする場合です。File メソッドは使えないようなので PhysicalFileResult クラスを直接初期化して return します。Content-Disposition ヘッダは自力でコードを書いて設定します(RFC 6266 に準拠したい場合はそれも自力でコーディング要)。

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Hosting;
using MvcCoreApp.Models;
using Microsoft.AspNetCore.Http;
using System.IO;

namespace MvcCoreApp.Controllers
{
  public class DownloadController : Controller
  {
    // Core では Server.MapPath が使えないことの対応
    private readonly IWebHostEnvironment _hostingEnvironment;

    public DownloadController(
                      IWebHostEnvironment hostingEnvironment)
    {
      _hostingEnvironment = hostingEnvironment;
    }

    [HttpGet("/downloadfile")]
    [ResponseCache(Duration = 0, 
                   Location = ResponseCacheLocation.None, 
                   NoStore = true)]
    public IActionResult DownloadFile()
    {            
      // Content-Disposition ヘッダを設定
      // (RFC 6266 対応してないので注意)
      Response.Headers.Append("Content-Disposition", 
                            "attachment;filename=test.pdf");

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

      // ダウンロードするファイルの物理パス。アプリケーション
      // ルート直下の Files フォルダの test.pdf とする
      string physicalPath = contentRootPath + "\\" + 
                            "Files\\" + "test.pdf";
      return new PhysicalFileResult(physicalPath, 
                                    "application/pdf");
    }
  }
}

VirtualFileResult

wwwroot フォルダ下にある既存のファイルを仮想パスを指定してダウンロードする場合です。File メソッドが利用できます。

[HttpGet("/downloadfile")]
[ResponseCache(Duration = 0, 
               Location = ResponseCacheLocation.None, 
               NoStore = true)]
public IActionResult DownloadFile()
{
  // wwwroot/Files フォルダ内の test.pdf を指定
  string virtualPath = "/Files/test.pdf";

  // RFC 6266 に準拠した Content-Disposition ヘッダを生成する
  // ので第 3 引数のファイル名には日本語が使えます
  return File(virtualPath, "application/pdf", "日本語.pdf");
}

FileContentResult

ダウンロードするコンテンツがバイト配列として提供される場合です。コンテンツを SQL Server からバイト配列として取得するような時に利用できそうです。

[HttpGet("/downloadfile")]
[ResponseCache(Duration = 0, 
               Location = ResponseCacheLocation.None, 
               NoStore = true)]
public IActionResult DownloadFile()
{
  // 以下はバイト配列を取得するための単なるサンプル
  string contentRootPath = 
                    _hostingEnvironment.ContentRootPath;
  string physicalPath = contentRootPath + "\\" + 
                        "Files\\" + "test.pdf";
  byte[] data = System.IO.File.ReadAllBytes(physicalPath);

  return File(data, "application/pdf", "日本語.pdf");
}

FileStreamResult

ダウンロードするコンテンツがストリームとして提供される場合です。コンテンツを Web API から HttpClient を利用して取得するような時に利用できるでしょうか。

[HttpGet("/downloadfile")]
[ResponseCache(Duration = 0, 
               Location = ResponseCacheLocation.None, 
               NoStore = true)]
public IActionResult DownloadFile()
{
  // 以下は stream を取得するための単なるサンプル
  string contentRootPath = 
                    _hostingEnvironment.ContentRootPath;
  string physicalPath = contentRootPath + "\\" + 
                        "Files\\" + "test.pdf";
  FileStream stream = 
            new FileStream(physicalPath, FileMode.Open);

  return File(stream, "application/pdf", "日本語.pdf");
}

Tags: , ,

Upload Download

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。ブログ2はそれ以外の日々の出来事などのトピックスになっています。

Calendar

<<  February 2026  >>
MoTuWeThFrSaSu
2627282930311
2345678
9101112131415
16171819202122
2324252627281
2345678

View posts in large calendar