To create Partial Response the ActionResult will require some basic information about the file. It also has to be able to generate entity tag:
public abstract class RangeFileResult : ActionResultNow we should get the actual byte range(s) from the request:
{
...
#region Properties
public string ContentType { get; private set; }
public string FileName { get; private set; }
public DateTime FileModificationDate { get; private set; }
private DateTime HttpModificationDate { get; set; }
public long FileLength { get; private set; }
private string EntityTag { get; set; }
private long[] RangesStartIndexes { get; set; }
private long[] RangesEndIndexes { get; set; }
private bool RangeRequest { get; set; }
private bool MultipartRequest { get; set; }
#endregion
#region Constructor
protected RangeFileResult(string contentType, string fileName, DateTime modificationDate, long fileLength)
{
if (String.IsNullOrEmpty(contentType))
throw new ArgumentNullException("contentType");
ContentType = contentType;
FileName = fileName;
FileLength = fileLength;
FileModificationDate = modificationDate;
//Modification date for header values comparisons purposes
HttpModificationDate = modificationDate.ToUniversalTime();
HttpModificationDate = new DateTime(HttpModificationDate.Year, HttpModificationDate.Month, HttpModificationDate.Day, HttpModificationDate.Hour, HttpModificationDate.Minute, HttpModificationDate.Second, DateTimeKind.Utc);
}
#endregion
#region Methods
protected virtual string GenerateEntityTag(ControllerContext context)
{
//Generate entity tag based on file name and modification date
byte[] entityTagBytes = Encoding.ASCII.GetBytes(String.Format("{0}|{1}", FileName, FileModificationDate));
return Convert.ToBase64String(new MD5CryptoServiceProvider().ComputeHash(entityTagBytes));
}
...
#endregion
}
#region FieldsThe ranges have to be validated for consistency:
private static char[] _commaSplitArray = new char[] { ',' };
private static char[] _dashSplitArray = new char[] { '-' };
private static string[] _httpDateFormats = new string[] { "r", "dddd, dd-MMM-yy HH':'mm':'ss 'GMT'", "ddd MMM d HH':'mm':'ss yyyy" };
#endregion
...
#region Methods
...
//Helper method for getting HTTP headers values
private string GetHeader(HttpRequestBase request, string header, string defaultValue = "")
{
return String.IsNullOrEmpty(request.Headers[header]) ? defaultValue : request.Headers[header].Replace("\"", String.Empty);
}
private void GetRanges(HttpRequestBase request)
{
//Get "Range" header from request
string rangesHeader = GetHeader(request, "Range");
//Get "If-Range" header from request
string ifRangeHeader = GetHeader(request, "If-Range", EntityTag);
DateTime ifRangeHeaderDate;
bool isIfRangeHeaderDate = DateTime.TryParseExact(ifRangeHeader, _httpDateFormats, null, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out ifRangeHeaderDate);
//If there is no "Range" header,
//or the entity tag from "If-Range" header does not match this entity tag,
//or the modification date is greater than modification date from "If-Range" header
if (String.IsNullOrEmpty(rangesHeader) || (!isIfRangeHeaderDate && ifRangeHeader != EntityTag) || (isIfRangeHeaderDate && HttpModificationDate > ifRangeHeaderDate))
{
//Return entire file
RangesStartIndexes = new long[] { 0 };
RangesEndIndexes = new long[] { FileLength - 1 };
RangeRequest = false;
MultipartRequest = false;
}
//Otherwise
else
{
//Split "Range" header value into ranges
string[] ranges = rangesHeader.Replace("bytes=", String.Empty).Split(_commaSplitArray);
RangesStartIndexes = new long[ranges.Length];
RangesEndIndexes = new long[ranges.Length];
RangeRequest = true;
MultipartRequest = (ranges.Length > 1);
//Get the star and end index for the range
for (int i = 0; i < ranges.Length; i++)
{
string[] currentRange = ranges[i].Split(_dashSplitArray);
if (String.IsNullOrEmpty(currentRange[1]))
RangesEndIndexes[i] = FileLength - 1;
else
RangesEndIndexes[i] = Int64.Parse(currentRange[1]);
if (String.IsNullOrEmpty(currentRange[0]))
{
RangesStartIndexes[i] = FileLength - 1 - RangesEndIndexes[i];
RangesEndIndexes[i] = FileLength - 1;
}
else
RangesStartIndexes[i] = Int64.Parse(currentRange[0]);
}
}
}
...
#endregion
private bool ValidateRanges(HttpResponseBase response)We also should validate modification date against If-Modified-Since, If-Unmodified-Since and Unless-Modified-Since headers:
{
if (FileLength > Int32.MaxValue)
{
response.StatusCode = 413;
return false;
}
for (int i = 0; i < RangesStartIndexes.Length; i++)
{
if (RangesStartIndexes[i] > FileLength - 1 || RangesEndIndexes[i] > FileLength - 1 || RangesStartIndexes[i] < 0 || RangesEndIndexes[i] < 0 || RangesEndIndexes[i] < RangesStartIndexes[i])
{
response.StatusCode = 400;
return false;
}
}
return true;
}
private bool ValidateModificationDate(HttpRequestBase request, HttpResponseBase response)Last validation is for If-Match and If-None-Match headers:
{
//First validate "If-Modified-Since" header
string modifiedSinceHeader = GetHeader(request, "If-Modified-Since");
if (!String.IsNullOrEmpty(modifiedSinceHeader))
{
DateTime modifiedSinceDate;
DateTime.TryParseExact(modifiedSinceHeader, _httpDateFormats, null, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out modifiedSinceDate);
if (HttpModificationDate <= modifiedSinceDate)
{
response.StatusCode = 304;
return false;
}
}
//Then validate "If-Unmodified-Since" or "Unless-Modified-Since"
string unmodifiedSinceHeader = GetHeader(request, "If-Unmodified-Since", GetHeader(request, "Unless-Modified-Since"));
if (!String.IsNullOrEmpty(unmodifiedSinceHeader))
{
DateTime unmodifiedSinceDate;
bool unmodifiedSinceDateParsed = DateTime.TryParseExact(unmodifiedSinceHeader, _httpDateFormats, null, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out unmodifiedSinceDate);
if (HttpModificationDate > unmodifiedSinceDate)
{
response.StatusCode = 412;
return false;
}
}
return true;
}
private bool ValidateEntityTag(HttpRequestBase request, HttpResponseBase response)We can finally wrap it up and implement ExecuteResult method:
{
//Get "If-Match" header from request
string matchHeader = GetHeader(request, "If-Match");
//If header exists and it's value is different from "*"
if (!String.IsNullOrEmpty(matchHeader) && matchHeader != "*")
{
//Split header value into list of etity tags
string[] entitiesTags = matchHeader.Split(_commaSplitArray);
int entitieTagIndex;
for (entitieTagIndex = 0; entitieTagIndex < entitiesTags.Length; entitieTagIndex++)
{
if (EntityTag == entitiesTags[entitieTagIndex])
break;
}
//If our entity tag wasn't found
if (entitieTagIndex >= entitiesTags.Length)
{
//Set proper response status code
response.StatusCode = 412;
return false;
}
}
//Get "If-None-Match" header from request
string noneMatchHeader = GetHeader(request, "If-None-Match");
//If header exists
if (!String.IsNullOrEmpty(noneMatchHeader))
{
//If header value equals "*"
if (noneMatchHeader == "*")
{
//Set proper response status code
response.StatusCode = 412;
return false;
}
//Split header value into list of etity tags
string[] entitiesTags = noneMatchHeader.Split(_commaSplitArray);
foreach (string entityTag in entitiesTags)
{
if (EntityTag == entityTag)
{
//Set proper response status code
response.AddHeader("ETag", String.Format("\"{0}\"", entityTag));
response.StatusCode = 304;
return false;
}
}
}
return true;
}
protected abstract void WriteEntireEntity(HttpResponseBase response);The base class is now ready, to create any specific implementation we just need to override WriteEntireEntity and WriteEntityRange methods:
protected abstract void WriteEntityRange(HttpResponseBase response, long rangeStartIndex, long rangeEndIndex);
public override void ExecuteResult(ControllerContext context)
{
//Generate entity tag
EntityTag = GenerateEntityTag(context);
//Get ranges from request
GetRanges(context.HttpContext.Request);
//If all validations are successful
if (ValidateRanges(context.HttpContext.Response) && ValidateModificationDate(context.HttpContext.Request, context.HttpContext.Response) && ValidateEntityTag(context.HttpContext.Request, context.HttpContext.Response))
{
//Set common headers
context.HttpContext.Response.AddHeader("Last-Modified", FileModificationDate.ToString("r"));
context.HttpContext.Response.AddHeader("ETag", String.Format("\"{0}\"", EntityTag));
context.HttpContext.Response.AddHeader("Accept-Ranges", "bytes");
//If this is not range request
if (!RangeRequest)
{
//Set standard headers
context.HttpContext.Response.AddHeader("Content-Length", FileLength.ToString());
context.HttpContext.Response.ContentType = ContentType;
//Set status code
context.HttpContext.Response.StatusCode = 200;
//If this is not HEAD request
if (!context.HttpContext.Request.HttpMethod.Equals("HEAD"))
//Write entire file to response
WriteEntireEntity(context.HttpContext.Response);
}
//If this is range request
else
{
string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
//Compute and set content length
context.HttpContext.Response.AddHeader("Content-Length", GetContentLength(boundary).ToString());
//If this is not multipart request
if (!MultipartRequest)
{
//Set content range and type
context.HttpContext.Response.AddHeader("Content-Range", String.Format("bytes {0}-{1}/{2}", RangesStartIndexes[0], RangesEndIndexes[0], FileLength));
context.HttpContext.Response.ContentType = ContentType;
}
//Otherwise
else
//Set proper content type
context.HttpContext.Response.ContentType = String.Format("multipart/byteranges; boundary={0}", boundary);
//Set status code
context.HttpContext.Response.StatusCode = 206;
//If this not HEAD request
if (!context.HttpContext.Request.HttpMethod.Equals("HEAD"))
{
//For each requested range
for (int i = 0; i < RangesStartIndexes.Length; i++)
{
//If this is multipart request
if (MultipartRequest)
{
//Write additional multipart info
context.HttpContext.Response.Write(String.Format("--{0}\r\n", boundary));
context.HttpContext.Response.Write(String.Format("Content-Type: {0}\r\n", ContentType));
context.HttpContext.Response.Write(String.Format("Content-Range: bytes {0}-{1}/{2}\r\n\r\n", RangesStartIndexes[i], RangesEndIndexes[i], FileLength));
}
//Write range (with multipart separator if required)
if (context.HttpContext.Response.IsClientConnected)
{
WriteEntityRange(context.HttpContext.Response, RangesStartIndexes[i], RangesEndIndexes[i]);
if (MultipartRequest)
context.HttpContext.Response.Write("\r\n");
context.HttpContext.Response.Flush();
}
else
return;
}
//If this is multipart request
if (MultipartRequest)
context.HttpContext.Response.Write(String.Format("--{0}--", boundary));
}
}
}
}
//Helper method for computing content length
private int GetContentLength(string boundary)
{
int contentLength = 0;
for (int i = 0; i < RangesStartIndexes.Length; i++)
{
contentLength += Convert.ToInt32(RangesEndIndexes[i] - RangesStartIndexes[i]) + 1;
if (MultipartRequest)
contentLength += boundary.Length + ContentType.Length + RangesStartIndexes[i].ToString().Length + RangesEndIndexes[i].ToString().Length + FileLength.ToString().Length + 49;
}
if (MultipartRequest)
contentLength += boundary.Length + 4;
return contentLength;
}
public class RangeFilePathResult : RangeFileResultI have added the RangeFileResult class, and following implementations to the latest version of Lib.Web.Mvc:
{
#region Fields
private const int _bufferSize = 0x1000;
#endregion
#region Constructor
public RangeFilePathResult(string contentType, string fileName, DateTime modificationDate, long fileLength)
: base(contentType, fileName, modificationDate, fileLength)
{
if (String.IsNullOrEmpty(fileName))
throw new ArgumentNullException("fileName");
}
#endregion
#region Methods
protected override void WriteEntireEntity(HttpResponseBase response)
{
response.TransmitFile(FileName);
}
protected override void WriteEntityRange(HttpResponseBase response, long rangeStartIndex, long rangeEndIndex)
{
using (FileStream stream = new FileStream(FileName, FileMode.Open, FileAccess.Read))
{
stream.Seek(rangeStartIndex, SeekOrigin.Begin);
int bytesRemaining = Convert.ToInt32(rangeEndIndex - rangeStartIndex) + 1;
byte[] buffer = new byte[_bufferSize];
while (bytesRemaining > 0)
{
int bytesRead = stream.Read(buffer, 0, _bufferSize < bytesRemaining ? _bufferSize : bytesRemaining);
response.OutputStream.Write(buffer, 0, bytesRead);
bytesRemaining -= bytesRead;
}
stream.Close();
}
}
#endregion
}
- RangeFilePathResult
- RangeFileStreamResult
- RangeFileContentResult

Email