#108 Have response compression replace IHttpSendFileFeature

This commit is contained in:
Chris R 2016-10-05 16:00:12 -07:00 committed by GitHub
parent 1db5a9e58f
commit cd833a7d63
9 changed files with 273 additions and 10 deletions

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@ -26,6 +27,15 @@ namespace ResponseCompressionSample
{
app.UseResponseCompression();
app.Map("/testfile1kb.txt", fileApp =>
{
fileApp.Run(context =>
{
context.Response.ContentType = "text/plain";
return context.Response.SendFileAsync("testfile1kb.txt");
});
});
app.Map("/trickle", trickleApp =>
{
trickleApp.Run(async context =>
@ -57,6 +67,7 @@ namespace ResponseCompressionSample
{
options.UseConnectionLogging();
})
// .UseWebListener()
.ConfigureLogging(factory =>
{
factory.AddConsole(LogLevel.Debug);

View File

@ -2,9 +2,13 @@
"dependencies": {
"Microsoft.AspNetCore.ResponseCompression": "0.1.0-*",
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0-*",
"Microsoft.AspNetCore.Server.WebListener": "1.1.0-*",
"Microsoft.Extensions.Logging.Console": "1.1.0-*"
},
"buildOptions": {
"copyToOutput": [
"testfile1kb.txt"
],
"emitEntryPoint": true
},
"frameworks": {

View File

@ -0,0 +1 @@
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

View File

@ -6,6 +6,7 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Net.Http.Headers;
@ -14,30 +15,27 @@ namespace Microsoft.AspNetCore.ResponseCompression
/// <summary>
/// Stream wrapper that create specific compression stream only if necessary.
/// </summary>
internal class BodyWrapperStream : Stream, IHttpBufferingFeature
internal class BodyWrapperStream : Stream, IHttpBufferingFeature, IHttpSendFileFeature
{
private readonly HttpResponse _response;
private readonly Stream _bodyOriginalStream;
private readonly IResponseCompressionProvider _provider;
private readonly ICompressionProvider _compressionProvider;
private readonly IHttpBufferingFeature _innerBufferFeature;
private readonly IHttpSendFileFeature _innerSendFileFeature;
private bool _compressionChecked = false;
private Stream _compressionStream = null;
internal BodyWrapperStream(HttpResponse response, Stream bodyOriginalStream, IResponseCompressionProvider provider, ICompressionProvider compressionProvider,
IHttpBufferingFeature innerBufferFeature)
IHttpBufferingFeature innerBufferFeature, IHttpSendFileFeature innerSendFileFeature)
{
_response = response;
_bodyOriginalStream = bodyOriginalStream;
_provider = provider;
_compressionProvider = compressionProvider;
_innerBufferFeature = innerBufferFeature;
_innerSendFileFeature = innerSendFileFeature;
}
protected override void Dispose(bool disposing)
@ -216,5 +214,50 @@ namespace Microsoft.AspNetCore.ResponseCompression
_innerBufferFeature?.DisableResponseBuffering();
}
// The IHttpSendFileFeature feature will only be registered if _innerSendFileFeature exists.
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
OnWrite();
if (_compressionStream != null)
{
return InnerSendFileAsync(path, offset, count, cancellation);
}
return _innerSendFileFeature.SendFileAsync(path, offset, count, cancellation);
}
private async Task InnerSendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
cancellation.ThrowIfCancellationRequested();
var fileInfo = new FileInfo(path);
if (offset < 0 || offset > fileInfo.Length)
{
throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
}
if (count.HasValue &&
(count.Value < 0 || count.Value > fileInfo.Length - offset))
{
throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
}
int bufferSize = 1024 * 16;
var fileStream = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
bufferSize: bufferSize,
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
using (fileStream)
{
fileStream.Seek(offset, SeekOrigin.Begin);
await StreamCopyOperation.CopyToAsync(fileStream, _compressionStream, count, cancellation);
}
}
}
}

View File

@ -68,10 +68,16 @@ namespace Microsoft.AspNetCore.ResponseCompression
var bodyStream = context.Response.Body;
var originalBufferFeature = context.Features.Get<IHttpBufferingFeature>();
var originalSendFileFeature = context.Features.Get<IHttpSendFileFeature>();
var bodyWrapperStream = new BodyWrapperStream(context.Response, bodyStream, _provider, compressionProvider, originalBufferFeature);
var bodyWrapperStream = new BodyWrapperStream(context.Response, bodyStream, _provider, compressionProvider,
originalBufferFeature, originalSendFileFeature);
context.Response.Body = bodyWrapperStream;
context.Features.Set<IHttpBufferingFeature>(bodyWrapperStream);
if (originalSendFileFeature != null)
{
context.Features.Set<IHttpSendFileFeature>(bodyWrapperStream);
}
try
{
@ -84,6 +90,10 @@ namespace Microsoft.AspNetCore.ResponseCompression
{
context.Response.Body = bodyStream;
context.Features.Set(originalBufferFeature);
if (originalSendFileFeature != null)
{
context.Features.Set(originalSendFileFeature);
}
}
}
}

View File

