#108 Have response compression replace IHttpSendFileFeature
This commit is contained in:
parent
1db5a9e58f
commit
cd833a7d63
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
{
|
||||
"version": "1.1.0-*",
|
||||
"buildOptions": {
|
||||
"copyToOutput": [
|
||||
"testfile1kb.txt"
|
||||
],
|
||||
"warningsAsErrors": true
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
Loading…
Reference in New Issue