diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/HttpResponse.cs b/src/Microsoft.AspNetCore.Http.Abstractions/HttpResponse.cs index 0276fb62..e097c901 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/HttpResponse.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/HttpResponse.cs @@ -3,7 +3,9 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.FileProviders; namespace Microsoft.AspNetCore.Http { @@ -103,5 +105,23 @@ public abstract class HttpResponse /// The URL to redirect the client to. /// True if the redirect is permanent (301), otherwise false (302). public abstract void Redirect(string location, bool permanent); + + /// + /// Sends the given file using the SendFile extension. + /// + /// The file to send. + /// + /// + public abstract Task SendFileAsync(IFileInfo file, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Sends the given file using the SendFile extension. + /// + /// The file to send. + /// The offset in the file. + /// The number of types to send, or null to send the remainder of the file. + /// + /// + public abstract Task SendFileAsync(IFileInfo file, long offset, long? count, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/src/Microsoft.AspNetCore.Http.Extensions/StreamCopyOperation.cs b/src/Microsoft.AspNetCore.Http.Abstractions/StreamCopyOperation.cs similarity index 97% rename from src/Microsoft.AspNetCore.Http.Extensions/StreamCopyOperation.cs rename to src/Microsoft.AspNetCore.Http.Abstractions/StreamCopyOperation.cs index c42661a1..3b1a8766 100644 --- a/src/Microsoft.AspNetCore.Http.Extensions/StreamCopyOperation.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/StreamCopyOperation.cs @@ -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 diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/project.json b/src/Microsoft.AspNetCore.Http.Abstractions/project.json index 95ff47a8..a872ecac 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/project.json +++ b/src/Microsoft.AspNetCore.Http.Abstractions/project.json @@ -11,11 +11,13 @@ }, "dependencies": { "Microsoft.AspNetCore.Http.Features": "1.0.0-*", + "Microsoft.Extensions.FileProviders.Abstractions": "1.0.0-*", "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": { @@ -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-*" } } } diff --git a/src/Microsoft.AspNetCore.Http.Extensions/SendFileResponseExtensions.cs b/src/Microsoft.AspNetCore.Http.Extensions/SendFileResponseExtensions.cs deleted file mode 100644 index 6058eb9a..00000000 --- a/src/Microsoft.AspNetCore.Http.Extensions/SendFileResponseExtensions.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// 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; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.AspNetCore.Http.Features; - -namespace Microsoft.AspNetCore.Http -{ - /// - /// Provides extensions for HttpResponse exposing the SendFile extension. - /// - public static class SendFileResponseExtensions - { - /// - /// Sends the given file using the SendFile extension. - /// - /// - /// The full path to the file. - /// - public static Task SendFileAsync(this HttpResponse response, string fileName) - { - if (response == null) - { - throw new ArgumentNullException(nameof(response)); - } - - if (fileName == null) - { - throw new ArgumentNullException(nameof(fileName)); - } - - return response.SendFileAsync(fileName, 0, null, CancellationToken.None); - } - - /// - /// Sends the given file using the SendFile extension. - /// - /// - /// The full path to the file. - /// The offset in the file. - /// The number of types to send, or null to send the remainder of the file. - /// - /// - public static Task SendFileAsync(this HttpResponse response, string fileName, long offset, long? count, CancellationToken cancellationToken) - { - if (response == null) - { - throw new ArgumentNullException(nameof(response)); - } - - if (fileName == null) - { - throw new ArgumentNullException(nameof(fileName)); - } - - var sendFile = response.HttpContext.Features.Get(); - if (sendFile == null) - { - return SendFileAsync(response.Body, fileName, offset, count, cancellationToken); - } - - return sendFile.SendFileAsync(fileName, offset, count, cancellationToken); - } - - // Not safe for overlapped writes. - private static async Task SendFileAsync(Stream outputStream, string fileName, long offset, long? length, CancellationToken cancel) - { - cancel.ThrowIfCancellationRequested(); - - var fileInfo = new FileInfo(fileName); - if (offset < 0 || offset > fileInfo.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty); - } - - if (length.HasValue && - (length.Value < 0 || length.Value > fileInfo.Length - offset)) - { - throw new ArgumentOutOfRangeException(nameof(length), length, string.Empty); - } - - int bufferSize = 1024 * 16; - - var fileStream = new FileStream( - fileName, - FileMode.Open, - FileAccess.Read, - FileShare.ReadWrite, - bufferSize: bufferSize, - options: FileOptions.Asynchronous | FileOptions.SequentialScan); - - using (fileStream) - { - fileStream.Seek(offset, SeekOrigin.Begin); - - await StreamCopyOperation.CopyToAsync(fileStream, outputStream, length, cancel); - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Http.Extensions/project.json b/src/Microsoft.AspNetCore.Http.Extensions/project.json index f38f4a0f..9549bd30 100644 --- a/src/Microsoft.AspNetCore.Http.Extensions/project.json +++ b/src/Microsoft.AspNetCore.Http.Extensions/project.json @@ -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": { } } } diff --git a/src/Microsoft.AspNetCore.Http/DefaultHttpResponse.cs b/src/Microsoft.AspNetCore.Http/DefaultHttpResponse.cs index 0676913d..bcd387bd 100644 --- a/src/Microsoft.AspNetCore.Http/DefaultHttpResponse.cs +++ b/src/Microsoft.AspNetCore.Http/DefaultHttpResponse.cs @@ -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 @@ -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; } } @@ -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); + } + + /// + /// Sends the given file using the SendFile extension. + /// + /// The file to send. + /// The offset in the file. + /// The number of types to send, or null to send the remainder of the file. + /// + /// + 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(); + + 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; diff --git a/test/Microsoft.AspNetCore.Http.Extensions.Tests/SendFileResponseExtensionsTests.cs b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/SendFileTests.cs similarity index 63% rename from test/Microsoft.AspNetCore.Http.Extensions.Tests/SendFileResponseExtensionsTests.cs rename to test/Microsoft.AspNetCore.Http.Abstractions.Tests/SendFileTests.cs index c1e145c3..c5239b46 100644 --- a/test/Microsoft.AspNetCore.Http.Extensions.Tests/SendFileResponseExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/SendFileTests.cs @@ -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(() => response.SendFileAsync("foo")); - } - [Fact] public async Task SendFileWorks() { @@ -27,7 +21,8 @@ public async Task SendFileWorks() var fakeFeature = new FakeSendFileFeature(); context.Features.Set(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); @@ -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; + } } }