@ -16,9 +16,8 @@
]
},
"dependencies": {
"Microsoft.AspNetCore.Http.Abstractions": "1.1.0-*",
"Microsoft.AspNetCore.Http.Extensions": "1.1.0-*",
"Microsoft.Extensions.Options": "1.1.0-*",
"Microsoft.Net.Http.Headers": "1.1.0-*",
"NETStandard.Library": "1.6.1-*"
},
"frameworks": {

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@ -579,6 +580,168 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
}
}
[Fact]
public async Task SendFileAsync_OnlySetIfFeatureAlreadyExists()
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddResponseCompression(TextPlain);
})
.Configure(app =>
{
app.UseResponseCompression();
app.Run(context =>
{
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
context.Response.ContentType = TextPlain;
context.Response.ContentLength = 1024;
var sendFile = context.Features.Get<IHttpSendFileFeature>();
Assert.Null(sendFile);
return Task.FromResult(0);
});
});
var server = new TestServer(builder);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "");
request.Headers.AcceptEncoding.ParseAdd("gzip");
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
}
[Fact]
public async Task SendFileAsync_DifferentContentType_NotBypassed()
{
FakeSendFileFeature fakeSendFile = null;
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddResponseCompression(TextPlain);
})
.Configure(app =>
{
app.Use((context, next) =>
{
fakeSendFile = new FakeSendFileFeature(context.Response.Body);
context.Features.Set<IHttpSendFileFeature>(fakeSendFile);
return next();
});
app.UseResponseCompression();
app.Run(context =>
{
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
context.Response.ContentType = "custom/type";
context.Response.ContentLength = 1024;
var sendFile = context.Features.Get<IHttpSendFileFeature>();
Assert.NotNull(sendFile);
return sendFile.SendFileAsync("testfile1kb.txt", 0, null, CancellationToken.None);
});
});
var server = new TestServer(builder);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "");
request.Headers.AcceptEncoding.ParseAdd("gzip");
var response = await client.SendAsync(request);
CheckResponseNotCompressed(response, expectedBodyLength: 1024);
Assert.True(fakeSendFile.Invoked);
}
[Fact]
public async Task SendFileAsync_FirstWrite_CompressesAndFlushes()
{
FakeSendFileFeature fakeSendFile = null;
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddResponseCompression(TextPlain);
})
.Configure(app =>
{
app.Use((context, next) =>
{
fakeSendFile = new FakeSendFileFeature(context.Response.Body);
context.Features.Set<IHttpSendFileFeature>(fakeSendFile);
return next();
});
app.UseResponseCompression();
app.Run(context =>
{
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
context.Response.ContentType = TextPlain;
context.Response.ContentLength = 1024;
var sendFile = context.Features.Get<IHttpSendFileFeature>();
Assert.NotNull(sendFile);
return sendFile.SendFileAsync("testfile1kb.txt", 0, null, CancellationToken.None);
});
});
var server = new TestServer(builder);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "");
request.Headers.AcceptEncoding.ParseAdd("gzip");
var response = await client.SendAsync(request);
CheckResponseCompressed(response, expectedBodyLength: 34);
Assert.False(fakeSendFile.Invoked);
}
[Fact]
public async Task SendFileAsync_AfterFirstWrite_CompressesAndFlushes()
{
FakeSendFileFeature fakeSendFile = null;
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddResponseCompression(TextPlain);
})
.Configure(app =>
{
app.Use((context, next) =>
{
fakeSendFile = new FakeSendFileFeature(context.Response.Body);
context.Features.Set<IHttpSendFileFeature>(fakeSendFile);
return next();
});
app.UseResponseCompression();
app.Run(async context =>
{
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
context.Response.ContentType = TextPlain;
var sendFile = context.Features.Get<IHttpSendFileFeature>();
Assert.NotNull(sendFile);
await context.Response.WriteAsync(new string('a', 100));
await sendFile.SendFileAsync("testfile1kb.txt", 0, null, CancellationToken.None);
});
});
var server = new TestServer(builder);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "");
request.Headers.AcceptEncoding.ParseAdd("gzip");
var response = await client.SendAsync(request);
CheckResponseCompressed(response, expectedBodyLength: 40);
Assert.False(fakeSendFile.Invoked);
}
private Task<HttpResponseMessage> InvokeMiddleware(int uncompressedBodyLength, string[] requestAcceptEncodings, string responseType, Action<HttpResponse> addResponseAction = null)
{
var builder = new WebHostBuilder()
@ -593,6 +756,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
{
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
context.Response.ContentType = responseType;
Assert.Null(context.Features.Get<IHttpSendFileFeature>());
if (addResponseAction != null)
{
addResponseAction(context.Response);
@ -628,5 +792,32 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
Assert.Empty(response.Content.Headers.ContentEncoding);
Assert.Equal(expectedBodyLength, response.Content.Headers.ContentLength);
}
private class FakeSendFileFeature : IHttpSendFileFeature
{
private readonly Stream _innerBody;
public FakeSendFileFeature(Stream innerBody)
{
_innerBody = innerBody;
}
public bool Invoked { get; set; }
public async Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
// This implementation should only be delegated to if compression is disabled.
Invoked = true;
using (var file = new FileStream(path, FileMode.Open))
{
file.Seek(offset, SeekOrigin.Begin);
if (count.HasValue)
{
throw new NotImplementedException("Not implemented for testing");
}
await file.CopyToAsync(_innerBody, 81920, cancellation);
}
}
}
}
}

View File

@ -1,6 +1,9 @@
{
"version": "1.1.0-*",
"buildOptions": {
"copyToOutput": [
"testfile1kb.txt"
],
"warningsAsErrors": true
},
"dependencies": {

View File

@ -0,0 +1 @@
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa