WEB ダウンロード

Web アプリケーションでのスマートな ASP.NET ファイル ダウンロードの作成

Joe Stagner


この記事で取り上げる話題:

  • ASP.NET サイトからのダイナミック ダウンロード
  • リンクのダイナミック生成
  • 再開可能なダウンロードおよびカスタム ハンドラ
  • カスタム ダウンロード メカニズムに関わるセキュリティ上の懸案事項

この記事で使用する技術:

  • ASP.NET

サンプルコードのダウンロード:
Downloading2006_09.exe
(174 KB)


目次

基本ダウンロード リンク
すべてのファイル タイプの強制ダウンロード
いくつかの部分に分割した巨大ファイルのダウンロード
さらに気のきいた解決策
失敗したダウンロードの再開

おそらく、貴社の Web サイトからユーザーがファイルをダウンロードする必要が生じることはよくあると思われます。 他方、ダウンロードを設けるのは、リンクを設けるのと同じくらい簡単なので、そのプロセスに関する記事を読む必要すらないのではないでしょうか。 しかし、Web 機能の多方面にわたる革新によって、その作業はそれほど簡単ではないと思える理由はたくさんあります。 たとえば、ファイルが、ブラウザに表示されるコンテンツとしてではなく、ファイルとしてダウンロードされるのが望ましい場合があります。 たとえば、ファイルへのパスがまだ分からない (あるいは、ファイルはディスク上に存在さえしていない) ので、おなじみのシンプルな HTML リンクを使用できない場合もあります。 たとえば、大量のダウンロード中にユーザーの接続が切断されるのではないかと心配する必要がある場合もあります。

この記事では、そのような問題に対するいくつかの解決策を示します。それによって、貴社のユーザーは、エラーを生じないより速やかなダウンロード操作を行えるようになります。 この後、動的にリンクを生成する方法を解説し、既定のファイル動作をう回する方法を説明し、HTTP 1.1 機能を使用する再開可能な ASP.NET 起動のダウンロードを取り上げます。

基本ダウンロード リンク

最初に、ミッシング リンクの問題に取り組みましょう。 ファイルへのパスがどのようになるかが分からない場合、単にリンクのリストを後でデータベースから引くことができます。 さらに、実行時に指定のディレクトリ内のファイルを列挙することで、リンク リストを動的に作成することさえ可能です。 この後、第 2 のアプローチを探ってみます。

図 1に示されているとおり、Visual Basic 2005 で DataGrid を作成し、ダウンロード ディレクトリ内のすべてのファイルへのリンクをそこに詰め込んだと想定してください。 それを行うためには、ページ内で Server.MapPath を使用して、ダウンロード ディレクトリ (この場合は ./downloadfiles/) への完全パスを取り出し、DirectoryInfo.GetFiles を使用して、そのディレクトリ内のすべてのファイルのリストを取り出し、次に、その結果の FileInfo オブジェクトの配列から、各関連プロパティ用の列を備えた DataTable を構築しました。 この DataTable を、ページ上の DataGrid にバインドすることができます。そこから、次のように HyperLinkColumn 定義を使用して、リンクを生成することができます。

<asp:HyperLinkColumn DataNavigateUrlField="Name"
    DataNavigateUrlFormatString="downloadfiles/{0}"
    DataTextField="Name"
    HeaderText="File Name:"
    SortExpression="Name" />

リンクをクリックすると、各ファイル タイプを開くためにどのヘルパ アプリケーションが登録されているかに応じて、各ファイルはそれぞれ異なるやり方でブラウザによって扱われることに気付かれるでしょう。 既定では、.asp ページ、.html ページ、.jpg、.gif、.txt のどのページをクリックしても、それはブラウザそのものの中で開かれ、[名前を付けて保存] ダイアログは表示されません。 その理由は、これらのファイル拡張子は既知の MIME タイプであるからです。 そのため、ブラウザ自身がファイルのレンダリング方法を承知しているか、あるいは、ブラウザで使用されるヘルパ アプリケーションがオペレーティング システムに備えられています。Webcasts (.wmv や .avi など)、PodCasts (.mp3 あるいは .wma)、PowerPoint ファイル、およびすべての Microsoft Office 文書は既知の MIME タイプであるので、既定としてインラインで開かないようにしようとすると、問題が起きます。


