Last active
November 8, 2018 20:31
-
-
Save alfeg/668d2547ee8b8a6241c4dc1abfa39c4e to your computer and use it in GitHub Desktop.
PartialFileContentResult to return 206 partial responses from MVC 5
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
public class PartialFileContentResult : ActionResult | |
{ | |
private readonly Stream stream; | |
private readonly string contentType; | |
private readonly CancellationToken token; | |
/// <summary>Initializes a new instance of the <see cref="T:System.Web.Mvc.FileContentResult" /> class by using the specified file contents and content type.</summary> | |
/// <param name="fileContents">The byte array to send to the response.</param> | |
/// <param name="contentType">The content type to use for the response.</param> | |
/// <exception cref="T:System.ArgumentNullException">The <paramref name="fileContents" /> parameter is null.</exception> | |
public PartialFileContentResult(Stream stream, string contentType, CancellationToken token) | |
{ | |
this.stream = stream; | |
this.contentType = contentType; | |
this.token = token; | |
} | |
public override void ExecuteResult(ControllerContext ctx) | |
{ | |
var req = ctx.HttpContext.Request; | |
var rangeHeader = req.Headers["Range"]; | |
if (rangeHeader != null) | |
{ | |
if (RangeHelpers.TryParseRanges(rangeHeader, out var ranges)) | |
{ | |
var byteRange = new ByteRangeStreamContent(stream, new RangeHeaderValue(ranges[0].from, ranges[0].to),MediaTypeHeaderValue.Parse(contentType)); | |
var response = ctx.HttpContext.Response; | |
response.StatusCode = 206; | |
response.ContentType = byteRange.Headers.ContentType.ToString(); | |
response.AddHeader("Content-Length", byteRange.Headers.ContentLength.ToString()); | |
response.AddHeader("Content-Range", byteRange.Headers.ContentRange.ToString()); | |
using (this.stream) | |
{ | |
byte[] buffer = new byte[4096]; | |
stream.Seek(ranges[0].Item1 ?? 0, SeekOrigin.Begin); | |
while (true) | |
{ | |
token.ThrowIfCancellationRequested(); | |
if (!HttpContext.Current.Response.IsClientConnected) | |
{ | |
return; | |
} | |
long bufferSize = 4096; | |
if (ranges[0].to != null) | |
{ | |
bufferSize = Math.Min(bufferSize, ranges[0].to.Value - stream.Position); | |
} | |
int count = this.stream.Read(buffer, 0, (int) bufferSize); | |
if (count != 0) | |
response.OutputStream.Write(buffer, 0, count); | |
else | |
break; | |
} | |
} | |
return; | |
} | |
} | |
new FileStreamResult(stream, contentType).ExecuteResult(ctx); | |
} | |
} |
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
internal static class RangeHelpers | |
{ | |
// Examples: | |
// bytes=0-499 | |
// bytes=500- | |
// bytes=-500 | |
// bytes=0-0,-1 | |
// bytes=500-600,601-999 | |
// Any individual bad range fails the whole parse and the header should be ignored. | |
internal static bool TryParseRanges(string rangeHeader, out IList<(long? from, long? to)> parsedRanges) | |
{ | |
parsedRanges = null; | |
if (string.IsNullOrWhiteSpace(rangeHeader) | |
|| !rangeHeader.StartsWith("bytes=", StringComparison.OrdinalIgnoreCase)) | |
{ | |
return false; | |
} | |
string[] subRanges = rangeHeader.Substring("bytes=".Length).Replace(" ", string.Empty).Split(','); | |
var ranges = new List<(long? from, long? to)>(); | |
for (int i = 0; i < subRanges.Length; i++) | |
{ | |
long? first = null, second = null; | |
string subRange = subRanges[i]; | |
int dashIndex = subRange.IndexOf('-'); | |
if (dashIndex < 0) | |
{ | |
return false; | |
} | |
else if (dashIndex == 0) | |
{ | |
// -500 | |
string remainder = subRange.Substring(1); | |
if (!TryParseLong(remainder, out second)) | |
{ | |
return false; | |
} | |
} | |
else if (dashIndex == (subRange.Length - 1)) | |
{ | |
// 500- | |
string remainder = subRange.Substring(0, subRange.Length - 1); | |
if (!TryParseLong(remainder, out first)) | |
{ | |
return false; | |
} | |
} | |
else | |
{ | |
// 0-499 | |
string firstString = subRange.Substring(0, dashIndex); | |
string secondString = subRange.Substring(dashIndex + 1, subRange.Length - dashIndex - 1); | |
if (!TryParseLong(firstString, out first) || !TryParseLong(secondString, out second) | |
|| first.Value > second.Value) | |
{ | |
return false; | |
} | |
} | |
ranges.Add((first, second)); | |
} | |
if (ranges.Count > 0) | |
{ | |
parsedRanges = ranges; | |
return true; | |
} | |
return false; | |
} | |
private static bool TryParseLong(string input, out long? result) | |
{ | |
if (!string.IsNullOrWhiteSpace(input) | |
&& int.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out var temp)) | |
{ | |
result = temp; | |
return true; | |
} | |
result = null; | |
return false; | |
} | |
// 14.35.1 Byte Ranges - If a syntactically valid byte-range-set includes at least one byte-range-spec whose | |
// first-byte-pos is less than the current length of the entity-body, or at least one suffix-byte-range-spec | |
// with a non-zero suffix-length, then the byte-range-set is satisfiable. | |
// Adjusts ranges to be absolute and within bounds. | |
internal static IList<Tuple<long, long>> NormalizeRanges(IList<Tuple<long?, long?>> ranges, long length) | |
{ | |
IList<Tuple<long, long>> normalizedRanges = new List<Tuple<long, long>>(ranges.Count); | |
for (int i = 0; i < ranges.Count; i++) | |
{ | |
Tuple<long?, long?> range = ranges[i]; | |
long? start = range.Item1, end = range.Item2; | |
// X-[Y] | |
if (start.HasValue) | |
{ | |
if (start.Value >= length) | |
{ | |
// Not satisfiable, skip/discard. | |
continue; | |
} | |
if (!end.HasValue || end.Value >= length) | |
{ | |
end = length - 1; | |
} | |
} | |
else | |
{ | |
// suffix range "-X" e.g. the last X bytes, resolve | |
if (end.Value == 0) | |
{ | |
// Not satisfiable, skip/discard. | |
continue; | |
} | |
long bytes = Math.Min(end.Value, length); | |
start = length - bytes; | |
end = start + bytes - 1; | |
} | |
normalizedRanges.Add(new Tuple<long, long>(start.Value, end.Value)); | |
} | |
return normalizedRanges; | |
} | |
} |
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
new PartialFileContentResult(fileStream, "video/mp4", HttpContext.Response.ClientDisconnectedToken) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment