Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions API/Protocol/IRequestTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ public interface IRequestTarget
PathSegment? Current { get; }

bool IsLast { get; }

bool HasTrailingSlash { get; }

void Advance(int segments = 1);

ReadOnlyMemory<byte>? Next(int offset);
PathSegment? Next(int offset);

IRequestTarget CopyAndAppend(ReadOnlyMemory<byte> suffix);

string AsString(bool decode = true, bool remainingOnly = false);

Expand Down
33 changes: 33 additions & 0 deletions Benchmarks/Benchmarks/Compression/PreCompressedBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using BenchmarkDotNet.Attributes;

using GenHTTP.Benchmarks.Infrastructure;

using GenHTTP.Modules.Compression;
using GenHTTP.Modules.Compression.Algorithms;
using GenHTTP.Modules.IO;

namespace GenHTTP.Benchmarks.Benchmarks.Compression;

[MemoryDiagnoser]
public class PreCompressedBenchmark
{
private readonly BenchmarkContext _context = CreateContext();

[Benchmark]
public ValueTask BenchmarkPreCompressedStaticFiles() => _context.Execute();

private static BenchmarkContext CreateContext()
{
var tree = ResourceTree.FromDirectory("./Resources/");

var handler = PreCompressedResources.From(tree)
.Add(new BrotliAlgorithm())
.Add(CompressedContent.Default());

var request = "GET /file.js HTTP/1.1\r\nHost: localhost:8080\r\nAccept-Encoding: br;q=1, gzip;q=0.8\r\n\r\n";

return new(request, handler.Build());
}

}

9 changes: 7 additions & 2 deletions Benchmarks/GenHTTP.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,14 @@
</ItemGroup>

<ItemGroup>
<None Update="Resources\file.js">
<None Remove="Resources\file.js.br" />
<Content Include="Resources\file.js.br">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</Content>
<None Remove="Resources\file.js" />
<Content Include="Resources\file.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
6 changes: 3 additions & 3 deletions Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using BenchmarkDotNet.Running;

using GenHTTP.Benchmarks.Benchmarks.IO;
using GenHTTP.Benchmarks.Benchmarks.Compression;

BenchmarkRunner.Run<StaticFileBenchmark>();
BenchmarkRunner.Run<PreCompressedBenchmark>();

// await new StaticFileBenchmark().BenchmarkStaticFile();
// await new PreCompressedBenchmark().BenchmarkPreCompressedStaticFiles();
Binary file added Benchmarks/Resources/file.js.br
Binary file not shown.
1 change: 0 additions & 1 deletion Engine/Internal/Protocol/ClientHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using GenHTTP.Engine.Shared.Types;
using Glyph11;
using Glyph11.Parser;
using Glyph11.Parser.Hardened;
using Glyph11.Parser.UltraHardened;
using Glyph11.Protocol;
using StringContent = GenHTTP.Modules.IO.Strings.StringContent;
Expand Down Expand Up @@ -61,7 +60,7 @@
context.Server.Companion?.OnServerError(ServerErrorScope.ClientConnection, connection.GetAddress(), e);
}

try

Check warning on line 63 in Engine/Internal/Protocol/ClientHandler.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Combine this 'try' with the one starting on line 54.

