-
-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for range requests (closes #124)
- Loading branch information
1 parent
a7d8a7e
commit a460793
Showing
13 changed files
with
717 additions
and
14 deletions.
There are no files selected for viewing
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
using GenHTTP.Api.Infrastructure; | ||
|
||
namespace GenHTTP.Modules.IO | ||
{ | ||
|
||
public static class Extensions | ||
{ | ||
|
||
/// <summary> | ||
/// Configures the server to respond with partial responses if | ||
/// requested by the client. | ||
/// </summary> | ||
/// <param name="host">The host to add the feature to</param> | ||
public static IServerHost RangeSupport(this IServerHost host) | ||
{ | ||
host.Add(IO.RangeSupport.Create()); | ||
return host; | ||
} | ||
|
||
} | ||
|
||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
using GenHTTP.Api.Content; | ||
|
||
using GenHTTP.Modules.IO.Ranges; | ||
|
||
namespace GenHTTP.Modules.IO | ||
{ | ||
|
||
public static class RangeSupport | ||
{ | ||
|
||
public static RangeSupportConcernBuilder Create() => new(); | ||
|
||
#region Extensions | ||
|
||
/// <summary> | ||
/// Adds range support as a concern to the given builder. | ||
/// </summary> | ||
public static T AddRangeSupport<T>(this T builder) where T : IHandlerBuilder<T> | ||
{ | ||
builder.Add(Create()); | ||
return builder; | ||
} | ||
|
||
#endregion | ||
|
||
} | ||
|
||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Text.RegularExpressions; | ||
using System.Threading.Tasks; | ||
|
||
using GenHTTP.Api.Content; | ||
using GenHTTP.Api.Protocol; | ||
|
||
namespace GenHTTP.Modules.IO.Ranges | ||
{ | ||
|
||
public class RangeSupportConcern : IConcern | ||
{ | ||
private static readonly Regex _PATTERN = new(@"^\s*bytes\s*=\s*([0-9]*)-([0-9]*)\s*$", RegexOptions.IgnoreCase | RegexOptions.Compiled); | ||
|
||
#region Get-/Setters | ||
|
||
public IHandler Content { get; } | ||
|
||
public IHandler Parent { get; } | ||
|
||
#endregion | ||
|
||
#region Initialization | ||
|
||
public RangeSupportConcern(IHandler parent, Func<IHandler, IHandler> contentFactory) | ||
{ | ||
Parent = parent; | ||
Content = contentFactory(this); | ||
} | ||
|
||
#endregion | ||
|
||
#region Functionality | ||
|
||
public ValueTask PrepareAsync() => ValueTask.CompletedTask; | ||
|
||
public IEnumerable<ContentElement> GetContent(IRequest request) => Content.GetContent(request); | ||
|
||
public async ValueTask<IResponse?> HandleAsync(IRequest request) | ||
{ | ||
if (request.Method == RequestMethod.GET || request.Method == RequestMethod.HEAD) | ||
{ | ||
var response = await Content.HandleAsync(request); | ||
|
||
if (response != null) | ||
{ | ||
if (response.Status == ResponseStatus.OK) | ||
{ | ||
var length = response.ContentLength; | ||
|
||
if (length != null) | ||
{ | ||
response["Accept-Ranges"] = "bytes"; | ||
|
||
if (request.Headers.TryGetValue("Range", out var requested)) | ||
{ | ||
var match = _PATTERN.Match(requested); | ||
|
||
if (match.Success) | ||
{ | ||
var startString = match.Groups[1].Value; | ||
var endString = match.Groups[2].Value; | ||
|
||
var start = (string.IsNullOrEmpty(startString)) ? null : (ulong?)ulong.Parse(startString); | ||
var end = (string.IsNullOrEmpty(endString)) ? null : (ulong?)ulong.Parse(endString); | ||
|
||
if ((start != null) || (end != null)) | ||
{ | ||
if (end == null) | ||
{ | ||
return GetRangeFromStart(request, response, length!.Value, start!.Value); | ||
} | ||
else if (start == null) | ||
{ | ||
return GetRangeFromEnd(request, response, length!.Value, end!.Value); | ||
} | ||
else | ||
{ | ||
return GetFullRange(request, response, length!.Value, start!.Value, end!.Value); | ||
} | ||
} | ||
} | ||
else | ||
{ | ||
return NotSatisfiable(request, length!.Value); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
return response; | ||
} | ||
else | ||
{ | ||
return await Content.HandleAsync(request); | ||
} | ||
} | ||
|
||
private static IResponse GetRangeFromStart(IRequest request, IResponse response, ulong length, ulong start) | ||
{ | ||
if (start > length) return NotSatisfiable(request, length); | ||
|
||
return GetRange(response, start, length - 1, length); | ||
} | ||
|
||
private static IResponse GetRangeFromEnd(IRequest request, IResponse response, ulong length, ulong end) | ||
{ | ||
if (end > length) return NotSatisfiable(request, length); | ||
|
||
return GetRange(response, length - end, length - 1, length); | ||
} | ||
|
||
private static IResponse GetFullRange(IRequest request, IResponse response, ulong length, ulong start, ulong end) | ||
{ | ||
if (start > end || end >= length) return NotSatisfiable(request, length); | ||
|
||
return GetRange(response, start, end, length); | ||
} | ||
|
||
private static IResponse GetRange(IResponse response, ulong actualStart, ulong actualEnd, ulong totalLength) | ||
{ | ||
response.Status = new(ResponseStatus.PartialContent); | ||
|
||
response["Content-Range"] = $"bytes {actualStart}-{actualEnd}/{totalLength}"; | ||
|
||
response.Content = new RangedContent(response.Content!, actualStart, actualEnd); | ||
response.ContentLength = actualEnd - actualStart + 1; | ||
|
||
return response; | ||
} | ||
|
||
private static IResponse NotSatisfiable(IRequest request, ulong totalLength) | ||
{ | ||
var content = Resource.FromString($"Requested length cannot be satisfied (available = {totalLength} bytes)") | ||
.Build(); | ||
|
||
return request.Respond() | ||
.Status(ResponseStatus.RequestedRangeNotSatisfiable) | ||
.Header("Content-Range", $"bytes */{totalLength}") | ||
.Content(content) | ||
.Build(); | ||
} | ||
|
||
#endregion | ||
|
||
} | ||
|
||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
using System; | ||
|
||
using GenHTTP.Api.Content; | ||
|
||
namespace GenHTTP.Modules.IO.Ranges | ||
{ | ||
|
||
public class RangeSupportConcernBuilder : IConcernBuilder | ||
{ | ||
|
||
#region Functionality | ||
|
||
public IConcern Build(IHandler parent, Func<IHandler, IHandler> contentFactory) | ||
{ | ||
return new RangeSupportConcern(parent, contentFactory); | ||
} | ||
|
||
#endregion | ||
|
||
} | ||
|
||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
using System.IO; | ||
using System.Threading.Tasks; | ||
|
||
using GenHTTP.Api.Protocol; | ||
|
||
namespace GenHTTP.Modules.IO.Ranges | ||
{ | ||
|
||
public class RangedContent : IResponseContent | ||
{ | ||
|
||
#region Get-/Setters | ||
|
||
public ulong? Length { get; } | ||
|
||
private IResponseContent Source { get; } | ||
|
||
private ulong Start { get; } | ||
|
||
private ulong End { get; } | ||
|
||
#endregion | ||
|
||
#region Initialization | ||
|
||
public RangedContent(IResponseContent source, ulong start, ulong end) | ||
{ | ||
Source = source; | ||
|
||
Start = start; | ||
End = end; | ||
|
||
Length = (End - Start); | ||
} | ||
|
||
#endregion | ||
|
||
#region Functionality | ||
|
||
public async ValueTask<ulong?> CalculateChecksumAsync() | ||
{ | ||
var checksum = await Source.CalculateChecksumAsync(); | ||
|
||
if (checksum != null) | ||
{ | ||
unchecked | ||
{ | ||
checksum = checksum * 17 + Start; | ||
checksum = checksum * 17 + End; | ||
} | ||
} | ||
|
||
return checksum; | ||
} | ||
|
||
public ValueTask WriteAsync(Stream target, uint bufferSize) => Source.WriteAsync(new RangedStream(target, Start, End), bufferSize); | ||
|
||
#endregion | ||
|
||
} | ||
|
||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
using System; | ||
using System.IO; | ||
|
||
namespace GenHTTP.Modules.IO.Ranges | ||
{ | ||
|
||
/// <summary> | ||
/// A stream that can be configured to just write a specified | ||
/// portion of the incoming data into the underlying stream. | ||
/// </summary> | ||
public class RangedStream : Stream | ||
{ | ||
|
||
#region Get-/Setters | ||
|
||
private Stream Target { get; } | ||
|
||
private long Start { get; } | ||
|
||
private long End { get; } | ||
|
||
public override bool CanRead => false; | ||
|
||
public override bool CanSeek => false; | ||
|
||
public override bool CanWrite => true; | ||
|
||
public override long Length => (End - Start); | ||
|
||
public override long Position { get; set; } | ||
|
||
#endregion | ||
|
||
#region Initialization | ||
|
||
/// <summary> | ||
/// Creates a ranged stream that writes the specified portion of | ||
/// data to the given target stream. | ||
/// </summary> | ||
/// <param name="target">The stream to write to</param> | ||
/// <param name="start">The zero based index of the starting position</param> | ||
/// <param name="end">The zero based index of the inclusive end position</param> | ||
public RangedStream(Stream target, ulong start, ulong end) | ||
{ | ||
Target = target; | ||
|
||
Start = (long)start; | ||
End = (long)end; | ||
} | ||
|
||
#endregion | ||
|
||
#region Functionality | ||
|
||
public override void Flush() => Target.Flush(); | ||
|
||
public override void Write(byte[] buffer, int offset, int count) | ||
{ | ||
if (Position > End) return; | ||
|
||
long actualOffset = offset; | ||
long actualCount = count; | ||
|
||
if (Position < Start) | ||
{ | ||
actualOffset += (int)(Start - Position); | ||
actualCount -= (int)(Start - Position); | ||
} | ||
|
||
if (Start + actualCount > End + 1) | ||
{ | ||
actualCount = Math.Min(End - Start + 1, actualCount); | ||
} | ||
|
||
if (actualOffset < buffer.Length) | ||
{ | ||
var toWrite = Math.Min(buffer.Length - actualOffset, actualCount); | ||
|
||
Target.Write(buffer, (int)actualOffset, (int)toWrite); | ||
} | ||
|
||
Position += count; | ||
} | ||
|
||
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); | ||
|
||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); | ||
|
||
public override void SetLength(long value) => throw new NotSupportedException(); | ||
|
||
#endregion | ||
|
||
} | ||
|
||
} |
Oops, something went wrong.