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( _
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( _
      ' 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
      ' 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 += _
            iResponseContentLength += _
               iResponseContentLength += _
               alRequestedRangesBegin( _
            iResponseContentLength += _
               alRequestedRangesend( _
            iResponseContentLength += _
            ' 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 += _
         ' 8 is the length of dash and line break 
         ' characters
         iResponseContentLength += 8
         ' 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 & "/" & _
      End If
      ' Range response 
      objResponse.StatusCode = 206 ' Partial Response
      ' 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 = _
      ' 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( _
   ' Write the Last-Modified Date into the Response
   objResponse.AppendHeader( _
   ' Tell the client software that we accept 
   ' Range requests
   objResponse.AppendHeader( _
   ' 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
      ' 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).

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