3/20/2006

MS makes things simple but with limitations

Filed under: General — russell @ 6:38 pm

I have been working on a project that requires files to be uploaded to a Web server. No big deal... ordinarily. However this project is .Net. As a result the most obvious and simple implementation requires the entire contents of the file to be moved into memory. This is due to the architecture of IIS. IIS 6.0 deals with this a little better when run in isolation mode. However, this also has implication.

I found some good writings on this topic. It would seem that MS dealt with the download issue, but not upload. In any case the following presents a good overview of the issue. The upload issue is a tougher one to deal with. All of the implemented solutions I found use an ISAPI filter or something at that level in the architecture. Any way here are the links:

A nice overview of the problem (from the download perspective) here:
http://www.objectsharp.com/Blogs/bruce/articles/1571.aspx

A good discussion of the upload issues (with solutions) here:
http://forums.asp.net/1/55127/ShowPost.aspx#55127

MS Solutions to the problem:
http://support.microsoft.com/kb/823409/EN-US/ (Download)
http://support.microsoft.com/kb/295626/EN-US/ (Upload)

I snipped a bit of VB code from coderage at the asp.net forum available after the jump

One thing to consider is that the headers and view state come over with the file. You need to turn off any runat=server controls. Play with a text file to get some sense of how to strip headers.

Code:
Ok, here's the html file tha has the form: 
	
<html>
<head><title>UploadForm</title></head>
<body>
<form name="UploadForm" method="post" encoding="multipart/form-data">
File to Upload:&nbsp;<input type="file" name="file"/><br /><br />
<input type="submit" value="Submit"/>
</form>
</body>
</html> 
	
Ok, now add the following to your Web.config file inside the <system .web> element: 
	
<httpruntime executionTimeout="1800" maxRequestLength="524288000" /> 
	
<httpmodules>
<add name="UploadModule" type="HttpUploadApp.UploadModule,HttpUploadApp" />
</httpmodules> 
	
This configures the timeout to 30 minutes, accepts up to 500MB files, and registers the HttpModule. My assembly name is HttpUploadApp as the namespace is the same thing. 
	
Next is the HttpModule class. This is a rough example that writes every HTTP request that has a content body to a file whose name is a GUID with a ".txt" extension saved in a directory named "C:\Upload\". This was just for test purposes!!!
HttpModule class (VB.NET): 
	
Imports System
Imports System.IO
Imports System.Web 
	
Public Class UploadModule 
	
Implements IHttpModule 
	
Public Sub New()
MyBase.New()
End Sub 
	
Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
End Sub 
	
Public Sub Init(ByVal context As System.Web.HttpApplication) Implements System.Web.IHttpModule.Init
AddHandler context.BeginRequest, AddressOf BeginRequest
End Sub 
	
Private Sub BeginRequest(ByVal source As Object, ByVal e As EventArgs) 
	
Dim app As HttpApplication = CType(source, HttpApplication)
Dim context As HttpContext = app.Context 
	
' Get the HttpWorkerRequest Object!!!
Dim hwr As HttpWorkerRequest = CType(context.GetType.GetProperty("WorkerRequest", Reflection.BindingFlags.Instance Or Reflection.BindingFlags.NonPublic).GetValue(context, Nothing), HttpWorkerRequest) 
	
If hwr.HasEntityBody Then 
	
Dim g As Guid = Guid.NewGuid()
Dim fn As String = "C:\Upload\" + g.ToString + ".txt" 
	
Dim fs As New FileStream(fn, FileMode.CreateNew, FileAccess.Write, FileShare.Read)
Try 
	
Const BUFFSIZE As Integer = 65536 
	
Dim Buffer As Byte()
Dim Received As Integer = 0
Dim TotalReceived As Integer = 0
Dim ContentLength As Integer = CType(hwr.GetKnownRequestHeader(HttpWorkerRequest.HeaderContentLength), Integer) 
	
Buffer = hwr.GetPreloadedEntityBody()
fs.Write(Buffer, 0, Buffer.Length)
Received = Buffer.Length
TotalReceived += Received 
	
If Not hwr.IsEntireEntityBodyIsPreloaded Then 
	
While (ContentLength - TotalReceived) >= Received
Buffer = New Byte(BUFFSIZE) {}
Received = hwr.ReadEntityBody(Buffer, BUFFSIZE)
fs.Write(Buffer, 0, Received)
TotalReceived += Received
End While 
	
Received = hwr.ReadEntityBody(Buffer, (ContentLength - TotalReceived))
fs.Write(Buffer, 0, Received)
TotalReceived += Received 
	
End If 
	
fs.Flush() 
	
Catch ex As Exception 
	
Finally
fs.Close()
End Try 
	
context.Response.Redirect("UploadForm.aspx") 
	
End If 
	
End Sub 
	
End Class 
	
Ok, that's it. What I didn't address here was keeping th aspnet_wp.exe process from recyling itself by opening another client-side browser winfow that loads an .aspx page with a client-side (JavaScript) timer refreshing itself every 10 seconds or so. Anyone can write that code --- it's so simple!!! 
	
CodeRage! 
	

And now a C# version (again credited to a forum poster BjornB.)

Code:
using System;
using System.Web;
using System.IO;
using System.Reflection; 
	
namespace My.PlayGround
{
/// <summary>
/// Summary description for UploadHandler.
/// </summary>
public class UploadHandler : IHttpModule
{
public UploadHandler()
{
} 
	
public void Init(HttpApplication application)
{
// Register our event handler with with the application object
application.BeginRequest += new EventHandler(this.BeginRequest);
} 
	
public void Dispose()
{} 
	
public void BeginRequest(object sender, EventArgs args)
{
try
{
// Create an instance of th application object
HttpApplication application = (HttpApplication) sender;
// Create an instance of the HTTP worker request
HttpWorkerRequest request = (HttpWorkerRequest) application.Context.GetType().GetProperty("WorkerRequest", (BindingFlags)36).GetValue(application.Context, null); 
	
// Only trigger if the request is of type 'multipart/form-data'
if(application.Context.Request.ContentType.IndexOf("multipart/form-data") > -1)
{
// Create a new unique identifier to identify each request
string guid = Guid.NewGuid().ToString();
// Please alter pathname because the root of the C drive is not really the place to put your tempfiles
string filename = "c:\\requests\\request_" + guid + ".txt"; 
	
// Check if a request body is sent by the browser
if(request.HasEntityBody())
{
// Get the content length of the request
int content_length = Convert.ToInt32(request.GetKnownRequestHeader(HttpWorkerRequest.HeaderContentLength));
int content_received = 0;
// This is a nice feature to redirect users if they upload a file
// larger then 100Mb BEFORE it is being uploaded
if(content_length > 102400000)
{
application.Context.Response.Redirect("http://www.av.com");
} 
	
// Create a file to store the stream
FileStream newFile = new FileStream(filename, FileMode.Create); 
	
// Get the preloaded buffered data
byte[] body = request.GetPreloadedEntityBody();
content_received += body.Length; 
	
// Write the preloaded data to new file
newFile.Write(body, 0, body.Length); 
	
// Get all the other data to be written to file if available
if(!request.IsEntireEntityBodyIsPreloaded())
{
// Create an input buffer to store the incomming data
byte[] a_buffer = new byte[16384];
int bytes_read = 16384; 
	
while((content_length - content_received) >= bytes_read)
{
bytes_read = request.ReadEntityBody(a_buffer, a_buffer.Length);
content_received += bytes_read;
newFile.Write(a_buffer, 0, bytes_read);
} 
	
// Read the last part of the stream
bytes_read = request.ReadEntityBody(a_buffer, (content_length - content_received));
newFile.Write(a_buffer, 0, bytes_read);
content_received += bytes_read;
} 
	
// Flush all data to the file
newFile.Flush(); 
	
// Close the file
newFile.Close(); 
	
// Redirect to the page to avoid the browser hanging
string current_page = application.Context.Request.CurrentExecutionFilePath;
current_page = current_page.Substring(current_page.LastIndexOf("/")+1) + "?" + application.Context.Request.QueryString;
application.Context.Response.Redirect(current_page);
}
}
}
catch(System.Threading.ThreadAbortException)
{}
catch(Exception exception)
{
object e = exception;
}
}
}
}