public class FileDownload : IDisposable
readonly static string MULTIPART_BOUNDARY = " <q1w2e3r4t5y6u7i8o9p0> "; readonly static string MULTIPART_CONTENTTYPE = " multipart/byteranges; boundary= " + MULTIPART_BOUNDARY; readonly static string HTTP_HEADER_Content_Disposition = " Content-Disposition "; readonly static string HTTP_HEADER_ACCEPT_RANGES = " Accept-Ranges "; readonly static string HTTP_HEADER_ACCEPT_RANGES_BYTES = " bytes "; readonly static string HTTP_HEADER_CONTENT_TYPE = " Content-Type "; readonly static string HTTP_HEADER_CONTENT_RANGE = " Content-Range "; readonly static string HTTP_HEADER_CONTENT_LENGTH = " Content-Length "; readonly static string HTTP_HEADER_ENTITY_TAG = " ETag "; readonly static string HTTP_HEADER_LAST_MODIFIED = " Last-Modified "; readonly static string HTTP_HEADER_RANGE = " Range "; readonly static string HTTP_HEADER_IF_RANGE = " If-Range "; readonly static string HTTP_HEADER_IF_MATCH = " If-Match "; readonly static string HTTP_HEADER_IF_NONE_MATCH = " If-None-Match "; readonly static string HTTP_HEADER_IF_MODIFIED_SINCE = " If-Modified-Since "; readonly static string HTTP_HEADER_IF_UNMODIFIED_SINCE = " If-Unmodified-Since "; readonly static string HTTP_HEADER_UNLESS_MODIFIED_SINCE = " Unless-Modified-Since "; readonly static string HTTP_METHOD_GET = " GET "; readonly static string HTTP_METHOD_HEAD = " HEAD "; readonly static string HTTP_METHOD_POST = " POST "; private string contentType = " application/octet-stream "; public string ContentType { get { return contentType; } set { contentType = value; } }
Process Download
/// <summary> /// start to download file /// </summary> /// <param name="fileName"> download file full path </param> /// <param name="headerFileName"> as save file name </param> /// <returns></returns> public DownLoadState ProcessDownload( string fileName, string headerFileName) { HttpContext objContext = HttpContext.Current; // The Response object from the Context HttpResponse objResponse = objContext.Response; // The Request object from the Context HttpRequest objRequest = objContext.Request; // File information object... FileInformation fileInfo; objResponse.BufferOutput = false; // Begin() contains start positions for each requested Range long[] alRequestedRangesBegin = new long[ 1]; // End() contains end positions for each requested Range long[] alRequestedRangesend = new long[ 1]; // Response Header value: Content Length... int iResponseContentLength = 0; // The Stream we're using to download the file in chunks... System.IO.Stream objStream; // Total Bytes to read (per requested range) int iBytesToRead; // Size of the Buffer for chunk-wise reading int iBufferSize = 25000; // The Buffer itself byte[] bBuffer = new byte[iBufferSize]; // Indicates if the download was interrupted bool bDownloadBroken = false; // Indicates if this is a range request bool bIsRangeRequest = false; // Indicates if this is a multipart range request bool bMultipart = false; // Loop counter used to iterate through the ranges int iLoop; if ( string.IsNullOrEmpty(headerFileName)) headerFileName = Path.GetFileName(fileName).Replace( ' ', ' - '); // Content-Disposition value string Content_Disposition_File = " attachment; filename= " + headerFileName + ""; fileInfo = new FileInformation(fileName); fileInfo.ContentType = this.ContentType; // Clear the current output content from the buffer objResponse.Clear(); if (!(objRequest.HttpMethod.Equals(HTTP_METHOD_GET) || objRequest.HttpMethod.Equals(HTTP_METHOD_HEAD) || objRequest.HttpMethod.Equals(HTTP_METHOD_POST, StringComparison.OrdinalIgnoreCase))) // Currently, only the GET and HEAD methods are supported... objResponse.StatusCode = 501; // Not implemented else if (!fileInfo.Exists) // The requested file could not be retrieved... objResponse.StatusCode = 404; // Not found else if (fileInfo.Length > Int32.MaxValue) // The file size is too large... objResponse.StatusCode = 413; // Request Entity Too Large else if (!ParseRequestHeaderRange(objRequest, ref alRequestedRangesBegin, ref alRequestedRangesend, fileInfo.Length, ref bIsRangeRequest)) // The Range request contained bad entries objResponse.StatusCode = 400; // Bad Request else if (!CheckIfModifiedSince(objRequest, fileInfo)) // The entity is still unmodified... objResponse.StatusCode = 304; // Not Modified else if (!CheckIfUnmodifiedSince(objRequest, fileInfo)) // The entity was modified since the requested date... objResponse.StatusCode = 412; // Precondition failed else if (!CheckIfMatch(objRequest, fileInfo)) // The entity does not match the request... objResponse.StatusCode = 412; // Precondition failed else if (!CheckIfNoneMatch(objRequest, objResponse, fileInfo)) { // The entity does match the none-match request, the response // code was set inside the CheckifNoneMatch function } else if (!ValidateDomain(objContext)) // To check the url is from validate domain objResponse.StatusCode = 400; else { // Preliminary checks where successful... if (bIsRangeRequest && CheckIfRange(objRequest, fileInfo)) { // This is a Range request... bMultipart = (alRequestedRangesBegin.GetUpperBound( 0) > 0); // Loop through each Range to calculate the entire Response length for (iLoop = alRequestedRangesBegin.GetLowerBound( 0); iLoop <= alRequestedRangesBegin.GetUpperBound( 0); iLoop++) { // The length of the content (for this range) iResponseContentLength += Convert.ToInt32(alRequestedRangesend[iLoop] - alRequestedRangesBegin[iLoop]) + 1; if (bMultipart) { iResponseContentLength += HTTP_HEADER_Content_Disposition.Length; iResponseContentLength += MULTIPART_BOUNDARY.Length; iResponseContentLength += fileInfo.ContentType.Length; iResponseContentLength += alRequestedRangesBegin[iLoop].ToString().Length; iResponseContentLength += alRequestedRangesend[iLoop].ToString().Length; iResponseContentLength += fileInfo.Length.ToString().Length; iResponseContentLength += 49; } } if (bMultipart) { iResponseContentLength += MULTIPART_BOUNDARY.Length; // 8 is the length of dash and line break characters iResponseContentLength += 8; } else { // in the initial HTTP Header objResponse.AppendHeader(HTTP_HEADER_CONTENT_RANGE, " bytes " + alRequestedRangesBegin[ 0].ToString() + " - " + alRequestedRangesend[ 0].ToString() + " / " + fileInfo.Length.ToString()); } // 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(fileInfo.Length); // Return a normal OK status... objResponse.StatusCode = 200; } // Write file name into the Response objResponse.AppendHeader(HTTP_HEADER_Content_Disposition, Content_Disposition_File); // 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, fileInfo.LastWriteTimeUTC.ToString( " r ")); // Tell the client software that we accept Range request 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, " \" " + fileInfo.EntityTag + " \" "); // Write the Content Type into the Response if (bMultipart) // 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 = fileInfo.ContentType; if (objRequest.HttpMethod.Equals(HTTP_METHOD_HEAD)) { // Only the HEAD was requested, so we can quit the Response right here... } else { // Flush the HEAD information to the client... objResponse.Flush(); // Download is in progress... fileInfo.State = DownLoadState.Progressing; // Open the file as filestream objStream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read); try { // Now, for each requested range, stream the chunks to the client: for (iLoop = alRequestedRangesBegin.GetLowerBound( 0); iLoop <= alRequestedRangesBegin.GetUpperBound( 0); iLoop++) { // Move the stream to the desired start position... objStream.Seek(alRequestedRangesBegin[iLoop], SeekOrigin.Begin); // Calculate the total amount of bytes for this range iBytesToRead = Convert.ToInt32(alRequestedRangesend[iLoop] - alRequestedRangesBegin[iLoop]) + 1; if (bMultipart) { // if this is a multipart response, we must add // certain headers before streaming the content: // The multipart boundary objResponse.Output.WriteLine( " -- " + MULTIPART_BOUNDARY); // The mime type of this part of the content objResponse.Output.WriteLine(HTTP_HEADER_CONTENT_TYPE + " : " + fileInfo.ContentType); // The actual range objResponse.Output.WriteLine(HTTP_HEADER_CONTENT_RANGE + " : bytes " + alRequestedRangesBegin[iLoop].ToString() + " - " + alRequestedRangesend[iLoop].ToString() + " / " + fileInfo.Length.ToString()); /* objResponse.AppendHeader(HTTP_HEADER_CONTENT_RANGE,": bytes " + alRequestedRangesBegin[iLoop].ToString() + "-" + alRequestedRangesend[iLoop].ToString() + "/" + objFile.Length.ToString()); */ // Indicating the end of the intermediate headers objResponse.Output.WriteLine(); } if (iBytesToRead > 0) { // Acording to Microsoft hotfix for .NET framework 2 the TransmitFile method got one overload // If hotfix for .NET framework 2 installed objResponse.TransmitFile(fileName, alRequestedRangesBegin[iLoop], iBytesToRead); // If hotfix not installed // CallHiddenTransmitFile(objResponse, fileName, alRequestedRangesBegin[iLoop], iBytesToRead); } // In Multipart responses, mark the end of the part if (bMultipart) objResponse.Output.WriteLine(); // No need to proceed to the next part if the // client was disconnected if (bDownloadBroken) break; } // At this point, the response was finished or cancelled... if (bDownloadBroken) // Download is broken... fileInfo.State = DownLoadState.Broken; else { if (bMultipart) { // In multipart responses, close the response once more with // the boundary and line breaks objResponse.Output.WriteLine( " -- " + MULTIPART_BOUNDARY + " -- "); objResponse.Output.WriteLine(); } // The download was finished fileInfo.State = DownLoadState.Finished; } } finally { objStream.Close(); } } } return fileInfo.State; }
private string[] domains = null; public string[] Domains { get { if ( null == domains) { string domainStr = ConfigurationManager.AppSettings[ " Domains "].ToLower(); domains = domainStr.Split( new char[] { ' ; ' }, StringSplitOptions.RemoveEmptyEntries); } return domains; } } bool ValidateDomain(HttpContext context) { bool isValidate = true; // Check UrlReferrer is null if ( null == context.Request.UrlReferrer || null == context.Request.UrlReferrer.Host) { isValidate = false; } else { // check the request url is from the domain if (!Domains.Contains(context.Request.UrlReferrer.Host.ToLower())) { isValidate = false; } } return isValidate; } void CallHiddenTransmitFile(HttpResponse objResponse, string fileName, long offset, long size) { Type tempType = objResponse.GetType(); FieldInfo tempInfo = tempType.GetField( " _httpWriter ", BindingFlags.NonPublic | BindingFlags.Instance); HttpWriter _httpWriter = (HttpWriter)tempInfo.GetValue(objResponse); tempType = tempInfo.FieldType; tempInfo = tempType.GetField( " _buffers ", BindingFlags.NonPublic | BindingFlags.Instance); ArrayList _buffers = (ArrayList)tempInfo.GetValue(_httpWriter); tempType = _httpWriter.GetType(); tempInfo = tempType.GetField( " _responseBufferingOn ", BindingFlags.NonPublic | BindingFlags.Instance); bool responseBufferingOn = ( bool)tempInfo.GetValue(_httpWriter); Type classType = tempType.Assembly.GetType( " System.Web.HttpFileResponseElement "); ConstructorInfo[] cinf = classType.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic); object ret = cinf[ 2].Invoke( new object[] { fileName, offset, size, true, false, true }); _buffers.Add(ret); if (!responseBufferingOn) { objResponse.Flush(); } } bool CheckIfRange(HttpRequest objRequest, FileInformation objFile) { string sRequestHeaderIfRange; // Retrieve If-Range Header value from Request (objFile.EntityTag if none is indicated) sRequestHeaderIfRange = RetrieveHeader(objRequest, HTTP_HEADER_IF_RANGE, objFile.EntityTag); // If the requested file entity matches the current // file entity, return true return sRequestHeaderIfRange.Equals(objFile.EntityTag); } bool CheckIfMatch(HttpRequest objRequest, FileInformation objFile) { string sRequestHeaderIfMatch; string[] sEntityIDs; bool breturn = false; // Retrieve If-Match Header value from Request (*, meaning any, if none is indicated) sRequestHeaderIfMatch = RetrieveHeader(objRequest, HTTP_HEADER_IF_MATCH, " * "); if (sRequestHeaderIfMatch.Equals( " * ")) // 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 = sRequestHeaderIfMatch.Replace( " bytes= ", "").Split( " , ".ToCharArray()); // Loop through all entity IDs, finding one // which matches the current file's ID will // be enough to satisfy the If-Match for ( int iLoop = sEntityIDs.GetLowerBound( 0); iLoop <= sEntityIDs.GetUpperBound( 0); iLoop++) { if (sEntityIDs[iLoop].Trim().Equals(objFile.EntityTag)) breturn = true; } } return breturn; } bool CheckIfNoneMatch(HttpRequest objRequest, HttpResponse objResponse, FileInformation objFile) { string sRequestHeaderIfNoneMatch; string[] sEntityIDs; bool breturn = true; string sreturn = ""; // Retrieve If-None-Match Header value from Request (*, meaning any, if none is indicated) sRequestHeaderIfNoneMatch = RetrieveHeader(objRequest, HTTP_HEADER_IF_NONE_MATCH, String.Empty); if (sRequestHeaderIfNoneMatch.Equals(String.Empty)) // Perform the request normally... breturn = true; else { if (sRequestHeaderIfNoneMatch.Equals( " * ")) { // The server must not perform the request objResponse.StatusCode = 412; // Precondition failed breturn = false; } else { // One or more Match IDs where sent by the client software... sEntityIDs = sRequestHeaderIfNoneMatch.Replace( " bytes= ", "").Split( " , ".ToCharArray()); // 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 ( int iLoop = sEntityIDs.GetLowerBound( 0); iLoop <= sEntityIDs.GetUpperBound( 0); iLoop++) { if (sEntityIDs[iLoop].Trim().Equals(objFile.EntityTag)) { sreturn = sEntityIDs[iLoop]; breturn = false; } } if (!breturn) { // One of the requested entities matches the current file // s tag, objResponse.AppendHeader( " ETag ", sreturn); objResponse.StatusCode = 304; // Not Modified } } } return breturn; } bool CheckIfModifiedSince(HttpRequest objRequest, FileInformation objFile) { string sDate; DateTime dDate; bool breturn; // 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(String.Empty)) // 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.LastWriteTimeUTC >= DateTime.Parse(sDate)); } catch { // Converting the indicated date value failed, return false breturn = false; } } return breturn; } bool CheckIfUnmodifiedSince(HttpRequest objRequest, FileInformation objFile) { string sDate; DateTime dDate; bool breturn; // Retrieve If-Unmodified-Since Header value from Request (Empty if none is indicated) sDate = RetrieveHeader(objRequest, HTTP_HEADER_IF_UNMODIFIED_SINCE, String.Empty); if (sDate.Equals(String.Empty)) // If-Unmodified-Since was not sent, check Unless-Modified-Since... sDate = RetrieveHeader(objRequest, HTTP_HEADER_UNLESS_MODIFIED_SINCE, String.Empty); if (sDate.Equals(String.Empty)) // 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.LastWriteTimeUTC < DateTime.Parse(sDate); } catch { // Converting the indicated date value failed, return false breturn = false; } } return breturn; } bool ParseRequestHeaderRange(HttpRequest objRequest, ref long[] lBegin, ref long[] lEnd, long lMax, ref bool bRangeRequest) { bool bValidRanges; string sSource; int iLoop; string[] sRanges; // Retrieve Range Header value from Request (Empty if none is indicated) sSource = RetrieveHeader(objRequest, HTTP_HEADER_RANGE, String.Empty); if (sSource.Equals(String.Empty)) { // No Range was requested, return the entire file range... lBegin = new long[ 1]; // ReDim lBegin(0); lEnd = new long[ 1]; // 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... 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( " , ".ToCharArray()); lBegin = new long[sRanges.GetUpperBound( 0) + 1]; // ReDim lBegin(sRanges.GetUpperBound(0)); lEnd = new long[sRanges.GetUpperBound( 0) + 1]; // ReDim lEnd(sRanges.GetUpperBound(0)); // Check each found Range request for consistency for (iLoop = sRanges.GetLowerBound( 0); iLoop <= sRanges.GetUpperBound( 0); iLoop++) { // Split this range request by the dash character, // sRange(0) contains the requested begin-value, // sRange(1) contains the requested end-value... string[] sRange = sRanges[iLoop].Split( " - ".ToCharArray()); // Determine the end of the requested range if (sRange[ 1].Equals(String.Empty)) // No end was specified, take the entire range lEnd[iLoop] = lMax - 1; else // An end was specified... lEnd[iLoop] = long.Parse(sRange[ 1]); // Determine the begin of the requested range if (sRange[ 0].Equals(String.Empty)) { // 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(sRange[ 0]); // Begin and end must not exceed the file size if ((lBegin[iLoop] > (lMax - 1)) || (lEnd[iLoop] > (lMax - 1))) bValidRanges = false; // Begin and end cannot be < 0 if ((lBegin[iLoop] < 0) || (lEnd[iLoop] < 0)) bValidRanges = false; // End must be larger or equal to begin value if (lEnd[iLoop] < lBegin[iLoop]) // The requested Range is invalid... bValidRanges = false; } } return bValidRanges; } string RetrieveHeader(HttpRequest objRequest, string sHeader, string sDefault) { string sreturn; // 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.Headers[sHeader]; if ((sreturn == null) || (sreturn.Equals( string.Empty))) // 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( " \" ", ""); } string GenerateHash(System.IO.Stream objStream, long lBegin, long lEnd) { byte[] bByte = new byte[Convert.ToInt32(lEnd)]; objStream.Read(bByte, Convert.ToInt32(lBegin), Convert.ToInt32(lEnd - lBegin) + 1); // Instantiate an MD5 Provider object MD5CryptoServiceProvider Md5 = new System.Security.Cryptography.MD5CryptoServiceProvider(); // Compute the hash value from the source byte[] ByteHash = Md5.ComputeHash(bByte); // And convert it to String format for return return Convert.ToBase64String(ByteHash); } #region IDisposable Members void IDisposable.Dispose() { throw new NotImplementedException(); } #endregion
File Information
1 public class FileInformation 2 { 3 private DownLoadState state; 4 private DateTime createTime; 5 private string fileName = ""; 6 private string contentType = " application/octet-stream "; 7 private long? fileLength = null; 8 9 public FileInformation( string fileName) 10 { 11 this.fileName = fileName; 12 createTime = DateTime.Now; 13 } 14 15 public bool Exists 16 { 17 get 18 { 19 return File.Exists(fileName); 20 } 21 } 22 23 public string FullName 24 { 25 get { return fileName;} 26 } 27 28 public DateTime LastWriteTimeUTC 29 { 30 get { return createTime.ToUniversalTime(); } 31 } 32 33 public long Length 34 { 35 get 36 { 37 if (fileLength.HasValue == false) 38 { 39 FileInfo info = new FileInfo(fileName); 40 fileLength = info.Length; 41 } 42 return fileLength.Value; 43 } 44 } 45 46 public string ContentType 47 { 48 get 49 { 50 return contentType; 51 } 52 set 53 { 54 contentType = value; 55 } 56 } 57 public string EntityTag 58 { 59 get 60 { 61 return fileName.GetHashCode().ToString();; 62 } 63 } 64 65 public virtual DownLoadState State 66 { 67 get 68 { 69 return state; 70 } 71 set 72 { 73 state = value; 74 } 75 } 76 }
DownLoad Status
1 public enum DownLoadState 2 { 3 /// Clear: No download in progress, 4 /// the file can be manipulated 5 Clear = 1, 6 7 /// Locked: A dynamically created file must 8 /// not be changed 9 Locked = 2, 10 11 /// In Progress: File is locked, and download 12 /// is currently in progress 13 Progressing = 6, 14 15 /// Broken: File is locked, download was in 16 /// progress, but was cancelled 17 Broken = 10, 18 19 /// Finished: File is locked, download 20 /// was completed 21 Finished = 18 22 }