Skip to content

Instantly share code, notes, and snippets.

@danbarua
Created November 10, 2014 08:22
Show Gist options
  • Save danbarua/0fd9fcad5ec85e0c02dc to your computer and use it in GitHub Desktop.
Save danbarua/0fd9fcad5ec85e0c02dc to your computer and use it in GitHub Desktop.
206 Partial Content responses for Nancy
namespace Infrastructure
{
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using Nancy;
using Util;
public class PartialContentResponse : Response
{
private const string ContentLength = "Content-Length";
private const string AcceptRanges = "Accept-Ranges";
private const string ContentRange = "Content-Range";
private const string ContentDisposition = "Content-Disposition";
private readonly NancyContext context;
private readonly Stream source;
private readonly long contentLength;
private readonly long rangeStart;
private readonly long rangeEnd;
public PartialContentResponse(Func<Stream> source, string contentType, NancyContext context, bool forceDownload = false, string fileName = null)
{
if (forceDownload && fileName == null) throw new ArgumentNullException("fileName", "fileName must be provided when forceDownload is true.");
this.context = context;
this.ContentType = contentType;
this.StatusCode = HttpStatusCode.OK;
this.source = source();
this.contentLength = this.source.Length;
this.Headers[ContentLength] = this.contentLength.ToString(CultureInfo.InvariantCulture);
this.Headers[AcceptRanges] = "bytes";
if (forceDownload)
{
this.Headers[ContentDisposition] = "attachment; filename={0}".Fmt(fileName);
}
// rangeHeader should be of the format "bytes=0-" or "bytes=0-12345" or "bytes=123-456"
var rangeHeader = context.Request.Headers["Range"].FirstOrDefault();
if (!string.IsNullOrEmpty(rangeHeader) && rangeHeader.Contains("="))
{
var rangeParts = rangeHeader.SplitOnFirst("=")[1].SplitOnFirst("-");
this.rangeStart = long.Parse(rangeParts[0]);
this.rangeEnd = rangeParts.Length == 2 && !string.IsNullOrEmpty(rangeParts[1])
? int.Parse(rangeParts[1]) // the client requested a chunk
: this.contentLength - 1;
if (this.rangeStart < 0 || this.rangeEnd > this.contentLength - 1)
{
this.StatusCode = HttpStatusCode.RequestedRangeNotSatisfiable;
this.Contents = GetStringContents(string.Empty);
}
else
{
this.Headers[ContentRange] = "bytes {0}-{1}/{2}".Fmt(this.rangeStart, this.rangeEnd, this.contentLength);
this.StatusCode = HttpStatusCode.PartialContent;
this.Contents = this.GetResponseBodyDelegate(source);
}
}
else
{
this.rangeStart = 0;
this.rangeEnd = this.contentLength - 1;
this.Contents = this.GetResponseBodyDelegate(source);
}
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public override void Dispose()
{
if (this.source != null) this.source.Dispose();
}
private Action<Stream> GetResponseBodyDelegate(Func<Stream> sourceDelegate)
{
return stream =>
{
{
if (this.rangeStart == 0 && this.rangeEnd == this.contentLength - 1) this.source.CopyTo(stream);
else
{
if (!this.source.CanSeek)
throw new InvalidOperationException(
"Sending Range Responses requires a seekable stream eg. FileStream or MemoryStream");
var totalBytesToSend = this.rangeEnd - this.rangeStart + 1;
const int BufferSize = 0x1000;
var buffer = new byte[BufferSize];
var bytesRemaining = totalBytesToSend;
this.source.Seek(this.rangeStart, SeekOrigin.Begin);
while (bytesRemaining > 0)
{
var count = bytesRemaining <= buffer.Length
? this.source.Read(buffer, 0, (int)Math.Min(bytesRemaining, int.MaxValue))
: this.source.Read(buffer, 0, buffer.Length);
try
{
stream.Write(buffer, 0, count);
stream.Flush();
bytesRemaining -= count;
}
catch (Exception httpException)
{
/* in Asp.Net we can call HttpResponseBase.IsClientConnected
* to see if the client broke off the connection
* and avoid trying to flush the response stream.
* instead I'll swallow the exception that IIS throws in this situation
* and rethrow anything else.*/
if (httpException.Message
== "An error occurred while communicating with the remote host. The error code is 0x80070057.") return;
throw;
}
}
}
}
};
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment