Skip to content
This repository was archived by the owner on Nov 20, 2018. It is now read-only.
Closed
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
20 changes: 20 additions & 0 deletions src/Microsoft.AspNetCore.Http.Abstractions/HttpResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.FileProviders;

namespace Microsoft.AspNetCore.Http
{
Expand Down Expand Up @@ -103,5 +105,23 @@ public abstract class HttpResponse
/// <param name="location">The URL to redirect the client to.</param>
/// <param name="permanent"><c>True</c> if the redirect is permanent (301), otherwise <c>false</c> (302).</param>
public abstract void Redirect(string location, bool permanent);

/// <summary>
/// Sends the given file using the SendFile extension.
/// </summary>
/// <param name="file">The file to send.</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public abstract Task SendFileAsync(IFileInfo file, CancellationToken cancellationToken = default(CancellationToken));

/// <summary>
/// Sends the given file using the SendFile extension.
/// </summary>
/// <param name="file">The file to send.</param>
/// <param name="offset">The offset in the file.</param>
/// <param name="count">The number of types to send, or null to send the remainder of the file.</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public abstract Task SendFileAsync(IFileInfo file, long offset, long? count, CancellationToken cancellationToken = default(CancellationToken));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Http.Extensions
namespace Microsoft.AspNetCore.Http
{
// FYI: In most cases the source will be a FileStream and the destination will be to the network.
public static class StreamCopyOperation
Expand Down
7 changes: 5 additions & 2 deletions src/Microsoft.AspNetCore.Http.Abstractions/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
},
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "1.0.0-*",
"Microsoft.Extensions.FileProviders.Abstractions": "1.0.0-*",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HttpAbstractions picking up a dependency on FileProviders is pretty strange.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not just FileProviders, it's FileProviders.Abstractions. In my head, it made sense that one abstraction package depends on another 😄

"Microsoft.Extensions.ActivatorUtilities.Sources": {
"type": "build",
"version": "1.0.0-*"
},
"System.Text.Encodings.Web": "4.0.0-*"
"System.Text.Encodings.Web": "4.0.0-*",
"System.Buffers": "4.0.0-*"
},
"frameworks": {
"net451": {
Expand All @@ -32,7 +34,8 @@
"System.Net.WebSockets": "4.0.0-*",
"System.Reflection.TypeExtensions": "4.1.0-*",
"System.Runtime.InteropServices": "4.0.21-*",
"System.Security.Cryptography.X509Certificates": "4.0.0-*"
"System.Security.Cryptography.X509Certificates": "4.0.0-*",
"System.IO.FileSystem": "4.0.1-*"
}
}
}
Expand Down
104 changes: 0 additions & 104 deletions src/Microsoft.AspNetCore.Http.Extensions/SendFileResponseExtensions.cs

This file was deleted.

9 changes: 2 additions & 7 deletions src/Microsoft.AspNetCore.Http.Extensions/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,10 @@
},
"dependencies": {
"Microsoft.AspNetCore.Http.Abstractions": "1.0.0-*",
"Microsoft.Net.Http.Headers": "1.0.0-*",
"System.Buffers": "4.0.0-*"
"Microsoft.Net.Http.Headers": "1.0.0-*"
},
"frameworks": {
"net451": {},
"dotnet5.4": {
"dependencies": {
"System.IO.FileSystem": "4.0.1-*"
}
}
"dotnet5.4": { }
}
}
88 changes: 87 additions & 1 deletion src/Microsoft.AspNetCore.Http/DefaultHttpResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Features.Internal;
using Microsoft.Extensions.FileProviders;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.Http.Internal
Expand Down Expand Up @@ -37,7 +39,6 @@ public virtual void Uninitialize()

private IResponseCookiesFeature ResponseCookiesFeature =>
_features.Fetch(ref _features.Cache.Cookies, f => new ResponseCookiesFeature(f));


public override HttpContext HttpContext { get { return _context; } }

Expand Down Expand Up @@ -133,6 +134,91 @@ public override void Redirect(string location, bool permanent)
Headers[HeaderNames.Location] = location;
}

public override Task SendFileAsync(IFileInfo file, CancellationToken cancellationToken = default(CancellationToken))
{
if (file == null)
{
throw new ArgumentNullException(nameof(file));
}

return SendFileAsync(file, 0, null, cancellationToken);
}

/// <summary>
/// Sends the given file using the SendFile extension.
/// </summary>
/// <param name="file">The file to send.</param>
/// <param name="offset">The offset in the file.</param>
/// <param name="count">The number of types to send, or null to send the remainder of the file.</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public override async Task SendFileAsync(IFileInfo file, long offset, long? count, CancellationToken cancellationToken = default(CancellationToken))
{
if (file == null)
{
throw new ArgumentNullException(nameof(file));
}

if (string.IsNullOrEmpty(file.PhysicalPath))
{
using (var readStream = file.CreateReadStream())
{
await SendFileAsync(Body, readStream, offset, count, cancellationToken);
return;
}
}

var sendFile = HttpContext.Features.Get<IHttpSendFileFeature>();

if (sendFile == null)
{
await SendFileAsync(Body, file.PhysicalPath, offset, count, cancellationToken);
return;
}

await sendFile.SendFileAsync(file.PhysicalPath, offset, count, cancellationToken);
}

private static async Task SendFileAsync(Stream outputStream, string fileName, long offset, long? count, CancellationToken cancellationToken)
{
int bufferSize = 1024 * 16;

var fileStream = new FileStream(
fileName,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
bufferSize: bufferSize,
options: FileOptions.Asynchronous | FileOptions.SequentialScan);

using (fileStream)
{
await SendFileAsync(outputStream, fileStream, offset, count, cancellationToken);
}
}

// Not safe for overlapped writes.
private static async Task SendFileAsync(Stream outputStream, Stream readStream, long offset, long? length, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

if (offset < 0 || offset > readStream.Length)
{
throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
}

if (length.HasValue &&
(length.Value < 0 || length.Value > readStream.Length - offset))
{
throw new ArgumentOutOfRangeException(nameof(length), length, string.Empty);
}

readStream.Seek(offset, SeekOrigin.Begin); // TODO: What if !CanSeek?

await StreamCopyOperation.CopyToAsync(readStream, outputStream, length, cancellationToken);
}


struct FeatureInterfaces
{
public IHttpResponseFeature Response;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,13 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.Extensions.FileProviders;
using Xunit;

namespace Microsoft.AspNetCore.Http.Extensions.Tests
namespace Microsoft.AspNetCore.Http.Tests
{
public class SendFileResponseExtensionsTests
public class SendFileTests
{
[Fact]
public Task SendFileWhenFileNotFoundThrows()
{
var response = new DefaultHttpContext().Response;
return Assert.ThrowsAsync<FileNotFoundException>(() => response.SendFileAsync("foo"));
}

[Fact]
public async Task SendFileWorks()
{
Expand All @@ -27,7 +21,8 @@ public async Task SendFileWorks()
var fakeFeature = new FakeSendFileFeature();
context.Features.Set<IHttpSendFileFeature>(fakeFeature);

await response.SendFileAsync("bob", 1, 3, CancellationToken.None);
var fileInfo = new FakeFileInfo("bob");
await response.SendFileAsync(fileInfo, 1, 3, CancellationToken.None);

Assert.Equal("bob", fakeFeature.name);
Assert.Equal(1, fakeFeature.offset);
Expand All @@ -51,5 +46,28 @@ public Task SendFileAsync(string path, long offset, long? length, CancellationTo
return Task.FromResult(0);
}
}

private class FakeFileInfo : IFileInfo
{
public FakeFileInfo(string name, bool exists = true)
{
Name = name;
Exists = exists;
}

public Stream CreateReadStream() => new MemoryStream();

public bool Exists { get; }

public long Length => 0;

public string PhysicalPath => Name;

public string Name { get; }

public DateTimeOffset LastModified => DateTimeOffset.UtcNow;

public bool IsDirectory => false;
}
}
}