Created
November 10, 2014 08:22
-
-
Save danbarua/0fd9fcad5ec85e0c02dc to your computer and use it in GitHub Desktop.
206 Partial Content responses for Nancy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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