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;
+ }
}
}