by WebSurfer
2015年1月3日 13:54
ファイルアップロードに用いる ASP.NET サーバーコントロールの FileUpload をページに配置すると、ASP.NET がブラウザに送信する html コードをレンダリングする際、form 要素に enctype="multipart/form-data" を追加します。
下の画像で、赤線で囲った部分がそれです。ちなみに、ソースコードでは form 要素は <form id="form1" runat="server"> となっています。
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" 属性の設定がないと、ファイルはアップロードされません。当然、サーバー側でファイルを取得できません。
知ってました? 実は、サーバーコントロールしか使ったことがない自分は、知らなかったです。なので、そこに気がつかず、ちょっとハマってしまいました。(汗)
by WebSurfer
2013年2月16日 16:50
画像、データ、ファイルなど、Page でないコンテンツをダウンロードするには、.aspx ページを利用するより、HTTP ハンドラ(.ashx)を利用したほうが良さそうだという話です。
.aspx ページを利用してファイルをダウンロードするサンプルは、MSDN ライブラリなどでもよく目にしますが、いろいろ問題がありそうです。
MSDN ライブラリの HttpResponse クラス にある画像をダウンロードするサンプルコードを例に取って問題点を説明します。
このコードでは、まず bmp.Save(Response.OutputStream, ImageFormat.Jpeg) メソッドによって画像データが jpeg 形式で出力ストリームに保存され、その後 Response.Flush メソッドによって保存された画像データが chunked コーディングされてクライアントに送信されます。
そのあと直ちに、データの終了を示す 0 が送信されればいいのですが、サーバーは <!DOCTYPE ... で始まる html コードも生成し、それもクライアントに送信してから終了します。
従って、jpeg 形式のデータとしては余計な html コードが含まれてしまいます。IE9 で試した限りでは画像は表示されましたが、すべてのブラウザで問題ないかは保障の限りではありません。
以下のような解決策を考えましたが、.aspx ページを利用する限り決定打はなさそうです。
-
Response.Flush の後、Response.Close する ⇒ 時々そのようなサンプルを目にしますが、これは解決策というよりは改悪です。Response.Close は "クライアントへのソケット接続を閉じます" ということなので、 chunked コーディングでデータの終了を示す 0 が送信されないまま終了してしまいます。IE9 でしか試してませんが、画像は表示されません。
-
Response.Flush に替えて Response.End を使う ⇒ 応答ヘッダに Content-Length が指定されて(chunked コーディングされず)、データはまとめて送信されます。送信されるデータの内容も OK です。ただし、最近知ったのですが、.NET 4 以降の MSDN ライブラリの説明で、End メソッドの使用は非推奨になっています。理由は、End メソッドでスローされる ThreadAbortException がパフォーマンスに悪影響を及ぼすからだそうです。代わりに HttpApplication.CompleteRequest メソッドを呼び出せとのことです。
-
Response.Flush に替えて HttpApplication.CompleteRequest を呼び出す ⇒ やはり <!DOCTYPE ... で始まる html コードも送信されてしまいます。Response.Flush と異なる点は、chunked コーディングされず、Content-Length が指定されてデータが(html コードも含めて)まとめて送信されることです。
-
html コードを全部削除し、Flush メソッドも End メソッドも使わない ⇒ これは見かけよさそうです。余計な html コードはくっついてきません。
-
Response.Flush の後 Response.SuppressContent を true に設定する(2016/6/28 追記) ⇒ そうすると画像のみが送信され、<!DOCTYPE html... 以下は送信されません。ただし、Response.Flush の前で true に設定するとコンテンツは一切送信されない(ヘッダと chunked の終わりを示す 0 のみ送信される)ので注意してください。
.aspx をページを使う場合は上記 4 または 5 の方法がよさそうですが、普通と違うことをして思わぬところで副作用が出る可能性が否定しきれません。.aspx をページを使って余計な心配をするより、代わりに http ハンドラ(.ashx)を使った方が良さそうです。http ハンドラなら、Response.End メソッドで強制する等の処置は必要なく、HTTP パイプラインの最後まで普通に実行させれば済みます。
http ハンドラを使ったサンプルを書いておきます。コメントに注意事項を書いたので参考にしてください。
(2014/3/2 追記:IE の場合は UrlEncode を使ってファイル名をエンコードしていますが、半角空白は "+" に変換されるので、ブラウザ側ではそのまま "+" になってしまいます。それが気に入らない場合は、ダウンロードファイル名の文字化け に紹介したサンプルコードを参考に対処してください)
<%@ WebHandler Language="C#" Class="Handler" %>
using System;
using System.Web;
public class Handler : IHttpHandler
{
public void ProcessRequest (HttpContext context)
{
HttpResponse response = context.Response;
HttpRequest request = context.Request;
string fileName = "日本語.txt";
// IE の場合、日本語ファイル名の文字化け対策が必要。
// Firefox, Chrome, Safari, Opera の場合は不用。
// 2014/3/3 修正
// IE11 では Browser プロパティは "Mozilla" (.NET 2.0)
// または "InternetExplorer" (.NET 4) になる。IE の場合
// User Agent には必ず "Trident" と言う文字列が入ってい
// るらしいので、そちらで判定した方がよさそう。
if (request.Browser.Browser.ToUpper().IndexOf("IE") >= 0
|| request.UserAgent.Contains("Trident"))
{
fileName = context.Server.UrlEncode(fileName);
}
// キャッシュを許可するか否か、許可する場合は有効期限を
// 指定しておくべき。
// 以下のコードはキャッシュを許可しない場合の例。応答ヘ
// ッダーは次のようになる。
// Cache-Control: no-cache
// Pragma: no-cache
// Expires: -1
response.Cache.SetCacheability(HttpCacheability.NoCache);
response.Cache.SetExpires(DateTime.Now.ToUniversalTime());
response.Cache.SetMaxAge(new TimeSpan(0, 0, 0, 0));
// ブラウザによってファイルの種類を判断する方法が異なる。
// IE は、Content-Disposition: ヘッダが存在する場合は、
// filename パラメータで設定されたファイル名の拡張子を
// 優先的に使う。Content-Type: ヘッダの指定は無視される。
// 逆に、Opera など、Content-Type: ヘッダの指定を優先的
// に使うものもある。なので、Content-Disposition: ヘッダ
// と Content-Type: ヘッダの両方を正しく指定しておいた方
// がよさそう。
response.AppendHeader("Content-Disposition",
"attachment;filename=" + fileName);
response.ContentType = "text/plain";
// 文字列 "Hello World" を応答 HTTP ストリームに書込み。
// ファイルの場合は TransmitFile メソッドを使うのがお勧
// め(WriteFile メソッドは大きなファイルは扱えないので)
response.Write("Hello World");
// Flush, End, Close メソッド等は使用しないこと。
}
public bool IsReusable
{
get
{
return false;
}
}
}
呼び出し方は、a 要素の href 属性に HTTP ハンドラの URL を設定するのがよさそうです。
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 が正解のように思われます。