WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

FileUpload と CustomValidator

by WebSurfer 2015年3月10日 14:02

ASP.NET サーバーコントロールの FileUpload を使用してファイルをアップロードする際に、検証コントロールの CustomValidator を利用してファイルのサイズとタイプを検証し、検証結果 NG の場合はファイルをセーブしない方法について書きます。

アップロードされるファイルの検証

注:この記事は単一ファイルのアップロードの場合に限った話です。(ASP.NET 4.5 以降では FileUpload コントロールに AllowMultiple, HasFiles, PostedFiles プロパティなどが追加されています。なので、Web サーバーが ASP.NET 4.5 以降、ブラウザが HTML5 をサポートしていれば FileUpload を使っての複数ファイルの同時アップロードが可能です・・・が、それは今回は考えていません)

ファイルのアップロード後に、サーバー側だけでファイルのサイズやタイプを検証するのであれば、CustomValidator を使うメリットはあまりなさそうです。

しかし、RequiredFieldValidator, RegularExpressionValidator 等と同様に、ユーザー入力を submit する前にクライアント側で検証し、検証結果 NG の場合は submit を中止してエラーメッセージを表示するという機能を実装する場合は CustomValidator を利用するのがお勧めです。

理由は、(1) 他の ASP.NET 検証コントロールとの統一性が保てること、(2) 検証結果 NG の場合の submit の中止とエラーメッセージの表示のため検証コントロールの既存の機能が使えるということです。(特に (2) は ASP.NET の仕組みをよく理解していないと自力で実装するのは難しいと思われます)

CustomValidator を利用する場合、サーバー側での検証に使うメソッド、クライアント側での検証に使う JavaScript 関数のコードは自力で書かなければなりません。

サーバー側は特に難しくはないと思いますが、問題はクライアント側です。もともと JavaScript にはローカルファイルにアクセスする機能はありません。なので、JavaScript ではファイルのサイズやタイプなどの情報は取得できず、クライアント側での検証は諦めざるを得ませんでした。

ところが HTML5 によって話が変わってきました。今は、ブラウザが HTML5 File API 仕様をサポートしていれば、その API を利用して JavaScript でファイルのサイズやタイプなどの情報を取得でき、クライアント側での検証が可能が可能です。

以下に CustomValidator を使用して、クライアント側とサーバー側の両方でファイルのサイズとタイプを検証し、サイズが 500,000 bytes を超える場合、タイプが image/jpeg 以外の場合は検証 NG としてファイルのアップロードを中断するサンプルコードをアップしておきます。

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
  // サイズ制限 500,000 bytes, タイプ制限 image/jpeg とした。
  // ハードコーディングしないで web.config で appSettings に
  // 設定する方が良いかも。
  int maxFileSize = 500000;
  string allowedContentType = "image/jpeg";
    
  protected void Page_Load(object sender, EventArgs e)
  {
    Label1.Text = "";
  }
    
  protected void Button1_Click(object sender, EventArgs e)
  {
    if (Page.IsValid)
    {
      // FileUploadTest はアプリケーションルート直下に作った
      // ファイル格納用フォルダ。その物理パスを取得
      string savePath = MapPath("~/FileUploadTest/");

      savePath += Server.HtmlEncode(FileUpload1.FileName);

      FileUpload1.SaveAs(savePath);

      // アップロード完了時のメッセージ表示
      Label1.Text = FileUpload1.FileName +
        " (" + FileUpload1.PostedFile.ContentType + ") - " +
        FileUpload1.PostedFile.ContentLength.ToString() + 
        " bytes アップロード完了";
    }
  }

  // サーバー側の検証に使うメソッド。
  // ServerValidate イベントのハンドラに設定する 
  protected void ServerValidate(object source, 
      ServerValidateEventArgs args)
  {
    // FileUpload にファイルが格納されていることを確認
    if (FileUpload1.HasFile)
    {
      // MIME コンテンツタイプの取得 / 制限
      string contentType = FileUpload1.PostedFile.ContentType;
      if (contentType != allowedContentType)
      {
        ((CustomValidator)source).ErrorMessage =
              "タイプが " + allowedContentType + 
              "ではありません。";
        args.IsValid = false;
        return;
      }
        
      // ファイルサイズの取得 / 制限
      int fileSize = FileUpload1.PostedFile.ContentLength;
      if (fileSize > maxFileSize)
      {
        ((CustomValidator)source).ErrorMessage =
            "サイズが " + maxFileSize.ToString() +
            " bytes を超えています。";
        args.IsValid = false;
        return;
      }

      args.IsValid = true;
    }
    else
    {
      ((CustomValidator)source).ErrorMessage = 
          "ファイルが選択されていません";
      args.IsValid = false;
    }
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
  <title>FileUpload</title>
  <script type="text/javascript">
  //<![CDATA[
    // クライアント側での検証用 JavaScript 関数。
    // ClientValidationFunction プロパティに設定する
    function ClientValidate(val, args)
    {
      // HTML5 File API の File, FileList がサポートされている
      // 場合のみ検証する。サポートされてないとスクリプトエラ
      // ーになるので下の if 文は必要
      if (window.File && window.FileList) 
      {
        var fileUpload = 
          document.getElementById("<%=FileUpload1.ClientID%>");

        // if (fileUpload.files[0] == null) でも引っかかるがファイルが
        // 選択されてないと fileUpload.files[0] は undefined になるので
        // 以下のようにするのが正解かも
        if (fileUpload.files[0] == undefined ||
            fileUpload.files[0] == null) {
        {
          // ここでサーバー側と同様に ErrorMessage プロパティ
          // に設定されたエラーメッセージを書き換えようとした
          // が不可。ウラワザ的方法があるかもしれないが諦めた。
          // 下の alert はデバッグ時の検証用
          alert("ファイル未選択");
          args.IsValid = false;
          return;
        }

        if (fileUpload.files[0].type != '<%=allowedContentType%>') 
        {
          // alert はデバッグ時の検証用
          alert("タイプ相違");
          args.IsValid = false;
          return;
        }

        // 2021/1/26 訂正
        // Number() を使わないと Chrome, Edge で Synrax error になる。
        // ブログの記事を書いた 2015/3/10 時点では問題なかったが
        // JavaScript の仕様が変わった?
        if (fileUpload.files[0].size > Number(<%=maxFileSize%>)) {
        {
          // alert はデバッグ時の検証用
          alert("サイズオーバー");
          args.IsValid = false;
          return;
        }
      }
      args.IsValid = true;
    }
  //]]>
  </script>
</head>
<body>
  <form id="form1" runat="server">
  <div>
    <h4>Select a file to upload:</h4>

    <asp:FileUpload id="FileUpload1"                 
      runat="server">
    </asp:FileUpload>
           
   <%-- CustomValidator だけは、ControlToValidate プロ
    パティを設定しなくても使用可。ValidateEmptyText プ
    ロパティはデフォルトの false のままで検証できる。
    ControlToValidate="FileUpload1" としておくと、
    ファイルを選択した時点でクライアント側の検証がかかる。
    ただし、そうすると未選択時にはクライアント側での検証
    はかからなくなるので注意--%>
    <asp:CustomValidator 
      ID="CustomValidator1" 
      runat="server" 
      ForeColor="Red"
      OnServerValidate="ServerValidate" 
      ClientValidationFunction="ClientValidate"            
      ErrorMessage="CustomValidator">
    </asp:CustomValidator>
    <br/>
    <br/>

    <asp:Button id="Button1" Text="Upload file"
      OnClick="Button1_Click" runat="server">
    </asp:Button>

    <hr />

    <asp:Label id="Label1" runat="server" />
  </div>
  </form>
</body>
</html>

自分が持っているブラウザで試した限りですが、File API がサポートされていてクライアント側での検証が可能だったのは、Firefox 36.0.1, Chrome 41.0.2272.76, Safari 5.1.7, Opera 12.17 でした。

IE10 以上も File API をサポートしているそうですが、自分は持ってないので検証できていません。IE9 で検証してみましたが、やはり File API は未サポートのようで、クライアント側での検証はかかりません。ただし、サーバー側での検証はかかるので、そこでは制限できます。

この記事の上にある画像は、上のサンプルコードを Chrome で表示した時のもので、MIME タイプが image/jpeg ではないのでクライアント側での検証結果が NG となり、ErrorMessage プロパティに設定されたエラーメッセージ "CustomValidator" が赤文字で表示されたところです。

注: CustomValidator の ControlToValidate プロパティを "FileUpload1" に設定しておくと、ファイルを選択した時点でクライアント側の検証がかかります。ただし、そうすると未選択時にはクライアント側での検証はかからなくなるので注意してください。上のサンプルコードでは ControlToValidate を設定 していませんので、submit(すなわち[Upload file]ボタンクリック)時点でクライアント側の検証がかかっています。

エラーメッセージ "CustomValidator" は、CustomeValidator をドラッグ&ドロップしたときのデフォルト ErrorMessage="CustomValidator" で、もちろんこれは初期設定で変更できます。ただし、クライアント側でエラーの内容に応じてこれを動的に書き換えることはできないようです(サーバー側では上のコードのようにして書換可能でしたが)。

最後に、HTML5 File API に関して参考になると思われる(と言うか自分が参考にした)記事へのリンクを張っておきます。

Tags: , ,

Validation

FileUpload と form 要素 の enctype

by WebSurfer 2015年1月3日 13:54

ファイルアップロードに用いる ASP.NET サーバーコントロールの FileUpload をページに配置すると、ASP.NET がブラウザに送信する html コードをレンダリングする際、form 要素に enctype="multipart/form-data" を追加します。

下の画像で、赤線で囲った部分がそれです。ちなみに、ソースコードでは form 要素は <form id="form1" runat="server"> となっています。

enctype の追加

FileUpload コントロールに限らず、HtmlInputFile や Ajax Control Toolkit の AjaxFileUpload, AsyncFileUpload を使ったり、<input type="file" ... /> 要素に runat="server" 属性を追加(結果、HtmlInputFile と同じになる)しても同様です。

普通の html 要素の <input type="file" ... /> を使った場合はそういうことはなく、自力で enctype="multipart/form-data" を form 要素に追加してやらなければなりません。

form 要素に enctype="multipart/form-data" 属性の設定がないと、ファイルはアップロードされません。当然、サーバー側でファイルを取得できません。

知ってました? 実は、サーバーコントロールしか使ったことがない自分は、知らなかったです。なので、そこに気がつかず、ちょっとハマってしまいました。(汗)

Tags: ,

Upload Download

アップロードされたファイルの一時保存先

by WebSurfer 2011年8月1日 23:01

サーバーにアップロードされたファイルがディスクに書き込まれる前に、サーバーのどこに一時保存されるでしょうか?

アップロードしたファイルの一時保存先

メモリかと思っていましたが、そうではなくて、ある値を超えるとディスクにバッファリングされるそうです。

その「ある値」というのは、httpRuntime 要素の requestLengthDiskThreshold 属性の 設定値で、デフォルト値はフォームやアップロードされたファイルすべてを含めて .NET 3.5, 4 の場合 80KB、.NET 2.0, 3.0 の場合は 256 バイトです(下の注記参照)。

以前、フォーラムで、「FileUpload を Session に保存しておいて、後で Save しようとしたが、ファイルサイズが大きいと失敗する。そのサイズの限度が大体 80KB」という報告があって、その原因究明に悩んだという話がありました。ディスクにバッファリングされることを知って理由が分りました。どうやらディスクに一時保存されたファイルは、サーバーが応答を返した後、消去されてしまうようです。

80KB 程度でディスクにバッファリングするのは効率が悪そうですが、同時に多数のユーザーがファイルをアップロードするようなサイトでは、メモリが分断されてすぐにメモリ不足になってしまうそうです。

逆に、めったに大きなファイルのアップロードはないサイトなら、ディスクにバッファリングされないように requestLengthDiskThreshold 属性の設定値を大きくしておく方がよさそうです。ちなみに、このブログでは 16MB に設定してあります。

<注>

デフォルト値は、以下のように MSDN ライブラリに書いてあることがいろいろ違っていて、イマイチはっきりしないので注意してください。

FileUpload クラス:要求の処理中にアップロードするファイルをメモリ内とサーバー上のどちらに一時的に格納するかを制御するには、httpRuntime 要素の requestLengthDiskThreshold 属性を設定します。この属性を設定すると、入力ストリームバッファーのサイズを管理できます。既定値は 256 バイトです。

HttpPostedFile クラス:既定では、フォームフィールドやアップロードされたファイルを含めて、サイズが 256KB を超えるすべての要求は、サーバーのメモリにではなくディスクにバッファーされます。

HttpRuntimeSection クラスの RequestLengthDiskThreshold プロパティ:入力ストリーム バッファリングのしきい値を示すバイト数。 既定値は 256 バイトです。

httpRuntime 要素の requestLengthDiskThreshold 属性:入力ストリームのバッファリングしきい値の限界値を KB 単位で指定します。 この値は、maxRequestLength 属性を超えないようにします。この属性は .NET Framework 2.0 で新たに追加されました。既定値は、80KB です。(左記は .NET 3.5, 4 の説明です。.NET 2.0, 3.0 の説明では「既定値は 256 です」となっています)。

実際に HttpRuntimeSection.RequestLengthDiskThreshold プロパティでデフォルト値を取得してみたところ、.NET 3.5, 4 の場合 80 でした(上の画像参照)。

MSDN ライブラリの RequestLengthDiskThreshold プロパティの説明では "プロパティは、入力ストリームのバッファリングのしきい値をバイト数で指定します" となっています(KB ではなくて)。でも、実際は 80 バイトではなくて、80KB が正解のように思われます。

Tags:

Upload Download

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar