Login | Register   
RSS Feed
Download our iPhone app
Browse DevX
Sign up for e-mail newsletters from DevX


Tracking and Resuming Large File Downloads in ASP.NET : Page 3

It's notoriously difficult to deal with large file downloads in Web applications, so for most sites, woe betide users if their download gets interrupted. But it doesn't have to be that way; you can make your ASP.NET applications capable of serving resumable large-file downloads. While you're at it, you can track the download progress so you can handle dynamically created files—and you can get all this without old-fashioned ISAPI DLLs and without unmanaged C++ code.

The HttpHandler Class: ZIPHandler
After mapping the .zip extension through to ASP.NET, IIS calls the ZipHandler class's ProcessRequest method (see Listing 1) each time a client requests a .zip file from the server.

The ProcessRequest method first creates an instance of a custom FileInformation class (see Listing 2), which encapsulates the download state (e.g. in-progress, broken, etc.). The sample code hard-codes the path to a sample file named download.zip. If you move the code to your own application, change it to open the requested file instead.

' ToDo - your code here ' Using objRequest, determine which file has been ' requested and open objFile with that file: ' Example: ' objFile = New Download.FileInformation ' (<Full path to file>) objFile = New Download.FileInformation( _ objContext.Server.MapPath("~/download.zip"))

Then, the procedure does a series of validation checks using the described HTTP headers (if the request provides them). It encapsulates each validation in a small private function, which returns True if the validation succeeds. If any validation check fails, the response terminates immediately, sending an appropriate StatusCode value.

If Not objRequest.HttpMethod.Equals( _ HTTP_METHOD_GET) Or Not objRequest.HttpMethod.Equals( _ HTTP_METHOD_HEAD) Then ' Currently, only the GET and HEAD methods ' are supported... objResponse.StatusCode = 501 ' Not implemented ElseIf Not objFile.Exists Then ' The requested file could not be retrieved... objResponse.StatusCode = 404 ' Not found ElseIf objFile.Length > Int32.MaxValue Then ' The file size is too large... objResponse.StatusCode = 413 ' Request Entity ' Too Large ElseIf Not ParseRequestHeaderRange(objRequest, _ alRequestedRangesBegin, alRequestedRangesend, _ objFile.Length, bIsRangeRequest) Then ' The Range request contained bad entries objResponse.StatusCode = 400 ' Bad Request ElseIf Not CheckIfModifiedSince(objRequest, _ objFile) Then ' The entity is still unmodified... objResponse.StatusCode = 304 ' Not Modified ElseIf Not CheckIfUnmodifiedSince(objRequest, _ objFile) Then ' The entity was modified since the requested ' date... objResponse.StatusCode = 412 ' Precondition failed ElseIf Not CheckIfMatch(objRequest, objFile) Then ' The entity does not match the request... objResponse.StatusCode = 412 ' Precondition failed ElseIf Not CheckIfNoneMatch(objRequest, objResponse, _ objFile) Then ' The entity does match the none-match request, ' the response code was set inside the ' CheckIfNoneMatch function Else ' Preliminary checks were successful...

One of these preliminary functions, ParseRequestHeaderRange (see Listing 3), checks to see if a client requested a file range, and thus a partial download. The method sets bIsRangeRequest to True, if the requested range is valid (invalid ranges are those which exceed the file's size, or contain illogical numbers). If a range was requested, the CheckIfRange method validates the IfRange header.

If the requested range is valid, the code calculates the response size. If the client requested multiple ranges, the response size value contains multipart header length values.

If a sent header value could not be confirmed, the procedure handles this download request not as a partial download, but instead restarts, sending a new download stream from the top of the file.

If bIsRangeRequest AndAlso _ CheckIfRange(objRequest, objFile) Then ' This is a Range request... ' If the Range arrays contain more than one entry, ' it even is a multipart range request... bMultipart = CBool( _ alRequestedRangesBegin.GetUpperBound(0) > 0) ' Go through each Range to get the entire Response ' length For iLoop = _ alRequestedRangesBegin.GetLowerBound(0) _ To alRequestedRangesBegin.GetUpperBound(0) ' The length of the content (for this range) iResponseContentLength += _ Convert.ToInt32(alRequestedRangesend( _ iLoop) - alRequestedRangesBegin(iLoop)) + 1 If bMultipart Then ' If this is a multipart range request, ' calculate the length of the intermediate ' headers to send iResponseContentLength += _ MULTIPART_BOUNDARY.Length iResponseContentLength += _ objFile.ContentType.Length iResponseContentLength += _ alRequestedRangesBegin( _ iLoop).ToString.Length iResponseContentLength += _ alRequestedRangesend( _ iLoop).ToString.Length iResponseContentLength += _ objFile.Length.ToString.Length ' 49 is the length of line break and other ' needed characters in one multipart header iResponseContentLength += 49 End If Next iLoop If bMultipart Then ' If this is a multipart range request, ' we must also calculate the length of ' the last intermediate header we must send iResponseContentLength += _ MULTIPART_BOUNDARY.Length ' 8 is the length of dash and line break ' characters iResponseContentLength += 8 Else ' This is no multipart range request, so ' we must indicate the response Range of ' in the initial HTTP Header objResponse.AppendHeader( _ HTTP_HEADER_CONTENT_RANGE, "bytes " & _ alRequestedRangesBegin(0).ToString & "-" & _ alRequestedRangesend(0).ToString & "/" & _ objFile.Length.ToString) End If ' Range response objResponse.StatusCode = 206 ' Partial Response Else ' This is not a Range request, or the requested ' Range entity ID does not match the current entity ' ID, so start a new download ' Indicate the file's complete size as content ' length iResponseContentLength = _ Convert.ToInt32(objFile.Length) ' Return a normal OK status... objResponse.StatusCode = 200 End If

Next the server must send a few important response headers, such as the content length, the ETag and the file's content type.

' Write the content length into the Response objResponse.AppendHeader( _ HTTP_HEADER_CONTENT_LENGTH, _ iResponseContentLength.ToString) ' Write the Last-Modified Date into the Response objResponse.AppendHeader( _ HTTP_HEADER_LAST_MODIFIED, _ objFile.LastWriteTimeUTC.ToString("r")) ' Tell the client software that we accept ' Range requests objResponse.AppendHeader( _ HTTP_HEADER_ACCEPT_RANGES, _ HTTP_HEADER_ACCEPT_RANGES_BYTES) ' Write the file's Entity Tag into the Response ' (in quotes!) objResponse.AppendHeader(HTTP_HEADER_ENTITY_TAG, _ """" & objFile.EntityTag & """") ' Write the Content Type into the Response If bMultipart Then ' Multipart messages have this special Type. ' In this case, the file's actual mime type is ' written into the Response at a later time... objResponse.ContentType = MULTIPART_CONTENTTYPE Else ' Single part messages have the files content ' type... objResponse.ContentType = objFile.ContentType End If

Everything is now prepared to begin downloading the file. You use a FileStream object to read byte chunks from the file. Set the State property of the FileInformation instance objFile to fsDownloadInProgress. As long as the client stays connected, the server reads chunks from the file and sends them to the client. The code sends special headers for multipart responses. Should the client break the connection, the server sets the file state to fsDownloadBroken. If the server completes sending the requested range or ranges, it sets the state to fsDownloadFinished (see Listing 4).

Comment and Contribute






(Maximum characters: 1200). You have 1200 characters left.



Thanks for your registration, follow us on our social networks to keep up-to-date