HttpRequestオブジェクト
通常、アップロードされたファイルのデータを取得するにはHttpRequestオブジェクトのFilesプロパティから取得します。
var stm = Request.Files[0].InputStream;
InputStreamプロパティにアクセスをすると、リクエストのBody部分のデータが全てメモリ上に読み込まれます。
500MBのファイルをアップロードしている場合は500MBのデータがIISのメモリ上に読み込まれることになります。
結果としてASP.NETやIISのメモリ上限などの制限に引っ掛かり例外が発生しアップロードができないということになってしまいます。
ファイルのアップロードした後の処理としてはファイルをそのままDBもしくはファイルシステムに保存することが多いと思われます。
本来であればこのような巨大なデータをきちんとStreamとして扱うことができれば問題は発生しないはずです。
アップロードされたデータを1MBずつ読み取り→別のストレージへ転送という形での実装ができればIISのメモリは常に1MBずつ使用されることになります。
この方法であればASP.NETやIISで例外が発生することはなく正常にファイルを保存することが可能になります。
しかしながら内部実装が全てのデータをメモリに展開するような実装になっているためStreamとして処理をさせることができません。
Request.Files[0].InputStreamではなくRequest.InputStreamなどの別のプロパティを使用した場合でも同様の動作をするのでStreamで読み取ることはできません。
HttpWorkerRequestオブジェクト
上記の問題はHttpWorkerRequestオブジェクトを使用することで解決可能です。HttpWorkerRequestオブジェクトは通常のプロパティ経由ではアクセスできません。
HttpContextオブジェクトから以下のような形で取得します。
HttpWorkerRequest workerRequest = ((IServiceProvider)context).GetService(typeof(HttpWorkerRequest)) as HttpWorkerRequest;
拡張メソッドを作成してみました。以下のようになります。
public static class HttpContextExtensions
{
public static HttpWorkerRequest GetHttpWorkerRequest(this HttpContext context)
{
return ((IServiceProvider)context).GetService(typeof(HttpWorkerRequest)) as HttpWorkerRequest;
}
}
これによってHttpWorkerRequestオブジェクトを以下のような形で簡単に取得できます。
var req = HttpContext.Current.GetHttpWorkerRequest();
HttpWorkerRequestクラスにはGetPreloadedEntityBodyメソッドとReadEntityBodyメソッドがあります。
これらのメソッドを使用することでデータをStreamとして読み出すことが可能です。
GetPreloadedEntityBodyメソッドを使用すると既に読み取られているデータを取得できます。
byte[] data = workerRequest.GetPreloadedEntityBody();
まだ読み取られていないデータはReadEntityBodyメソッドで取得できます。
var bufferSize = 10240;
var data = new byte[bufferSize];
var bytesRead = workerRequest.ReadEntityBody(data, bufferSize);
このHttpWorkerRequestを使用して巨大なファイルのアップロードを可能にするIHttpModuleの実装が以下になります。
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.IO;
using System.Reflection;
public class LargeFileUploadHttpModule : IHttpModule
{
public LargeFileUploadHttpModule()
{
}
public void Dispose()
{
}
public void Init(HttpApplication context)
{
context.BeginRequest += new EventHandler(context_BeginRequest);
}
void context_BeginRequest(object sender, EventArgs e)
{
HttpApplication app = sender as HttpApplication;
HttpContext context = app.Context;
HttpWorkerRequest workerRequest = context.GetHttpWorkerRequest();
if (context.Request.ContentType.IndexOf("multipart/form-data") == -1)
{
// Not multi-part form
return;
}
string fileName = context.Server.MapPath("~") + "\\temp\\aaa.dat";
if (workerRequest.HasEntityBody())
{
int length = int.Parse(workerRequest.GetKnownRequestHeader(HttpWorkerRequest.HeaderContentLength));
if (length < 10000)// if the length is too small
return;
if (length > 1000000000)// if the legth is too large
{
context.Response.Redirect("url of error page");
return;
}
FileStream fs = new FileStream(fileName, FileMode.Create);
byte[] data = workerRequest.GetPreloadedEntityBody();
fs.Write(data, 0, data.Length);
int bufferSize = 10240;
int bytesRead = 0;
int totalSize = 0;
totalSize += data.Length;
data = new byte[bufferSize];
while (totalSize < length)//!workerRequest.IsEntireEntityBodyIsPreloaded())
{
bytesRead = workerRequest.ReadEntityBody(data, bufferSize);
fs.Write(data, 0, bytesRead);
totalSize += bytesRead;
}
fs.Close();
context.Response.Redirect(context.Request.RawUrl);
}
}
}
引用元:ASP.NETフォーラムより→
huge file upload/post need direct access to post stream as it comes in