diff --git a/src/Microsoft.AspNet.Http.Abstractions/project.json b/src/Microsoft.AspNet.Http.Abstractions/project.json index 1316a951..d76ed1ff 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/project.json +++ b/src/Microsoft.AspNet.Http.Abstractions/project.json @@ -36,6 +36,7 @@ "System.Net.Primitives": "4.0.11-*", "System.Net.WebSockets": "4.0.0-*", "System.Reflection.TypeExtensions": "4.0.1-*", + "System.IO": "4.0.11-*", "System.Runtime": "4.0.21-*", "System.Runtime.InteropServices": "4.0.21-*", "System.Security.Claims": "4.0.1-*", diff --git a/src/Microsoft.AspNet.Http.Extensions/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Http.Extensions/Properties/Resources.Designer.cs deleted file mode 100644 index a0d5a215..00000000 --- a/src/Microsoft.AspNet.Http.Extensions/Properties/Resources.Designer.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -namespace Microsoft.AspNet.Http.Extensions -{ - using System.Globalization; - using System.Reflection; - using System.Resources; - - internal static class Resources - { - private static readonly ResourceManager _resourceManager - = new ResourceManager("Microsoft.AspNet.Http.Extensions.Resources", typeof(Resources).GetTypeInfo().Assembly); - - /// - /// This server does not support the sendfile.SendAsync extension. - /// - internal static string Exception_SendFileNotSupported - { - get { return GetString("Exception_SendFileNotSupported"); } - } - - /// - /// This server does not support the sendfile.SendAsync extension. - /// - internal static string FormatException_SendFileNotSupported() - { - return GetString("Exception_SendFileNotSupported"); - } - - private static string GetString(string name, params string[] formatterNames) - { - var value = _resourceManager.GetString(name); - - System.Diagnostics.Debug.Assert(value != null); - - if (formatterNames != null) - { - for (var i = 0; i < formatterNames.Length; i++) - { - value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); - } - } - - return value; - } - } -} diff --git a/src/Microsoft.AspNet.Http.Extensions/Resources.resx b/src/Microsoft.AspNet.Http.Extensions/Resources.resx deleted file mode 100644 index 2059135d..00000000 --- a/src/Microsoft.AspNet.Http.Extensions/Resources.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - This server does not support the sendfile.SendAsync extension. - - \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http.Extensions/SendFileResponseExtensions.cs b/src/Microsoft.AspNet.Http.Extensions/SendFileResponseExtensions.cs index 680c77e6..7218adde 100644 --- a/src/Microsoft.AspNet.Http.Extensions/SendFileResponseExtensions.cs +++ b/src/Microsoft.AspNet.Http.Extensions/SendFileResponseExtensions.cs @@ -2,9 +2,9 @@ // 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.AspNet.Http.Extensions; using Microsoft.AspNet.Http.Features; namespace Microsoft.AspNet.Http @@ -14,26 +14,11 @@ namespace Microsoft.AspNet.Http /// public static class SendFileResponseExtensions { - /// - /// Checks if the SendFile extension is supported. - /// - /// - /// True if sendfile feature exists in the response. - public static bool SupportsSendFile(this HttpResponse response) - { - if (response == null) - { - throw new ArgumentNullException(nameof(response)); - } - - return response.HttpContext.Features.Get() != null; - } - /// /// Sends the given file using the SendFile extension. /// /// - /// + /// The full to the file. /// public static Task SendFileAsync(this HttpResponse response, string fileName) { @@ -54,7 +39,7 @@ public static Task SendFileAsync(this HttpResponse response, string fileName) /// Sends the given file using the SendFile extension. /// /// - /// The full or relative path to the file. + /// The full to the file. /// The offset in the file. /// The number of types to send, or null to send the remainder of the file. /// @@ -74,10 +59,48 @@ public static Task SendFileAsync(this HttpResponse response, string fileName, lo var sendFile = response.HttpContext.Features.Get(); if (sendFile == null) { - throw new NotSupportedException(Resources.Exception_SendFileNotSupported); + 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); + + // TODO: Use buffer pool + var buffer = new byte[bufferSize]; + + await StreamCopyOperation.CopyToAsync(fileStream, buffer, outputStream, length, cancel); + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http.Extensions/StreamCopyOperation.cs b/src/Microsoft.AspNet.Http.Extensions/StreamCopyOperation.cs new file mode 100644 index 00000000..f7e6734c --- /dev/null +++ b/src/Microsoft.AspNet.Http.Extensions/StreamCopyOperation.cs @@ -0,0 +1,57 @@ +// 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.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Http +{ + // FYI: In most cases the source will be a FileStream and the destination will be to the network. + internal static class StreamCopyOperation + { + internal static async Task CopyToAsync(Stream source, byte[] buffer, Stream destination, long? length, CancellationToken cancel) + { + long? bytesRemaining = length; + Debug.Assert(source != null); + Debug.Assert(destination != null); + Debug.Assert(!bytesRemaining.HasValue || bytesRemaining.Value >= 0); + Debug.Assert(buffer != null); + + while (true) + { + // The natural end of the range. + if (bytesRemaining.HasValue && bytesRemaining.Value <= 0) + { + return; + } + + cancel.ThrowIfCancellationRequested(); + + int readLength = buffer.Length; + if (bytesRemaining.HasValue) + { + readLength = (int)Math.Min(bytesRemaining.Value, (long)readLength); + } + int count = await source.ReadAsync(buffer, 0, readLength, cancel); + + if (bytesRemaining.HasValue) + { + bytesRemaining -= count; + } + + // End of the source stream. + if (count == 0) + { + return; + } + + cancel.ThrowIfCancellationRequested(); + + await destination.WriteAsync(buffer, 0, count, cancel); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Http.Extensions.Tests/SendFileResponseExtensionsTests.cs b/test/Microsoft.AspNet.Http.Extensions.Tests/SendFileResponseExtensionsTests.cs index cd16acb0..53c80517 100644 --- a/test/Microsoft.AspNet.Http.Extensions.Tests/SendFileResponseExtensionsTests.cs +++ b/test/Microsoft.AspNet.Http.Extensions.Tests/SendFileResponseExtensionsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. using System; +using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Http.Features; @@ -12,20 +13,10 @@ namespace Microsoft.AspNet.Http.Extensions.Tests public class SendFileResponseExtensionsTests { [Fact] - public void SendFileSupport() - { - var context = new DefaultHttpContext(); - var response = context.Response; - Assert.False(response.SupportsSendFile()); - context.Features.Set(new FakeSendFileFeature()); - Assert.True(response.SupportsSendFile()); - } - - [Fact] - public Task SendFileWhenNotSupported() + public Task SendFileWhenFileNotFoundThrows() { var response = new DefaultHttpContext().Response; - return Assert.ThrowsAsync(() => response.SendFileAsync("foo")); + return Assert.ThrowsAsync(() => response.SendFileAsync("foo")); } [Fact]