Hi,
I have a problem that I have been working with for a while.
I need to be able from server side (asp.net) to detect that the file i'm
streaming down to the client is saved completely/succsessfully on the
client's computer before updating some metadata on the server (file
downloaded date for instance)
However,
All examples i have tried, and all examples I have found that other people
says works - doesn't work for me :-(
Below is the code I'm using at the moment (an http handler for handling all
zip files)
However, the code seems to output the whole file to the response stream
before the user decides to press the saveas or cancel button in the forced
save as dialog in internet explorer.
By then when the user press the save or cancel button (or the x in the upper
right corner of the save as dialog) the server side has already finished the
output and the client is still connected wich results in an update of the
download date- wich is wrong since the user hasn't saved the complete file to
disc - the whole file has only been outstreamed from server.)
It doesn't help to set the bufferoutput=fa lse.
arrgh :-(
Do I need to write an activex on client side to handle the download and to
be able to verify that the download is completed? I don't want to mess the
app up with activex and/or java applets - but maybe I can't solve it without
some more logic on the client side?
Please give me a hint on how to solve this.
Regards,
Roy
*********CODE** ******
Public Class ZIPHandler
Implements IHttpHandler
#Region "Constants used for HTTP communication"
' The boundary is used in multipart/byteranges responses
' to separate each ranges content. It should be as unique
' as possible to avoid confusion with binary content.
Private Const MULTIPART_BOUND ARY As String = "<q1w2e3r4t5y6u 7i8o9p0>"
Private Const MULTIPART_CONTE NTTYPE As String = "multipart/byteranges;
boundary=" & MULTIPART_BOUND ARY
Private Const HTTP_HEADER_ACC EPT_RANGES As String = "Accept-Ranges"
Private Const HTTP_HEADER_ACC EPT_RANGES_BYTE S As String = "bytes"
Private Const HTTP_HEADER_CON TENT_TYPE As String = "Content-Type"
Private Const HTTP_HEADER_CON TENT_RANGE As String = "Content-Range"
Private Const HTTP_HEADER_CON TENT_LENGTH As String = "Content-Length"
Private Const HTTP_HEADER_ENT ITY_TAG As String = "ETag"
Private Const HTTP_HEADER_LAS T_MODIFIED As String = "Last-Modified"
Private Const HTTP_HEADER_RAN GE As String = "Range"
Private Const HTTP_HEADER_IF_ RANGE As String = "If-Range"
Private Const HTTP_HEADER_IF_ MATCH As String = "If-Match"
Private Const HTTP_HEADER_IF_ NONE_MATCH As String = "If-None-Match"
Private Const HTTP_HEADER_IF_ MODIFIED_SINCE As String = "If-Modified-Since"
Private Const HTTP_HEADER_IF_ UNMODIFIED_SINC E As String =
"If-Unmodified-Since"
Private Const HTTP_HEADER_UNL ESS_MODIFIED_SI NCE As String =
"Unless-Modified-Since"
Private Const HTTP_METHOD_GET As String = "GET"
Private Const HTTP_METHOD_HEA D As String = "HEAD"
#End Region
#Region "IHTTPHandl er"
Public ReadOnly Property IsReusable() As Boolean Implements
System.Web.IHtt pHandler.IsReus able
Get
' Allow ASP.NET to reuse instances of this class...
Return True
End Get
End Property
Public Sub ProcessRequest( ByVal objContext As System.Web.Http Context)
Implements System.Web.IHtt pHandler.Proces sRequest
' The Response object from the Context
Dim objResponse As HttpResponse = objContext.Resp onse
' The Request object from the Context
Dim objRequest As HttpRequest = objContext.Requ est
' File information object...
Dim objFile As Download.FileIn formation
' Long Arrays for Range values:
' ...Begin() contains start positions for each requested Range
Dim alRequestedRang esBegin() As Long
' ...End() contains end positions for each requested Range
Dim alRequestedRang esend() As Long
' Response Header value: Content Length...
Dim iResponseConten tLength As Int32
' The Stream we're using to download the file in chunks...
Dim objStream As System.IO.FileS tream
' Total Bytes to read (per requested range)
Dim iBytesToRead As Int32
' Size of the Buffer for chunk-wise reading
Dim iBufferSize As Int32 = 25000
' The Buffer itself
Dim bBuffer(iBuffer Size) As Byte
' Amount of Bytes read
Dim iLengthOfReadCh unk As Int32
' Indicates if the download was interrupted
Dim bDownloadBroken As Boolean
' Indicates if this is a range request
Dim bIsRangeRequest As Boolean
' Indicates if this is a multipart range request
Dim bMultipart As Boolean
' Loop counter used to iterate through the ranges
Dim iLoop As Int32
' ToDo - your code here (Determine which file is requested)
' Using objRequest, determine which file is requested to
' be downloaded, and open objFile with that file:
' Example:
' objFile = New Download.FileIn formation(<Full path to the requested
file>)
objFile = New
Download.FileIn formation(objCo ntext.Server.Ma pPath("~/download.zip"))
' Clear the current output content from the buffer
objResponse.Cle ar()
If Not objRequest.Http Method.Equals(H TTP_METHOD_GET) Or _
Not objRequest.Http Method.Equals(H TTP_METHOD_HEAD ) Then
' Currently, only the GET and HEAD methods
' are supported...
objResponse.Sta tusCode = 501 ' Not implemented
ElseIf Not objFile.Exists Then
' The requested file could not be retrieved...
objResponse.Sta tusCode = 404 ' Not found
ElseIf objFile.Length > Int32.MaxValue Then
' The file size is too large...
objResponse.Sta tusCode = 413 ' Request Entity Too Large
ElseIf Not ParseRequestHea derRange(objReq uest, alRequestedRang esBegin,
alRequestedRang esend, _
objFile.Length, bIsRangeRequest ) Then
' The Range request contained bad entries
objResponse.Sta tusCode = 400 ' Bad Request
ElseIf Not CheckIfModified Since(objReques t, objFile) Then
' The entity is still unmodified...
objResponse.Sta tusCode = 304 ' Not Modified
ElseIf Not CheckIfUnmodifi edSince(objRequ est, objFile) Then
' The entity was modified since the requested date...
objResponse.Sta tusCode = 412 ' Precondition failed
ElseIf Not CheckIfMatch(ob jRequest, objFile) Then
' The entity does not match the request...
objResponse.Sta tusCode = 412 ' Precondition failed
ElseIf Not CheckIfNoneMatc h(objRequest, objResponse, objFile) Then
' The entity does match the none-match request, the response
' code was set inside the CheckIfNoneMatc h function
Else
' Preliminary checks where successful...
If bIsRangeRequest AndAlso CheckIfRange(ob jRequest, 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(alRequest edRangesBegin.G etUpperBound(0) > 0)
' Loop through each Range to calculate the entire Response length
For iLoop = alRequestedRang esBegin.GetLowe rBound(0) To
alRequestedRang esBegin.GetUppe rBound(0)
' The length of the content (for this range)
iResponseConten tLength +=
Convert.ToInt32 (alRequestedRan gesend(iLoop) - alRequestedRang esBegin(iLoop))
+ 1
If bMultipart Then
' If this is a multipart range request, calculate
' the length of the intermediate headers to send
iResponseConten tLength += MULTIPART_BOUND ARY.Length
iResponseConten tLength += objFile.Content Type.Length
iResponseConten tLength +=
alRequestedRang esBegin(iLoop). ToString.Length
iResponseConten tLength +=
alRequestedRang esend(iLoop).To String.Length
iResponseConten tLength += objFile.Length. ToString.Length
' 49 is the length of line break and other
' needed characters in one multipart header
iResponseConten tLength += 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
iResponseConten tLength += MULTIPART_BOUND ARY.Length
' 8 is the length of dash and line break characters
iResponseConten tLength += 8
Else
' This is no multipart range request, so
' we must indicate the response Range of
' in the initial HTTP Header
objResponse.App endHeader(HTTP_ HEADER_CONTENT_ RANGE, "bytes " & _
alRequestedRang esBegin(0).ToSt ring & "-"
& _
alRequestedRang esend(0).ToStri ng & "/" & _
objFile.Length. ToString)
End If
' Range response
objResponse.Sta tusCode = 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
iResponseConten tLength = Convert.ToInt32 (objFile.Length )
' Return a normal OK status...
objResponse.Sta tusCode = 200
End If
' Write the content length into the Response
objResponse.App endHeader(HTTP_ HEADER_CONTENT_ LENGTH,
iResponseConten tLength.ToStrin g)
' Write the Last-Modified Date into the Response
objResponse.App endHeader(HTTP_ HEADER_LAST_MOD IFIED,
objFile.LastWri teTimeUTC.ToStr ing("r"))
' Tell the client software that we accept Range request
objResponse.App endHeader(HTTP_ HEADER_ACCEPT_R ANGES,
HTTP_HEADER_ACC EPT_RANGES_BYTE S)
' Write the file's Entity Tag into the Response (in quotes!)
objResponse.App endHeader(HTTP_ HEADER_ENTITY_T AG, """" &
objFile.EntityT ag & """")
' 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.Con tentType = MULTIPART_CONTE NTTYPE
Else
' Single part messages have the files content type...
objResponse.Con tentType = objFile.Content Type
End If
If objRequest.Http Method.Equals(H TTP_METHOD_HEAD ) Then
' Only the HEAD was requested, so we can quit the Response right
here...
Else
' Flush the HEAD information to the client...
objResponse.Flu sh()
' Download is in progress...
objFile.State = FileInformation .DownloadState. fsDownloadInPro gress
' Open the file as filestream
objStream = New System.IO.FileS tream(objFile.F ullName,
IO.FileMode.Ope n, _
IO.FileAccess.R ead, _
IO.FileShare.Re ad)
' Now, for each requested range, stream the chunks to the client:
For iLoop = alRequestedRang esBegin.GetLowe rBound(0) To
alRequestedRang esBegin.GetUppe rBound(0)
' Move the stream to the desired start position...
objStream.Seek( alRequestedRang esBegin(iLoop), IO.SeekOrigin.B egin)
' Calculate the total amount of bytes for this range
iBytesToRead = Convert.ToInt32 (alRequestedRan gesend(iLoop) -
alRequestedRang esBegin(iLoop)) + 1
If bMultipart Then
' If this is a multipart response, we must add
' certain headers before streaming the content:
' The multipart boundary
objResponse.Out put.WriteLine("--" & MULTIPART_BOUND ARY)
' The mime type of this part of the content
objResponse.Out put.WriteLine(H TTP_HEADER_CONT ENT_TYPE & ": " &
objFile.Content Type)
' The actual range
objResponse.Out put.WriteLine(H TTP_HEADER_CONT ENT_RANGE & ":
bytes " & _
alRequestedRang esBegin(iLoop). ToString & "-" & _
alRequestedRang esend(iLoop).To String & "/" & _
objFile.Length. ToString)
' Indicating the end of the intermediate headers
objResponse.Out put.WriteLine()
End If
' Now stream the range to the client...
While iBytesToRead > 0
'THISLOOP
If objResponse.IsC lientConnected Then
' Read a chunk of bytes from the stream
iLengthOfReadCh unk = objStream.Read( bBuffer, 0,
Math.Min(bBuffe r.Length, iBytesToRead))
' Write the data to the current output stream.
objResponse.Out putStream.Write (bBuffer, 0, iLengthOfReadCh unk)
' Flush the data to the HTML output.
objResponse.Flu sh()
' Clear the buffer
ReDim bBuffer(iBuffer Size)
' Reduce BytesToRead
iBytesToRead -= iLengthOfReadCh unk
Else
' The client was or has disconneceted from the server... stop
downstreaming.. .
iBytesToRead = -1
bDownloadBroken = True
End If
End While
' In Multipart responses, mark the end of the part
If bMultipart Then objResponse.Out put.WriteLine()
' No need to proceed to the next part if the
' client was disconnected
If bDownloadBroken Then Exit For
Next iLoop
' At this point, the response was finished or cancelled...
If bDownloadBroken Then
' Download is broken...
objFile.State = FileInformation .DownloadState. fsDownloadBroke n
Else
If bMultipart Then
' In multipart responses, close the response once more with
' the boundary and line breaks
objResponse.Out put.WriteLine("--" & MULTIPART_BOUND ARY & "--")
objResponse.Out put.WriteLine()
End If
' The download was finished
objFile.State = FileInformation .DownloadState. fsDownloadFinis hed
End If
objStream.Close ()
End If
End If
objResponse.End ()
End Sub
#End Region
#Region "Private helper functions"
Private Function CheckIfRange(By Val objRequest As HttpRequest, ByVal
objFile As Download.FileIn formation) As Boolean
Dim sRequestHeaderI fRange As String
' Checks the If-Range header if it was sent with the request.
'
' Returns True if the header value matches the file's entity tag,
' or if no header was sent,
' returns False if a header was sent, but does not match the file.
' Retrieve If-Range Header value from Request (objFile.Entity Tag if none
is indicated)
sRequestHeaderI fRange = RetrieveHeader( objRequest, HTTP_HEADER_IF_ RANGE,
objFile.EntityT ag)
' If the requested file entity matches the current
' file entity, return True
Return sRequestHeaderI fRange.Equals(o bjFile.EntityTa g)
End Function
Private Function CheckIfMatch(By Val objRequest As HttpRequest, ByVal
objFile As Download.FileIn formation) As Boolean
Dim sRequestHeaderI fMatch As String
Dim sEntityIDs() As String
Dim bReturn As Boolean
' Checks the If-Match header if it was sent with the request.
'
' Returns True if one of the header values matches the file's entity tag,
' or if no header was sent,
' returns False if a header was sent, but does not match the file.
' Retrieve If-Match Header value from Request (*, meaning any, if none
is indicated)
sRequestHeaderI fMatch = RetrieveHeader( objRequest, HTTP_HEADER_IF_ MATCH,
"*")
If sRequestHeaderI fMatch.Equals(" *") Then
' The server may perform the request as if the
' If-Match header does not exists...
bReturn = True
Else
' One or more Match IDs where sent by the client software...
sEntityIDs = sRequestHeaderI fMatch.Replace( "bytes=",
"").Split(",".T oCharArray)
' Loop through all entity IDs, finding one
' which matches the current file's ID will
' be enough to satisfy the If-Match
For iLoop As Int32 = sEntityIDs.GetL owerBound(0) To
sEntityIDs.GetU pperBound(0)
If sEntityIDs(iLoo p).Trim.Equals( objFile.EntityT ag) Then
bReturn = True
End If
Next iLoop
End If
' Return the result...
Return bReturn
End Function
Private Function CheckIfNoneMatc h(ByVal objRequest As HttpRequest, ByVal
objResponse As HttpResponse, ByVal objFile As Download.FileIn formation) As
Boolean
Dim sRequestHeaderI fNoneMatch As String
Dim sEntityIDs() As String
Dim bReturn As Boolean = True
Dim sReturn As String
' Checks the If-None-Match header if it was sent with the request.
'
' Returns True if one of the header values matches the file's entity tag,
' or if "*" was sent,
' returns False if a header was sent, but does not match the file, or
' if no header was sent.
' Retrieve If-None-Match Header value from Request (*, meaning any, if
none is indicated)
sRequestHeaderI fNoneMatch = RetrieveHeader( objRequest,
HTTP_HEADER_IF_ NONE_MATCH, String.Empty)
If sRequestHeaderI fNoneMatch.Equa ls(String.Empty ) Then
' Perform the request normally...
bReturn = True
ElseIf sRequestHeaderI fNoneMatch.Equa ls("*") Then
' The server must not perform the request
objResponse.Sta tusCode = 412 ' Precondition failed
bReturn = False
Else
' One or more Match IDs where sent by the client software...
sEntityIDs = sRequestHeaderI fNoneMatch.Repl ace("bytes=",
"").Split(",".T oCharArray)
' Loop through all entity IDs, finding one which
' does not match the current file's ID will be
' enough to satisfy the If-None-Match
For iLoop As Int32 = sEntityIDs.GetL owerBound(0) To
sEntityIDs.GetU pperBound(0)
If sEntityIDs(iLoo p).Trim.Equals( objFile.EntityT ag) Then
sReturn = sEntityIDs(iLoo p)
bReturn = False
End If
Next iLoop
If Not bReturn Then
' One of the requested entities matches the current file's tag,
objResponse.App endHeader("ETag ", sReturn)
objResponse.Sta tusCode = 304 ' Not Modified
End If
End If
' Return the result...
Return bReturn
End Function
Private Function CheckIfModified Since(ByVal objRequest As HttpRequest,
ByVal objFile As Download.FileIn formation) As Boolean
Dim sDate As String
Dim dDate As Date
Dim bReturn As Boolean
' Checks the If-Modified header if it was sent with the request.
'
' Returns True, if the file was modified since the
' indicated date (RFC 1123 format), or
' if no header was sent,
' returns False, if the file was not modified since
' the indicated date
' Retrieve If-Modified-Since Header value from Request (Empty if none is
indicated)
sDate = RetrieveHeader( objRequest, HTTP_HEADER_IF_ MODIFIED_SINCE,
String.Empty)
If sDate.Equals(St ring.Empty) Then
' No If-Modified-Since date was indicated,
' so just give this as True
bReturn = True
Else
Try
' ... to parse the indicated sDate to a datetime value
dDate = DateTime.Parse( sDate)
' Return True if the file was modified since or at the indicated
date...
bReturn = objFile.LastWri teTimeUTC >= DateTime.Parse( sDate)
Catch ex As Exception
' Converting the indicated date value failed, return False
bReturn = False
End Try
End If
Return bReturn
End Function
Private Function CheckIfUnmodifi edSince(ByVal objRequest As HttpRequest,
ByVal objFile As Download.FileIn formation) As Boolean
Dim sDate As String
Dim dDate As Date
Dim bReturn As Boolean
' Checks the If-Unmodified or Unless-Modified-Since header, if
' one of them was sent with the request.
'
' Returns True, if the file was not modified since the
' indicated date (RFC 1123 format), or
' if no header was sent,
' returns False, if the file was modified since the indicated date
' Retrieve If-Unmodified-Since Header value from Request (Empty if none
is indicated)
sDate = RetrieveHeader( objRequest, HTTP_HEADER_IF_ UNMODIFIED_SINC E,
String.Empty)
If sDate.Equals(St ring.Empty) Then
' If-Unmodified-Since was not sent, check Unless-Modified-Since...
sDate = RetrieveHeader( objRequest, HTTP_HEADER_UNL ESS_MODIFIED_SI NCE,
String.Empty)
End If
If sDate.Equals(St ring.Empty) Then
' No date was indicated,
' so just give this as True
bReturn = True
Else
Try
' ... to parse the indicated sDate to a datetime value
dDate = DateTime.Parse( sDate)
' Return True if the file was not modified since the indicated date...
bReturn = objFile.LastWri teTimeUTC < DateTime.Parse( sDate)
Catch ex As Exception
' Converting the indicated date value failed, return False
bReturn = False
End Try
End If
Return bReturn
End Function
Private Function ParseRequestHea derRange(ByVal objRequest As HttpRequest,
ByRef lBegin() As Long, ByRef lEnd() As Long, ByVal lMax As Long, ByRef
bRangeRequest As Boolean) As Boolean
Dim bValidRanges As Boolean
Dim sSource As String
Dim iLoop As Int32
Dim sRanges() As String
' Parses the Range header from the Request (if there is one)
' Returns True, if the Range header was valid, or if there was no
' Range header at all (meaning that the whole
' file was requested)
' Returns False, if the Range header asked for unsatisfieable
' ranges
' Retrieve Range Header value from Request (Empty if none is indicated)
sSource = RetrieveHeader( objRequest, HTTP_HEADER_RAN GE, String.Empty)
If sSource.Equals( String.Empty) Then
' No Range was requested, return the entire file range...
ReDim lBegin(0)
ReDim lEnd(0)
lBegin(0) = 0
lEnd(0) = lMax - 1
' A valid range is returned
bValidRanges = True
' no Range request
bRangeRequest = False
Else
' A Range was requested...
' Preset value...
bValidRanges = True
' Return True for the bRange parameter, telling the caller
' that the Request is indeed a Range request...
bRangeRequest = True
' Remove "bytes=" from the beginning, and split the remaining
' string by comma characters
sRanges = sSource.Replace ("bytes=", "").Split(",".T oCharArray)
ReDim lBegin(sRanges. GetUpperBound(0 ))
ReDim lEnd(sRanges.Ge tUpperBound(0))
' Check each found Range request for consistency
For iLoop = sRanges.GetLowe rBound(0) To sRanges.GetUppe rBound(0)
' Split this range request by the dash character,
' sRange(0) contains the requested begin-value,
' sRange(1) contains the requested end-value...
Dim sRange() As String = sRanges(iLoop). Split("-".ToCharArr ay)
' Determine the end of the requested range
If sRange(1).Equal s(String.Empty) Then
' No end was specified, take the entire range
lEnd(iLoop) = lMax - 1
Else
' An end was specified...
lEnd(iLoop) = Long.Parse(sRan ge(1))
End If
' Determine the begin of the requested range
If sRange(0).Equal s(String.Empty) Then
' No begin was specified, which means that
' the end value indicated to return the last n
' bytes of the file:
' Calculate the begin
lBegin(iLoop) = lMax - 1 - lEnd(iLoop)
' ... to the end of the file...
lEnd(iLoop) = lMax - 1
Else
' A normal begin value was indicated...
lBegin(iLoop) = Long.Parse(sRan ge(0))
End If
' Check if the requested range values are valid,
' return False if they are not.
'
' Note:
' Do not clean invalid values up by fitting them into
' valid parameters using Math.Min and Math.Max, because
' some download clients (like Go!Zilla) might send invalid
' (e.g. too large) range requests to determine the file limits!
' Begin and end must not exceed the file size
If (lBegin(iLoop) > (lMax - 1)) Or (lEnd(iLoop) > (lMax - 1)) Then
bValidRanges = False
End If
' Begin and end cannot be < 0
If (lBegin(iLoop) < 0) Or (lEnd(iLoop) < 0) Then
bValidRanges = False
End If
' End must be larger or equal to begin value
If lEnd(iLoop) < lBegin(iLoop) Then
' The requested Range is invalid...
bValidRanges = False
End If
Next iLoop
End If
Return bValidRanges
End Function
Private Function RetrieveHeader( ByVal objRequest As HttpRequest, ByVal
sHeader As String, ByVal sDefault As String) As String
Dim sReturn As String
' Retrieves the indicated Header's value from the Request,
' if the header was not sent, sDefault is returned.
'
' If the value contains quote characters, they are removed.
sReturn = objRequest.Head ers.Item(sHeade r)
If (sReturn Is Nothing) OrElse sReturn.Equals( String.Empty) Then
' The Header wos not found in the Request,
' return the indicated default value...
Return sDefault
Else
' Return the found header value, stripped of any quote characters...
Return sReturn.Replace ("""", "")
End If
End Function
Private Function GenerateHash(By Val objStream As System.IO.Strea m, ByVal
lBegin As Long, ByVal lEnd As Long) As String
Dim bByte(Convert.T oInt32(lEnd)) As Byte
objStream.Read( bByte, Convert.ToInt32 (lBegin), Convert.ToInt32 (lEnd -
lBegin) + 1)
'Instantiate an MD5 Provider object
Dim Md5 As New System.Security .Cryptography.M D5CryptoService Provider
'Compute the hash value from the source
Dim ByteHash() As Byte = Md5.ComputeHash (bByte)
'And convert it to String format for return
Return Convert.ToBase6 4String(ByteHas h)
End Function
#End Region
End Class
*********END CODE****