Check warning on line 63 in Engine/Internal/Protocol/ClientHandler.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Combine this 'try' with the one starting on line 54.
{
connection.Shutdown(SocketShutdown.Both);

Expand Down Expand Up @@ -193,7 +192,7 @@

internal async ValueTask<Connection> HandleRequestAsync(Request request)
{
// request.SetConnection(Server, EndPoint, Connection.GetAddress(), ClientCertificate);

Check warning on line 195 in Engine/Internal/Protocol/ClientHandler.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Remove this commented out code.

Check warning on line 195 in Engine/Internal/Protocol/ClientHandler.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Remove this commented out code.

var header = request.Header;

Expand Down
47 changes: 43 additions & 4 deletions Engine/Shared/Types/RequestTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public void Apply(ReadOnlyMemory<byte> path)
MoveNext();
}

public ReadOnlyMemory<byte>? Next(int offset)
public PathSegment? Next(int offset)
{
if (Current == null)
{
Expand All @@ -62,7 +62,7 @@ public void Apply(ReadOnlyMemory<byte> path)

if (offset == 0)
{
return Current.Value.Value;
return Current;
}

var span = _path.Span;
Expand All @@ -86,7 +86,7 @@ public void Apply(ReadOnlyMemory<byte> path)

if (skip == offset)
{
return idx < 0 ? _path.Slice(start, length - start) : _path.Slice(start, idx);
return new(idx < 0 ? _path.Slice(start, length - start) : _path.Slice(start, idx));
}

i += idx < 0 ? (length - start) : idx;
Expand Down Expand Up @@ -143,6 +143,45 @@ private void MoveNext()
}
}

public IRequestTarget CopyAndAppend(ReadOnlyMemory<byte> suffix)
{
var original = _path;

var buffer = new byte[original.Length + suffix.Length];

original.Span.CopyTo(buffer);
suffix.Span.CopyTo(buffer.AsSpan(original.Length));

var combined = new ReadOnlyMemory<byte>(buffer);

var copy = new RequestTarget
{
_path = combined,
_offset = _offset,
_segmentStart = _segmentStart
};

if (Current != null)
{
var span = combined.Span;

var relative = span[_segmentStart..].IndexOf((byte)'/');

if (relative < 0)
{
copy._offset = combined.Length;
copy.Current = new PathSegment(combined[_segmentStart..]);
}
else
{
copy._offset = _segmentStart + relative;
copy.Current = new PathSegment(combined.Slice(_segmentStart, relative));
}
}

return copy;
}

public string AsString(bool decode = true, bool remainingOnly = false)
{
ReadOnlyMemory<byte> slice;
Expand All @@ -162,7 +201,7 @@ public string AsString(bool decode = true, bool remainingOnly = false)
}

var stringPath = Encoding.ASCII.GetString(slice.Span);

if (decode && stringPath.Contains('%'))
{
return Uri.UnescapeDataString(stringPath);
Expand Down
43 changes: 43 additions & 0 deletions Modules/Compression/PreCompressedResources.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using GenHTTP.Api.Content.IO;
using GenHTTP.Api.Infrastructure;

using GenHTTP.Modules.Compression.PreCompression;

namespace GenHTTP.Modules.Compression;

/// <summary>
/// Allows to serve static resources that already are pre-compressed by
/// an external system.
/// </summary>
/// <remarks>
/// Expects compressed files to be stored side-by-side with the non-compressed
/// files (so for example "file.js" and "file.br.js").
///
/// By default, no algorithms are configured for a newly created handler, so
/// you need to register them by yourself. As the supported algorithms are
/// controlled by the external system pre-compressing the files, this is
/// more efficient than looping through all compression algorithms supported
/// by GenHTTP.
/// </remarks>
public static class PreCompressedResources
{

/// <summary>
/// Creates a new handler that will look for existing, pre-compressed files
/// based on the client preferences and serve them if suitable.
/// </summary>
/// <param name="tree">The tree to be served</param>
/// <returns>The newly created handler builder</returns>
public static PreCompressedResourceHandlerBuilder From(IBuilder<IResourceTree> tree)
=> From(tree.Build());

/// <summary>
/// Creates a new handler that will look for existing, pre-compressed files
/// based on the client preferences and serve them if suitable.
/// </summary>
/// <param name="tree">The tree to be served</param>
/// <returns>The newly created handler builder</returns>
public static PreCompressedResourceHandlerBuilder From(IResourceTree tree)
=> new(tree);

}
106 changes: 106 additions & 0 deletions Modules/Compression/PreCompression/PreCompressedResourceHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using GenHTTP.Api.Content;
using GenHTTP.Api.Content.IO;
using GenHTTP.Api.Protocol;

using GenHTTP.Modules.Compression.Providers;
using GenHTTP.Modules.IO;
using GenHTTP.Modules.IO.Streaming;

namespace GenHTTP.Modules.Compression.PreCompression;

public sealed class PreCompressedResourceHandler : IHandler
{
private readonly IResourceTree _tree;

private readonly IHandler _regular;

private readonly List<SupportedCompression> _algorithms;

public PreCompressedResourceHandler(IResourceTree tree, List<ICompressionAlgorithm> algorithms, char separator)
{
_tree = tree;

_regular = Resources.From(tree).Build();

_algorithms = algorithms.Select(a =>
{
var extension = new byte[a.Name.Value.Length + 1];
extension[0] = (byte)separator;
a.Name.Value.Span.CopyTo(extension.AsSpan(1));

return new SupportedCompression(a, extension);
})
.OrderByDescending(a => (int)a.Algorithm.Priority)
.ToList();
}

public ValueTask PrepareAsync() => _regular.PrepareAsync();

public async ValueTask<IResponse?> HandleAsync(IRequest request)
{
var target = request.Header.Target;

var file = GetFileName(target);

if (file is null)
{
return null;
}

var acceptEncodingHeader = request.Header.Headers.GetEntry(KnownHeaders.AcceptEncoding);

if (acceptEncodingHeader != null)
{
var requested = AcceptEncodingHeader.ParseSupported(acceptEncodingHeader.Value.Span);

foreach (var supported in _algorithms)
{
if (requested.Contains(supported.Algorithm.Name))
{
var newTarget = target.CopyAndAppend(supported.Extension);

var (_, resource) = await _tree.FindAsync(newTarget);

if (resource is not null)
{
var contentType = file.GuessContentType() ?? ContentType.ApplicationOctetStream;
var content = new ResourceContent(resource, contentType, supported.Algorithm.Name.Value);

return request.Respond()
.Content(content)
.Build();
}
}
}
}

return await _regular.HandleAsync(request);
}

private static string? GetFileName(IRequestTarget target)
{
if (target.HasTrailingSlash)
{
return null;
}

var offset = 0;

PathSegment? current = null;

while (true)
{
var next = target.Next(offset++);

if (next == null)
{
break;
}

current = next.Value;
}

return current?.Decode();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using GenHTTP.Api.Content;
using GenHTTP.Api.Content.IO;

namespace GenHTTP.Modules.Compression.PreCompression;

public sealed class PreCompressedResourceHandlerBuilder : IHandlerBuilder<PreCompressedResourceHandlerBuilder>
{
private readonly List<IConcernBuilder> _concerns = [];

private readonly List<ICompressionAlgorithm> _algorithms = [];

private readonly IResourceTree _source;

private char _separator = '.';

public PreCompressedResourceHandlerBuilder(IResourceTree source)
{
_source = source;
}

/// <summary>
/// Sets the separator used to build the paths to the compressed files. Defaults to '.'.
/// </summary>
/// <param name="separator">The separator used to built file paths</param>
/// <example>
/// If your files are stored like "file.js+br", you can set this to '+'.
/// </example>
public PreCompressedResourceHandlerBuilder Separator(char separator)
{
_separator = separator;
return this;
}

/// <summary>
/// Registers a algorithm used to search for file names.
/// </summary>
/// <param name="algorithm">The algorithm to use for searching</param>
/// <remarks>
/// The handler will not use the compression or decompression features of the algorithm,
/// but use the priority field to prefer algorithms with better compression ratios.
/// </remarks>
public PreCompressedResourceHandlerBuilder Add(ICompressionAlgorithm algorithm)
{
_algorithms.Add(algorithm);
return this;
}

public PreCompressedResourceHandlerBuilder Add(IConcernBuilder concern)
{
_concerns.Add(concern);
return this;
}

public IHandler Build() => Concerns.Chain(_concerns, new PreCompressedResourceHandler(_source, _algorithms, _separator));

}
12 changes: 12 additions & 0 deletions Modules/Compression/PreCompression/SupportedCompression.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using GenHTTP.Api.Content.IO;

namespace GenHTTP.Modules.Compression.PreCompression;

public readonly struct SupportedCompression(ICompressionAlgorithm algorithm, ReadOnlyMemory<byte> extension)
{

public ICompressionAlgorithm Algorithm => algorithm;

public ReadOnlyMemory<byte> Extension => extension;

}
Loading
Loading