Added the ability for the server to handle byte-range requests, and also added a static file handler to utilize it

This commit is contained in:
LukePulverenti Luke Pulverenti luke pulverenti 2012-08-10 09:07:58 -04:00
parent dce7706382
commit 2536011247
7 changed files with 548 additions and 223 deletions

View File

@ -46,9 +46,7 @@ namespace MediaBrowser.Api.HttpHandlers
}
}
public override DateTime? LastDateModified
{
get
protected override DateTime? GetLastDateModified()
{
try
{
@ -56,8 +54,7 @@ namespace MediaBrowser.Api.HttpHandlers
}
catch
{
return null;
}
return base.GetLastDateModified();
}
}

View File

@ -1,8 +1,8 @@
using System;
using System.ComponentModel.Composition;
using System.Net;
using System.Reactive.Linq;
using MediaBrowser.Api.HttpHandlers;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Net.Handlers;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller;
@ -22,78 +22,75 @@ namespace MediaBrowser.Api
{
var httpServer = Kernel.Instance.HttpServer;
httpServer.Where(ctx => ctx.LocalPath.IndexOf("/api/", StringComparison.OrdinalIgnoreCase) != -1).Subscribe(ctx =>
httpServer.Where(ctx => ctx.Request.Url.LocalPath.IndexOf("/api/", StringComparison.OrdinalIgnoreCase) != -1).Subscribe(ctx =>
{
BaseHandler handler = GetHandler(ctx);
if (handler != null)
{
ctx.Respond(handler);
handler.ProcessRequest(ctx);
}
});
}
private BaseHandler GetHandler(RequestContext ctx)
private BaseHandler GetHandler(HttpListenerContext ctx)
{
BaseHandler handler = null;
string localPath = ctx.LocalPath;
string localPath = ctx.Request.Url.LocalPath;
if (localPath.EndsWith("/api/item", StringComparison.OrdinalIgnoreCase))
{
handler = new ItemHandler();
return new ItemHandler();
}
else if (localPath.EndsWith("/api/image", StringComparison.OrdinalIgnoreCase))
{
handler = new ImageHandler();
return new ImageHandler();
}
else if (localPath.EndsWith("/api/users", StringComparison.OrdinalIgnoreCase))
{
handler = new UsersHandler();
return new UsersHandler();
}
else if (localPath.EndsWith("/api/genre", StringComparison.OrdinalIgnoreCase))
{
handler = new GenreHandler();
return new GenreHandler();
}
else if (localPath.EndsWith("/api/genres", StringComparison.OrdinalIgnoreCase))
{
handler = new GenresHandler();
return new GenresHandler();
}
else if (localPath.EndsWith("/api/studio", StringComparison.OrdinalIgnoreCase))
{
handler = new StudioHandler();
return new StudioHandler();
}
else if (localPath.EndsWith("/api/studios", StringComparison.OrdinalIgnoreCase))
{
handler = new StudiosHandler();
return new StudiosHandler();
}
else if (localPath.EndsWith("/api/recentlyaddeditems", StringComparison.OrdinalIgnoreCase))
{
handler = new RecentlyAddedItemsHandler();
return new RecentlyAddedItemsHandler();
}
else if (localPath.EndsWith("/api/inprogressitems", StringComparison.OrdinalIgnoreCase))
{
handler = new InProgressItemsHandler();
return new InProgressItemsHandler();
}
else if (localPath.EndsWith("/api/userconfiguration", StringComparison.OrdinalIgnoreCase))
{
handler = new UserConfigurationHandler();
return new UserConfigurationHandler();
}
else if (localPath.EndsWith("/api/plugins", StringComparison.OrdinalIgnoreCase))
{
handler = new PluginsHandler();
return new PluginsHandler();
}
else if (localPath.EndsWith("/api/pluginconfiguration", StringComparison.OrdinalIgnoreCase))
{
handler = new PluginConfigurationHandler();
return new PluginConfigurationHandler();
}
if (handler != null)
else if (localPath.EndsWith("/api/static", StringComparison.OrdinalIgnoreCase))
{
handler.RequestContext = ctx;
return new StaticFileHandler();
}
return handler;
return null;
}
}
}

View File

@ -58,6 +58,7 @@
<Compile Include="Configuration\ApplicationPaths.cs" />
<Compile Include="Configuration\BaseApplicationConfiguration.cs" />
<Compile Include="Events\GenericItemEventArgs.cs" />
<Compile Include="Net\Handlers\StaticFileHandler.cs" />
<Compile Include="Net\MimeTypes.cs" />
<Compile Include="Serialization\JsonSerializer.cs" />
<Compile Include="Kernel\BaseKernel.cs" />
@ -73,7 +74,6 @@
<Compile Include="Net\Handlers\BaseJsonHandler.cs" />
<Compile Include="Net\HttpServer.cs" />
<Compile Include="Net\Request.cs" />
<Compile Include="Net\RequestContext.cs" />
<Compile Include="Net\StreamExtensions.cs" />
<Compile Include="Plugins\BasePlugin.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />

View File

@ -3,28 +3,17 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using MediaBrowser.Common.Logging;
namespace MediaBrowser.Common.Net.Handlers
{
public abstract class BaseHandler
{
/// <summary>
/// Response headers
/// </summary>
public IDictionary<string, string> Headers = new Dictionary<string, string>();
private Stream CompressedStream { get; set; }
public virtual bool UseChunkedEncoding
{
get
{
return true;
}
}
public virtual long? ContentLength
public virtual bool? UseChunkedEncoding
{
get
{
@ -32,6 +21,21 @@ namespace MediaBrowser.Common.Net.Handlers
}
}
private bool _TotalContentLengthDiscovered = false;
private long? _TotalContentLength = null;
public long? TotalContentLength
{
get
{
if (!_TotalContentLengthDiscovered)
{
_TotalContentLength = GetTotalContentLength();
}
return _TotalContentLength;
}
}
/// <summary>
/// Returns true or false indicating if the handler writes to the stream asynchronously.
/// If so the subclass will be responsible for disposing the stream when complete.
@ -44,29 +48,18 @@ namespace MediaBrowser.Common.Net.Handlers
}
}
/// <summary>
/// The action to write the response to the output stream
/// </summary>
public Action<Stream> WriteStream
protected virtual bool SupportsByteRangeRequests
{
get
{
return s =>
{
WriteReponse(s);
if (!IsAsyncHandler)
{
DisposeResponseStream();
}
};
return false;
}
}
/// <summary>
/// The original RequestContext
/// The original HttpListenerContext
/// </summary>
public RequestContext RequestContext { get; set; }
protected HttpListenerContext HttpListenerContext { get; private set; }
/// <summary>
/// The original QueryString
@ -75,7 +68,54 @@ namespace MediaBrowser.Common.Net.Handlers
{
get
{
return RequestContext.Request.QueryString;
return HttpListenerContext.Request.QueryString;
}
}
protected List<KeyValuePair<long, long?>> _RequestedRanges = null;
protected IEnumerable<KeyValuePair<long, long?>> RequestedRanges
{
get
{
if (_RequestedRanges == null)
{
_RequestedRanges = new List<KeyValuePair<long, long?>>();
if (IsRangeRequest)
{
// Example: bytes=0-,32-63
string[] ranges = HttpListenerContext.Request.Headers["Range"].Split('=')[1].Split(',');
foreach (string range in ranges)
{
string[] vals = range.Split('-');
long start = 0;
long? end = null;
if (!string.IsNullOrEmpty(vals[0]))
{
start = long.Parse(vals[0]);
}
if (!string.IsNullOrEmpty(vals[1]))
{
end = long.Parse(vals[1]);
}
_RequestedRanges.Add(new KeyValuePair<long, long?>(start, end));
}
}
}
return _RequestedRanges;
}
}
protected bool IsRangeRequest
{
get
{
return HttpListenerContext.Request.Headers.AllKeys.Contains("Range");
}
}
@ -87,13 +127,7 @@ namespace MediaBrowser.Common.Net.Handlers
/// <summary>
/// Gets the status code to include in the response headers
/// </summary>
public virtual int StatusCode
{
get
{
return 200;
}
}
protected int StatusCode { get; set; }
/// <summary>
/// Gets the cache duration to include in the response headers
@ -106,15 +140,22 @@ namespace MediaBrowser.Common.Net.Handlers
}
}
private bool _LastDateModifiedDiscovered = false;
private DateTime? _LastDateModified = null;
/// <summary>
/// Gets the last date modified of the content being returned, if this can be determined.
/// This will be used to invalidate the cache, so it's not needed if CacheDuration is 0.
/// </summary>
public virtual DateTime? LastDateModified
public DateTime? LastDateModified
{
get
{
return null;
if (!_LastDateModifiedDiscovered)
{
_LastDateModified = GetLastDateModified();
}
return _LastDateModified;
}
}
@ -130,7 +171,7 @@ namespace MediaBrowser.Common.Net.Handlers
{
get
{
string enc = RequestContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
return enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1;
}
@ -140,7 +181,7 @@ namespace MediaBrowser.Common.Net.Handlers
{
get
{
string enc = RequestContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
if (enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1)
{
@ -155,30 +196,127 @@ namespace MediaBrowser.Common.Net.Handlers
}
}
protected virtual void PrepareResponseBeforeWriteOutput(HttpListenerResponse response)
public void ProcessRequest(HttpListenerContext ctx)
{
// Don't force this to true. HttpListener will default it to true if supported by the client.
if (!UseChunkedEncoding)
HttpListenerContext = ctx;
Logger.LogInfo("Http Server received request at: " + ctx.Request.Url.ToString());
Logger.LogInfo("Http Headers: " + string.Join(",", ctx.Request.Headers.AllKeys.Select(k => k + "=" + ctx.Request.Headers[k])));
ctx.Response.AddHeader("Access-Control-Allow-Origin", "*");
ctx.Response.KeepAlive = true;
if (SupportsByteRangeRequests && IsRangeRequest)
{
response.SendChunked = false;
ctx.Response.Headers["Accept-Ranges"] = "bytes";
}
if (ContentLength.HasValue)
{
response.ContentLength64 = ContentLength.Value;
}
// Set the initial status code
// When serving a range request, we need to return status code 206 to indicate a partial response body
StatusCode = SupportsByteRangeRequests && IsRangeRequest ? 206 : 200;
if (CompressResponse && ClientSupportsCompression)
{
response.AddHeader("Content-Encoding", CompressionMethod);
}
ctx.Response.ContentType = ContentType;
TimeSpan cacheDuration = CacheDuration;
if (ctx.Request.Headers.AllKeys.Contains("If-Modified-Since"))
{
DateTime ifModifiedSince;
if (DateTime.TryParse(ctx.Request.Headers["If-Modified-Since"].Replace(" GMT", string.Empty), out ifModifiedSince))
{
// If the cache hasn't expired yet just return a 304
if (IsCacheValid(ifModifiedSince, cacheDuration, LastDateModified))
{
StatusCode = 304;
}
}
}
if (StatusCode == 200 || StatusCode == 206)
{
ProcessUncachedResponse(ctx, cacheDuration);
}
else
{
ctx.Response.StatusCode = StatusCode;
ctx.Response.SendChunked = false;
DisposeResponseStream();
}
}
private void ProcessUncachedResponse(HttpListenerContext ctx, TimeSpan cacheDuration)
{
long? totalContentLength = TotalContentLength;
// By default, use chunked encoding if we don't know the content length
bool useChunkedEncoding = UseChunkedEncoding == null ? (totalContentLength == null) : UseChunkedEncoding.Value;
// Don't force this to true. HttpListener will default it to true if supported by the client.
if (!useChunkedEncoding)
{
ctx.Response.SendChunked = false;
}
// Set the content length, if we know it
if (totalContentLength.HasValue)
{
ctx.Response.ContentLength64 = totalContentLength.Value;
}
// Add the compression header
if (CompressResponse && ClientSupportsCompression)
{
ctx.Response.AddHeader("Content-Encoding", CompressionMethod);
}
// Add caching headers
if (cacheDuration.Ticks > 0)
{
CacheResponse(response, cacheDuration, LastDateModified);
CacheResponse(ctx.Response, cacheDuration, LastDateModified);
}
PrepareUncachedResponse(ctx, cacheDuration);
// Set the status code
ctx.Response.StatusCode = StatusCode;
if (StatusCode == 200 || StatusCode == 206)
{
// Finally, write the response data
Stream outputStream = ctx.Response.OutputStream;
if (CompressResponse && ClientSupportsCompression)
{
if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase))
{
CompressedStream = new DeflateStream(outputStream, CompressionLevel.Fastest, false);
}
else
{
CompressedStream = new GZipStream(outputStream, CompressionLevel.Fastest, false);
}
outputStream = CompressedStream;
}
WriteResponseToOutputStream(outputStream);
if (!IsAsyncHandler)
{
DisposeResponseStream();
}
}
else
{
ctx.Response.SendChunked = false;
DisposeResponseStream();
}
}
protected virtual void PrepareUncachedResponse(HttpListenerContext ctx, TimeSpan cacheDuration)
{
}
private void CacheResponse(HttpListenerResponse response, TimeSpan duration, DateTime? dateModified)
@ -190,29 +328,6 @@ namespace MediaBrowser.Common.Net.Handlers
response.Headers[HttpResponseHeader.LastModified] = lastModified.ToString("r");
}
private void WriteReponse(Stream stream)
{
PrepareResponseBeforeWriteOutput(RequestContext.Response);
if (CompressResponse && ClientSupportsCompression)
{
if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase))
{
CompressedStream = new DeflateStream(stream, CompressionLevel.Fastest, false);
}
else
{
CompressedStream = new GZipStream(stream, CompressionLevel.Fastest, false);
}
WriteResponseToOutputStream(CompressedStream);
}
else
{
WriteResponseToOutputStream(stream);
}
}
protected abstract void WriteResponseToOutputStream(Stream stream);
protected void DisposeResponseStream()
@ -222,7 +337,45 @@ namespace MediaBrowser.Common.Net.Handlers
CompressedStream.Dispose();
}
RequestContext.Response.OutputStream.Dispose();
HttpListenerContext.Response.OutputStream.Dispose();
}
private bool IsCacheValid(DateTime ifModifiedSince, TimeSpan cacheDuration, DateTime? dateModified)
{
if (dateModified.HasValue)
{
DateTime lastModified = NormalizeDateForComparison(dateModified.Value);
ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
return lastModified <= ifModifiedSince;
}
DateTime cacheExpirationDate = ifModifiedSince.Add(cacheDuration);
if (DateTime.Now < cacheExpirationDate)
{
return true;
}
return false;
}
/// <summary>
/// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
/// </summary>
private DateTime NormalizeDateForComparison(DateTime date)
{
return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second);
}
protected virtual long? GetTotalContentLength()
{
return null;
}
protected virtual DateTime? GetLastDateModified()
{
return null;
}
}
}