図 1 DataGrid 内の単純な HTML リンク

しかも、このやり方のダウンロードを使用可能にすると、裁量に委ねられるのは、非常に一般的なアクセス コントロール メカニズムのみです。 個々のディレクトリ単位であればダウンロード アクセスを制御できますが、個々のファイルあるいはファイル タイプへのアクセスの制御には、詳細にわたるアクセス コントロールが必要になります。これは、Web マスタおよびシステム管理者にとって非常に手間のかかる厄介な作業です。 さいわい、ASP.NET および .NET Framework にはいくつかの解決策が用意されています。 そのいくつかを以下に示してあります。

  • Response.WriteFile メソッドを使用する。
  • Response.BinaryWrite メソッドを使用してファイルのストリーミングを行う。
  • ASP.NET 2.0 で Response.TransferFile メソッドを使用する。
  • ISAPI フィルタを使用する。
  • カスタム ブラウザ コントロールへ書き込む。

すべてのファイル タイプの強制ダウンロード

上記の解決策のうち、最も簡単に採用できるものは Response.WriteFile メソッドです。 基本構文は、次のように、非常にシンプルです。完成したこの ASPX ページは、クエリ文字列パラメータとして指定されたファイル パスを探し、そのファイルをクライアントに提供します。

<%@ Page language="VB" AutoEventWireup="false" %>
<html>
   <body>
        <%
            If Request.QueryString("FileName") Then
                Response.Clear()
                Response.WriteFile(Request.QueryString("FileName"))
                Response.End()
            End If
        %>
   </body>
</html>

IIS ワーカー プロセス (IIS 5.0 では aspnet_wp.exe、IIS 6.0 では w3wp.exe) でコードを実行し、Response.Write を呼び出すと、ASP.NET ワーカー プロセスから IIS プロセス (inetinfo.exe あるいは dllhost.exe) へのデータの送信が開始されます。 ワーカー プロセスから IIS へデータが送信されると、そのデータはメモリ内のバッファに入れられます。 たいていは、これに関して心配することは何もありません。 しかし、非常に巨大なファイルの場合、これは有用な解決策にはなりません。

 

プラスの面としては、ファイルを送信する HTTP 応答が ASP.NET コードで作成されるので、ASP.NET の認証および許可のすべてのメカニズムへの全アクセス権限が与えられます。したがって、認証状況に基づいて、つまり実行時に Identity および Principal オブジェクトが存在するかどうか、あるいはその他の適切と思われるメカニズムに基づいて、決断を下すことができます。

このように、ASP.NET の組み込みのユーザー/グループ メカニズムのような既存のセキュリティ メカニズム、Authorization Manager および定義済みのロール グループなどの Microsoft サーバー アドイン、Active Directory Application Mode (ADAM) あるいは Active Directory でさえも統合して、ダウンロードの許可に対する肌理の細かい制御を行うことができます。

また、アプリケーション コード内部からのダウンロードを実施して、既知の MIME タイプの既定動作を置き換えることもできます。 そのためには、表示するリンクを変更する必要があります。ASPX ページにポストバックするハイパーリンクを構成するコードは次のとおりです。

<!-- FileFetch.aspx 内の DataGrid 定義において -- >
<asp:HyperLinkColumn DataNavigateUrlField="Name"
    DataNavigateUrlFormatString="FileFetch.aspx?FileName={0}"
    DataTextField="Name"
    HeaderText="File Name:"
    SortExpression="Name" />

次に、ページが要求されたときにクエリ文字列を検査する必要があります。それは、その要求が、クライアントのブラウザに送信される予定のファイル名引数を含むポストバックであるかどうかを確かめるためです (図 2 を参照)。 次に、Content-Disposition 応答ヘッダーのおかげで、グリッド内のリンクの 1 つをクリックすると、MIME タイプが何であっても、保存ダイアログが表示されます (図 3 を参照)。 また、どのファイルがダウンロード可能であるかに関する制限は、IsSafeFileName という名前のメソッドの呼び出しの結果に基づいていることにも注意してください。 このような措置をとる理由に関してと、このメソッドが何を行うかに関する詳細は、サイド バー「予定外のファイル アクセス」を参照してください。


図 3 ファイルの強制ダウンロードのダイアログ

この技法の利用時に注意すべき重要な測定基準は、ファイル ダウンロードのサイズです。 ファイルのサイズに対する制限を設ける必要があります。そうしないと、サイトがサービス妨害攻撃にさらされてしまいます。 リソースの許容量を超えるファイルのダウンロードを試みると、ページを表示できないことを知らせるランタイム エラーが起きるか、あるいは次のようなエラーが表示されます。

Server Application Unavailable (サーバー アプリケーションは使用できません)

The Web application you are attempting to access on this Web server is currently unavailable (アクセスしようとしているこの Web サーバー上の Web アプリケーションは現在使用できません。).
Please hit the "Refresh" button in your Web browser to retry your request (Web ブラウザの [更新] ボタンを押して、要求をやり直してください。).
Administrator Note: An error message detailing the cause of this specific request failure can be found inthe system event log of the Web server (管理者への注記 : この要求の失敗の原因を詳述したエラー メッセージが、Web サーバーのシステム イベント ログに記載されています。).
Please review this log entry to discover what caused this error to occur (そのログ エントリを調べて、このエラーが発生した原因を突き止めてください。).

ダウンロード可能なファイルの最大サイズは、サーバーのハードウェア構成および実行時状態の要因になります。 この問題に対処するには、support.microsoft.com/kb/823409に記載されている「サポート技術情報」の記事「[FIX] サイズの大きなファイルをダウンロードすると、大量のメモリが失われ、Aspnet_wp.exe プロセスが繰り返される」を参照してください。

このメソッドがその兆候を表すのは、ビデオなどの大きなファイルのダウンロード時であり、Windows 2000 および IIS 5.0 (あるいは、互換モードで稼働する IIS 6.0 を装備した Windows Server 2003) では特にそうです。 最小限のメモリを装備した構成の Web サーバー上では、この問題は一層大きくなります。なぜなら、クライアントにダウンロードできるようにするためには、事前にファイルをサーバー メモリ上にロードする必要があるからです。

IIS 5.0 が稼働する 2 GB の RAM を備えたサーバーであるテスト マシンから得た経験的な実績では、ファイル サイズが 200 MB に近づくとダウンロード障害が起きることが示されています。 実稼働環境では、並行実行中のユーザー数が増えるにつれて、サーバー メモリの制約が高まって、ユーザー側でダウンロード障害を起こします。この問題の解決策としては、さらに数行の明快なコード行が必要になります。

いくつかの部分に分割した巨大ファイルのダウンロード

上記のコード サンプルでのファイル サイズ問題の根源は、Response.WriteFile を 1 回呼び出しただけで、ソース ファイル全体がメモリ内のバッファに入れられることにあります。 大きなファイルの場合のもっと要領のよいアプローチとしては、図 4に例が示されているとおり、読み取ったファイルを複数の管理可能な小さいチャンク (かたまり) に分割してからクライアントに送信します。 現バージョンの Page_Load イベント ハンドラは、while ループを使用して一度に 10,000 バイトずつファイルを読み取ってから、それらのチャンクをブラウザに送信します。 このようにすれば、実行時にファイルの大部分がメモリ内にとどまったりするような事態は起きません。 現在、チャンク サイズは定数で設定されていますが、プログラマチックに修正したり、あるいは、構成ファイル内に移動して、サーバーの制約事項やパフォーマンス上の要件を満たすように変更することも可能です。 最大 1.6 GB までのファイルでこのコードをテストしましたが、ダウンロードは速やかに行われ、サーバー メモリの大量消費も起きませんでした。

IIS そのものは、サイズが 2 GB を超えるファイルのダウンロードをサポートしません。 それより大容量のダウンロードが必要になった場合、FTP、サード パーティのコントロール、Microsoft バックグラウンド インテリジェント転送サービス (BITS)、あるいは、ソケットを通してブラウザ ホストのカスタム コントロールへデータをストリーミングするようなカスタム ソリューションを使用する必要があります。

さらに気のきいた解決策

ファイルのダウンロード要求の広がり、および一般的に増える一方のファイル サイズが原因で、ASP.NET 開発チームは、ブラウザへの送信前にファイルをメモリにバッファリングしないでそのファイルをダウンロードするための特別なメソッドを ASP.NET に付け加えました。 それは、Response.TransmitFile というメソッドであり、ASP.NET 2.0 内に用意されています。

TransmitFile は、WriteFile と同じように使用できますが、通常は、パフォーマンス特性はこちらのほうが優れています。 TransmitFile はまた、さらに別の機能によって補強されています。 図 5のコードをご覧ください。ここでは、新たに追加された TransmitFile の追加機能を使用して、上述のメモリ使用上の問題が起きないようにしています。

ここの例では、コードにほんの数行付け加えるだけで、一部のセキュリティと耐障害機能を追加できました。 まず、セキュリティおよび論理的制約を追加しました。そのために、要求されたファイルのファイル拡張子を使用して MIME タイプを判別し、次のように、Response オブジェクトの ContentType プロパティの設定によって、要求された MIME タイプを HTTP ヘッダーに指定しました。

Response.ContentType = "application/x-zip-compressed"

これで、特定のコンテンツ タイプにのみダウンロードを限定し、それぞれ異なるファイル拡張子を 1 つのコンテンツ タイプにマップできるようになります。 また、Content-Disposition ヘッダーを追加するステートメントにも注目してください。 このステートメントを使用して、サーバーのハード ディスク上のオリジナルのファイル名とは別に、ダウンロードするファイルの名前を指定することができます。

このコードでは、オリジナルの名前にプレフィックスを付けることで、新規のファイル名を作成しています。 ここでのプレフィックスは静的であるのに対して、動的にプレフィックスを作成することもできます。そうすれば、ダウンロードしたファイルの名前が、ユーザーのハード ディスク上に既にあるファイルの名前と競合することはなくなります。

しかし、巨大ファイルを苦労しながら取り出している途中で、ダウンロードで障害が起きたらどうなるでしょうか。 これまで解説したコードは、単純なダウンロード リンクから長い道程をたどってきたものですが、障害を起こしたダウンロードをとどこおりなく処理して、既にサーバーからクライアントに一部が移動されたファイルのダウンロードを再開する方法をいまだに実現できていません。 これまでに試したどの解決策でも、障害が起きたときには、ユーザーはダウンロードを最初からもう一度やり直す必要があります。

失敗したダウンロードの再開

失敗したダウンロードの再開に関する問題に取り組むために、伝送しようとしているファイルを手動でチャンクに分割するアプローチに戻ってみましょう。TransmitFile メソッドを使用するコードのシンプルさには及ばないとしても、ファイルを複数のチャンクに分割して送受信するコードを手動で作成すると、それなりの利点があります。 どの時点でも、クライアントに既に送信済みのバイト数が実行時状態に組み入れられるので、それを合計のファイル サイズから差し引けば、あと何バイト送信すればファイルの転送が完了するかが分かります。

コードを見直すことによって、Response.IsClientConnected の結果が、読み取り/送信のループによってループ条件として検査されることがお分かりになります。 そのテストによって、クライアントがもう接続していなければ、伝送は必ず中断されることになります。 このテストが false となった (ファイルのダウンロードを開始した Web ブラウザの接続が切れた) 最初のループ反復処理では、サーバーはデータの送信を停止し、このファイル処理を完了するのに必要な残りのバイトを記録することができました。 しかも、前に失敗したダウンロードの完了をユーザーが試みた場合に備えて、クライアントで受信された部分的なファイルを保管することもできます。

再開可能なダウンロードという解決策の残りは、HTTP 1.1 プロトコル内のほとんど知られていない機能を使用して実施します。 通常、HTTP が持つステートレス特性は、Web 開発者の存在の障壁になりますが、この場合は、HTTP の仕様が大きな救いになります。 具体的に言うと、手元にあるタスクに関連した、Accept-Ranges および Etag という 2 つの HTTP 1.1 ヘッダー要素があります。

Accept-Ranges ヘッダー要素は、ただ単に、このプロセスでは再開可能なダウンロードがサポートされることをクライアント (この場合は Web ブラウザ) に伝えるだけです。 エンティティ タグ、つまり Etag 要素は、セッションの固有 ID を指定します。 したがって、再開可能なダウンロードを開始するために ASP.NET アプリケーションからブラウザに送信される HTTP ヘッダーは、次のようになります。

HTTP/1.1 200 OK
Connection: close
Date: Mon, 22 May 2006 11:09:13 GMT
Accept-Ranges: bytes
Last-Modified: Mon, 22 May 2006 08:09:13 GMT
ETag: "58afcc3dae87d52:3173"
Cache-Control: private
Content-Type: application/x-zip-compressed
Content-Length: 39551221

ETag および Accept-Headers を使用すれば、再開可能なダウンロードが Web サーバーでサポートされることをブラウザに知らせることができます。

ダウンロードが失敗した場合に、ファイルをもう一度要求すると、Internet Explorer から ETag およびファイル名が送信され、さらに、中断するまでにファイルのどの程度の部分の正常なダウンロードが完了したかを示す値範囲も送信されるので、Web サーバー (IIS) でダウンロードの再開を試みることができます。その 2 番目の要求は次のようになります。

GET http://192.168.0.1/download.zip HTTP/1.0
Range: bytes=933714-
Unless-Modified-Since: Sun, 26 Sep 2004 15:52:45 GMT
If-Range: "58afcc3dae87d52:3173"

If-Range 要素には、オリジナルの Etag 値が入っていることに注意してください。サーバーはこれを使用して、再送信されるファイルを識別することができます。さらに、Unless-Modified-Since 要素には、オリジナルのダウンロードが開始された日時が入っていることにも注意してください。サーバーはそれを使用して、オリジナルのダウンロードの開始時点以降にファイルが修正されたかどうかを判別します。修正されていた場合、サーバーは、ダウンロードを最初からやり直します。

やはりヘッダー内にある Range 要素は、ファイル処理を完了するのに必要なバイト数をサーバーに知らせます。サーバーはそれを使用して、部分的にダウンロード済みのファイル内のどこから処理を再開すればよいかを判別することができます。

ブラウザが異なれば、このようなヘッダーを使用する方法も少々異なります。ファイルを個々に識別するためにクライアントから送信されるその他の HTTP ヘッダーには、If-Match、If-Unmodified-Since、および Unless-Modified-Since があります。HTTP 1.1 には、どのヘッダーがクライアントでサポートされる必要があるかに関する具体的な指定はありません。したがって、Web ブラウザしだいで、これらの HTTP ヘッダーはいずれもサポートされない場合もあれば、Internet Explorer での規定のものとは異なるヘッダーが使用される場合もあり得ます。

既定では、次のようなヘッダーのセットが IIS に組み込まれます。

HTTP/1.1 206 Partial Content
Content-Range: bytes 933714-39551221/39551222
Accept-Ranges: bytes
Last-Modified: Sun, 26 Sep 2004 15:52:45 GMT
ETag: "58afcc3dae87d52:3173"
Cache-Control: private
Content-Type: application/x-zip-compressed
Content-Length: 2021408

このヘッダー セットには、オリジナルの要求のものとは異なる応答コードが組み込まれています。 もともと応答には 200 のコードが組み込まれていましたが、今回の要求では、206 の応答コード、つまり「ダウンロードの再開」が使用されています。それによって、後続のデータは完全なファイルではなく、ETag でファイル名を確認できる既に開始済みのダウンロードの続きであることがクライアントに知らされます。

一部の Web ブラウザはファイル名そのものに頼り切りますが、Internet Explorer の場合は、Etag ヘッダーが特に必要になります。 ETag ヘッダーが初期ダウンロード応答あるいは再開されるダウンロード内にない場合、Internet Explorer はダウンロードの再開を試みずに、新たにダウンロードを開始するだけです。

再開可能なダウンロード機能を ASP.NET ダウンロード アプリケーションで実装するためには、ブラウザからの要求 (ダウンロードの再開を求める) を先に取得し、要求中の HTTP ヘッダーを使用して該当する応答を ASP.NET コードで作成する必要があります。 この処置を行うには、通常の処理シーケンスよりも若干早い段階で要求をキャッチする必要があります。

幸運にも、.NET Framework が救いの手を差し伸べます。 これは、.NET のデザインの基本前提の好例です。すなわち、開発者が日々実行する必要のある標準的なプラミング (配管作業) の大部分の機能を、十分に分割されたオブジェクト ライブラリとして提供するものです。

この場合は、.NET Framework 内の System.Web 名前空間に用意されている IHttpHandler インターフェイスの利点を活用して、ご自分独自のカスタム HTTP ハンドラを構築することができます。IHttpHandler を実装する独自のクラスを作成すれば、ファイル タイプを指定して Web 要求を先に取得することができます。それによって、IIS がその既定の動作に従って応答するのに任せるのではなく、独自のコードでその要求に応答することができます。

この記事のダウンロード用コードには、再開可能なダウンロードをサポートする HTTP ハンドラの作業用実装が付属しています。 この機能に対しては大量のコードがあり、その実装には HTTP メカニズムのある程度の理解が必要ですが、.NET Framework によって、その実装は比較的簡単になります。 このソリューションでは、非常に大きなファイルをダウンロードする機能が提供され、ダウンロードを開始した後も参照を続行することができます。 ただし、制御の範囲を超えたインフラストラクチャ上の考慮事項があることも確かです。

たとえば、多くの企業およびインターネット サービス プロバイダは、独自のキャッシング メカニズムを保持しています。 Web キャッシュ サーバーの故障や誤った構成は、大容量のダウンロードが失敗する原因になることがあります。それは、ファイルが破損したり、セッションが完了していないのに終了したりするからです。ファイル サイズが 255 MB を超える場合は特にそうです。

255 MB を超えるファイルのダウンロードや他のカスタム関数が必要になった場合、カスタムあるいはサード パーティ製のダウンロード マネージャの使用を検討してみてください。 たとえば、カスタム ブラウザ コントロールあるいはブラウザ ヘルパ関数を作成して、ダウンロード内容を管理するか、それを BITS に引き渡すか、あるいはファイル要求そのものをカスタム コードで FTP クライアントに引き渡すことさえできます。 オプションは無限にあり、個々のニーズにあわせて調整する必要があります。

2 行のコードでの大きなファイルのダウンロードから、カスタム セキュリティを使用したセグメント別の再開可能なダウンロードにいたるまで、Web サイトのエンド ユーザーに最も適したダウンロード操作を構築するための充実したオプションが .NET Framework および ASP.NET に用意されています。


Joe Stagner は、2001 年に技術エバンジェリストとしてマイクロソフト社に入社し、現在はツールおよびプラットフォーム製品グループの開発者コミュニティのプログラム マネージャを務めています。 同氏の開発実績は 30 年にわたり、それを支えとして多種多様なテクニカル プラットフォームを対象に、市販用ソフトウェア アプリケーションの作成に携わってきました。


 この記事は、 MSDN マガジン - 2006 年 9 月号からの翻訳です。 .


Back to top