View File

@ -0,0 +1,282 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using MediaBrowser.Common.Logging;
namespace MediaBrowser.Common.Net.Handlers
{
public class StaticFileHandler : BaseHandler
{
public string Path
{
get
{
return QueryString["path"];
}
}
private bool FileStreamDiscovered = false;
private FileStream _FileStream = null;
private FileStream FileStream
{
get
{
if (!FileStreamDiscovered)
{
try
{
_FileStream = File.OpenRead(Path);
}
catch (FileNotFoundException)
{
StatusCode = 404;
}
catch (DirectoryNotFoundException)
{
StatusCode = 404;
}
catch (UnauthorizedAccessException)
{
StatusCode = 403;
}
finally
{
FileStreamDiscovered = true;
}
}
return _FileStream;
}
}
protected override bool SupportsByteRangeRequests
{
get
{
return true;
}
}
public override bool CompressResponse
{
get
{
string contentType = ContentType;
// Can't compress these
if (IsRangeRequest)
{
return false;
}
// Don't compress media
if (contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase))
{
return false;
}
// It will take some work to support compression within this handler
return false;
}
}
protected override long? GetTotalContentLength()
{
try
{
return FileStream.Length;
}
catch
{
return base.GetTotalContentLength();
}
}
protected override DateTime? GetLastDateModified()
{
try
{
return File.GetLastWriteTime(Path);
}
catch
{
return base.GetLastDateModified();
}
}
protected override bool IsAsyncHandler
{
get
{
return true;
}
}
public override string ContentType
{
get
{
return MimeTypes.GetMimeType(Path);
}
}
protected async override void WriteResponseToOutputStream(Stream stream)
{
try
{
if (FileStream != null)
{
if (IsRangeRequest)
{
KeyValuePair<long, long?> requestedRange = RequestedRanges.First();
// If the requested range is "0-" and we know the total length, we can optimize by avoiding having to buffer the content into memory
if (requestedRange.Value == null && TotalContentLength != null)
{
await ServeCompleteRangeRequest(requestedRange, stream);
}
else if (TotalContentLength.HasValue)
{
// This will have to buffer a portion of the content into memory
await ServePartialRangeRequestWithKnownTotalContentLength(requestedRange, stream);
}
else
{
// This will have to buffer the entire content into memory
await ServePartialRangeRequestWithUnknownTotalContentLength(requestedRange, stream);
}
}
else
{
await FileStream.CopyToAsync(stream);
}
}
}
catch (Exception ex)
{
Logger.LogException("WriteResponseToOutputStream", ex);
}
finally
{
if (FileStream != null)
{
FileStream.Dispose();
}
DisposeResponseStream();
}
}
/// <summary>
/// Handles a range request of "bytes=0-"
/// This will serve the complete content and add the content-range header
/// </summary>
private async Task ServeCompleteRangeRequest(KeyValuePair<long, long?> requestedRange, Stream responseStream)
{
long totalContentLength = TotalContentLength.Value;
long rangeStart = requestedRange.Key;
long rangeEnd = totalContentLength - 1;
long rangeLength = 1 + rangeEnd - rangeStart;
// Content-Length is the length of what we're serving, not the original content
HttpListenerContext.Response.ContentLength64 = rangeLength;
HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
if (rangeStart > 0)
{
FileStream.Position = rangeStart;
}
await FileStream.CopyToAsync(responseStream);
}
/// <summary>
/// Serves a partial range request where the total content length is not known
/// </summary>
private async Task ServePartialRangeRequestWithUnknownTotalContentLength(KeyValuePair<long, long?> requestedRange, Stream responseStream)
{
// Read the entire stream so that we can determine the length
byte[] bytes = await ReadBytes(FileStream, 0, null);
long totalContentLength = bytes.LongLength;
long rangeStart = requestedRange.Key;
long rangeEnd = requestedRange.Value ?? (totalContentLength - 1);
long rangeLength = 1 + rangeEnd - rangeStart;
// Content-Length is the length of what we're serving, not the original content
HttpListenerContext.Response.ContentLength64 = rangeLength;
HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
await responseStream.WriteAsync(bytes, Convert.ToInt32(rangeStart), Convert.ToInt32(rangeLength));
}
/// <summary>
/// Serves a partial range request where the total content length is already known
/// </summary>
private async Task ServePartialRangeRequestWithKnownTotalContentLength(KeyValuePair<long, long?> requestedRange, Stream responseStream)
{
long totalContentLength = TotalContentLength.Value;
long rangeStart = requestedRange.Key;
long rangeEnd = requestedRange.Value ?? (totalContentLength - 1);
long rangeLength = 1 + rangeEnd - rangeStart;
// Only read the bytes we need
byte[] bytes = await ReadBytes(FileStream, Convert.ToInt32(rangeStart), Convert.ToInt32(rangeLength));
// Content-Length is the length of what we're serving, not the original content
HttpListenerContext.Response.ContentLength64 = rangeLength;
HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
await responseStream.WriteAsync(bytes, 0, Convert.ToInt32(rangeLength));
}
/// <summary>
/// Reads bytes from a stream
/// </summary>
/// <param name="input">The input stream</param>
/// <param name="start">The starting position</param>
/// <param name="count">The number of bytes to read, or null to read to the end.</param>
private async Task<byte[]> ReadBytes(Stream input, int start, int? count)
{
if (start > 0)
{
input.Position = start;
}
if (count == null)
{
byte[] buffer = new byte[16 * 1024];
using (MemoryStream ms = new MemoryStream())
{
int read;
while ((read = await input.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await ms.WriteAsync(buffer, 0, read);
}
return ms.ToArray();
}
}
else
{
byte[] buffer = new byte[count.Value];
using (MemoryStream ms = new MemoryStream())
{
int read = await input.ReadAsync(buffer, 0, buffer.Length);
await ms.WriteAsync(buffer, 0, read);
return ms.ToArray();
}
}
}
}
}

View File

@ -4,10 +4,10 @@ using System.Reactive.Linq;
namespace MediaBrowser.Common.Net
{
public class HttpServer : IObservable<RequestContext>, IDisposable
public class HttpServer : IObservable<HttpListenerContext>, IDisposable
{
private readonly HttpListener listener;
private readonly IObservable<RequestContext> stream;
private readonly IObservable<HttpListenerContext> stream;
public HttpServer(string url)
{
@ -17,12 +17,11 @@ namespace MediaBrowser.Common.Net
stream = ObservableHttpContext();
}
private IObservable<RequestContext> ObservableHttpContext()
private IObservable<HttpListenerContext> ObservableHttpContext()
{
return Observable.Create<RequestContext>(obs =>
return Observable.Create<HttpListenerContext>(obs =>
Observable.FromAsyncPattern<HttpListenerContext>(listener.BeginGetContext,
listener.EndGetContext)()
.Select(c => new RequestContext(c))
.Subscribe(obs))
.Repeat()
.Retry()
@ -34,7 +33,7 @@ namespace MediaBrowser.Common.Net
listener.Stop();
}
public IDisposable Subscribe(IObserver<RequestContext> observer)
public IDisposable Subscribe(IObserver<HttpListenerContext> observer)
{
return stream.Subscribe(observer);
}

View File

@ -1,103 +0,0 @@
using System;
using System.Linq;
using System.Net;
using MediaBrowser.Common.Logging;
using MediaBrowser.Common.Net.Handlers;
namespace MediaBrowser.Common.Net
{
public class RequestContext
{
public HttpListenerRequest Request { get; private set; }
public HttpListenerResponse Response { get; private set; }
public string LocalPath
{
get
{
return Request.Url.LocalPath;
}
}
public RequestContext(HttpListenerContext context)
{
Response = context.Response;
Request = context.Request;
}
public void Respond(BaseHandler handler)
{
Logger.LogInfo("Http Server received request at: " + Request.Url.ToString());
Logger.LogInfo("Http Headers: " + string.Join(",", Request.Headers.AllKeys.Select(k => k + "=" + Request.Headers[k])));
Response.AddHeader("Access-Control-Allow-Origin", "*");
Response.KeepAlive = true;
foreach (var header in handler.Headers)
{
Response.AddHeader(header.Key, header.Value);
}
int statusCode = handler.StatusCode;
Response.ContentType = handler.ContentType;
TimeSpan cacheDuration = handler.CacheDuration;
if (Request.Headers.AllKeys.Contains("If-Modified-Since"))
{
DateTime ifModifiedSince;
if (DateTime.TryParse(Request.Headers["If-Modified-Since"].Replace(" GMT", string.Empty), out ifModifiedSince))
{
// If the cache hasn't expired yet just return a 304
if (IsCacheValid(ifModifiedSince, cacheDuration, handler.LastDateModified))
{
statusCode = 304;
}
}
}
Response.StatusCode = statusCode;
if (statusCode == 200 || statusCode == 206)
{
handler.WriteStream(Response.OutputStream);
}
else
{
Response.SendChunked = false;
Response.OutputStream.Dispose();
}
}
private bool IsCacheValid(DateTime ifModifiedSince, TimeSpan cacheDuration, DateTime? dateModified)
{
if (dateModified.HasValue)
{
DateTime lastModified = NormalizeDateForComparison(dateModified.Value);
ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
return lastModified <= ifModifiedSince;
}
DateTime cacheExpirationDate = ifModifiedSince.Add(cacheDuration);
if (DateTime.Now < cacheExpirationDate)
{
return true;
}
return false;
}
/// <summary>
/// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
/// </summary>
private DateTime NormalizeDateForComparison(DateTime date)
{
return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second);
}
}
}