Przeglądaj źródła

.Net Framework版本的ResumeFileResult

懒得勤快 6 lat temu
rodzic
commit
55ca75c4c7

+ 58 - 0
Masuit.Tools.UnitTest/Masuit.Tools.UnitTest.csproj

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="..\packages\NUnit.3.11.0\build\NUnit.props" Condition="Exists('..\packages\NUnit.3.11.0\build\NUnit.props')" />
   <Import Project="..\packages\MSTest.TestAdapter.1.4.0\build\net45\MSTest.TestAdapter.props" Condition="Exists('..\packages\MSTest.TestAdapter.1.4.0\build\net45\MSTest.TestAdapter.props')" />
   <Import Project="..\packages\xunit.core.2.4.1\build\xunit.core.props" Condition="Exists('..\packages\xunit.core.2.4.1\build\xunit.core.props')" />
   <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
@@ -40,15 +41,53 @@
     <WarningLevel>4</WarningLevel>
   </PropertyGroup>
   <ItemGroup>
+    <Reference Include="Castle.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc, processorArchitecture=MSIL">
+      <HintPath>..\packages\Castle.Core.4.3.1\lib\net45\Castle.Core.dll</HintPath>
+    </Reference>
     <Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
       <HintPath>..\packages\MSTest.TestFramework.1.4.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll</HintPath>
     </Reference>
     <Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
       <HintPath>..\packages\MSTest.TestFramework.1.4.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
     </Reference>
+    <Reference Include="Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath>
+    </Reference>
+    <Reference Include="Moq, Version=4.10.0.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
+      <HintPath>..\packages\Moq.4.10.1\lib\net45\Moq.dll</HintPath>
+    </Reference>
+    <Reference Include="nunit.framework, Version=3.11.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
+      <HintPath>..\packages\NUnit.3.11.0\lib\net45\nunit.framework.dll</HintPath>
+    </Reference>
     <Reference Include="System" />
+    <Reference Include="System.Configuration" />
     <Reference Include="System.Core" />
     <Reference Include="System.Net.Http" />
+    <Reference Include="System.Runtime.CompilerServices.Unsafe, Version=4.0.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.Runtime.CompilerServices.Unsafe.4.5.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Threading.Tasks.Extensions, Version=4.2.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.Threading.Tasks.Extensions.4.5.1\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web" />
+    <Reference Include="System.Web.Helpers, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.Helpers.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.Mvc, Version=5.2.7.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.Mvc.5.2.7\lib\net45\System.Web.Mvc.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.Razor.3.2.7\lib\net45\System.Web.Razor.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.WebPages, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.WebPages.Deployment, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.Deployment.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.Razor.dll</HintPath>
+    </Reference>
     <Reference Include="xunit.abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
       <HintPath>..\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll</HintPath>
     </Reference>
@@ -67,6 +106,16 @@
     <Compile Include="ExtensionMethodsTest.cs" />
     <Compile Include="HtmlToolsTest.cs" />
     <Compile Include="LinqExtensionTest.cs" />
+    <Compile Include="Mvc\BaseTests.cs" />
+    <Compile Include="Mvc\MimeMapperTests.cs" />
+    <Compile Include="Mvc\Mocks\MockHttpFilesCollection.cs" />
+    <Compile Include="Mvc\Mocks\MockHttpPostedFileBase.cs" />
+    <Compile Include="Mvc\Mocks\MockHttpRequest.cs" />
+    <Compile Include="Mvc\Mocks\MockHttpResponse.cs" />
+    <Compile Include="Mvc\Mocks\MockHttpSession.cs" />
+    <Compile Include="Mvc\Mocks\MockRequestContext.cs" />
+    <Compile Include="Mvc\Mocks\MockResumeFileResult.cs" />
+    <Compile Include="Mvc\ResumeFileResultTests.cs" />
     <Compile Include="NumberFormaterTest.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="TemplateTest.cs" />
@@ -83,6 +132,14 @@
   <ItemGroup>
     <Analyzer Include="..\packages\xunit.analyzers.0.10.0\analyzers\dotnet\cs\xunit.analyzers.dll" />
   </ItemGroup>
+  <ItemGroup>
+    <Content Include="Resources\download-test-file.txt">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+    <Content Include="Resources\download-test-file2.txt">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+  </ItemGroup>
   <Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
@@ -93,6 +150,7 @@
     <Error Condition="!Exists('..\packages\xunit.core.2.4.1\build\xunit.core.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\xunit.core.2.4.1\build\xunit.core.targets'))" />
     <Error Condition="!Exists('..\packages\MSTest.TestAdapter.1.4.0\build\net45\MSTest.TestAdapter.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.1.4.0\build\net45\MSTest.TestAdapter.props'))" />
     <Error Condition="!Exists('..\packages\MSTest.TestAdapter.1.4.0\build\net45\MSTest.TestAdapter.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.1.4.0\build\net45\MSTest.TestAdapter.targets'))" />
+    <Error Condition="!Exists('..\packages\NUnit.3.11.0\build\NUnit.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\NUnit.3.11.0\build\NUnit.props'))" />
   </Target>
   <Import Project="..\packages\xunit.core.2.4.1\build\xunit.core.targets" Condition="Exists('..\packages\xunit.core.2.4.1\build\xunit.core.targets')" />
   <Import Project="..\packages\MSTest.TestAdapter.1.4.0\build\net45\MSTest.TestAdapter.targets" Condition="Exists('..\packages\MSTest.TestAdapter.1.4.0\build\net45\MSTest.TestAdapter.targets')" />

+ 86 - 0
Masuit.Tools.UnitTest/Mvc/BaseTests.cs

@@ -0,0 +1,86 @@
+using Masuit.Tools.UnitTest.Mvc.Mocks;
+using Moq;
+using NUnit.Framework;
+using System;
+using System.IO;
+using System.Web;
+using System.Web.Mvc;
+using System.Web.Routing;
+using ModelBindingContext = System.Web.ModelBinding.ModelBindingContext;
+using ModelMetadataProviders = System.Web.ModelBinding.ModelMetadataProviders;
+
+namespace Masuit.Tools.UnitTest.Mvc
+{
+    [TestFixture]
+    public abstract class BaseTests
+    {
+        protected internal MockHttpResponse Response { get; set; }
+        protected internal HttpContextBase Context { get; set; }
+        protected internal MockHttpRequest Request { get; set; }
+        protected internal MockHttpSession Session { get; private set; }
+
+        protected string TestDirectoryPath()
+        {
+            return new DirectoryInfo(AppContext.BaseDirectory + ".\\Resources").FullName;
+        }
+
+        protected FileInfo TestFile(string fileName)
+        {
+            return new FileInfo($"{TestDirectoryPath()}\\{fileName}");
+        }
+
+        protected string FilePath(string fileName)
+        {
+            return TestFile(fileName).FullName;
+        }
+
+        protected BaseTests()
+        {
+            InitMocks();
+        }
+
+        [SetUp]
+        public void BaseTestsSetup()
+        {
+            InitMocks();
+        }
+
+        protected void InitMocks()
+        {
+            var mockHttpContext = new Mock<HttpContextBase>();
+            Context = mockHttpContext.Object;
+            Session = new MockHttpSession();
+            Request = new MockHttpRequest(new MockHttpFilesCollection(null));
+            Response = new MockHttpResponse();
+
+            mockHttpContext.Setup(ctx => ctx.Session).Returns(() => Session);
+            mockHttpContext.Setup(ctx => ctx.Request).Returns(() => Request);
+            mockHttpContext.Setup(ctx => ctx.Response).Returns(() => Response);
+            mockHttpContext.Setup(ctx => ctx.Cache).Returns(() => HttpRuntime.Cache);
+        }
+
+        protected ControllerContext ControllerContext<T>(T controller) where T : ControllerBase
+        {
+            return new ControllerContext(Context, new RouteData(), controller);
+        }
+
+        protected ModelBindingContext BindingContext<T>()
+        {
+            return new ModelBindingContext
+            {
+                ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(T))
+            };
+        }
+
+        protected T MockObject<T>() where T : class
+        {
+            var mock = new Mock<T>();
+            return mock.Object;
+        }
+
+        protected Mock<T> Mock<T>() where T : class
+        {
+            return new Mock<T>();
+        }
+    }
+}

+ 82 - 0
Masuit.Tools.UnitTest/Mvc/MimeMapperTests.cs

@@ -0,0 +1,82 @@
+// ReSharper disable InconsistentNaming
+
+using Masuit.Tools.Mvc.Mime;
+using NUnit.Framework;
+
+namespace Masuit.Tools.UnitTest.Mvc
+{
+    [TestFixture]
+    public class MimeMapperTests
+    {
+        private IMimeMapper _mapper;
+
+        [SetUp]
+        public void Setup()
+        {
+            _mapper = new MimeMapper();
+        }
+
+        [Test]
+        public void GetDefaultExtension()
+        {
+            Assert.AreEqual("text/plain", _mapper.GetMimeFromExtension("txt"));
+        }
+
+        [Test]
+        public void Search_Works_For_Extensions_With_Dot_As_Well()
+        {
+            Assert.IsNotNull(_mapper.GetMimeFromExtension("css"));
+            Assert.AreEqual(_mapper.GetMimeFromExtension("css"), _mapper.GetMimeFromExtension(".css"));
+        }
+
+        [Test]
+        public void It_Returns_Default_Mime_For_Null_In_Extension()
+        {
+            Assert.AreEqual("application/octet-stream", _mapper.GetMimeFromExtension(null));
+        }
+
+        [Test]
+        public void It_Returns_Default_Mime_For_Not_Found_Extension()
+        {
+            Assert.AreEqual("application/octet-stream", _mapper.GetMimeFromExtension("not found"));
+        }
+
+        [Test]
+        public void It_Searches_In_Full_Path()
+        {
+            Assert.AreEqual("image/gif", _mapper.GetMimeFromPath("C:\\folder1\\folder2\\text.gif"));
+        }
+
+        [Test]
+        public void It_Searches_In_Relative_Path()
+        {
+            Assert.AreEqual("image/gif", _mapper.GetMimeFromPath("..\\..\\..\\text.gif"));
+        }
+
+        [Test]
+        public void Extension_Overrides_Default_Mime()
+        {
+            _mapper = new MimeMapper(new MimeMappingItem
+            {
+                Extension = "txt",
+                MimeType = "my own mime type"
+            });
+            Assert.AreEqual("my own mime type", _mapper.GetMimeFromPath(".txt"));
+            Assert.AreEqual("my own mime type", _mapper.GetMimeFromPath("..\\..\\..\\text.txt"));
+        }
+
+        [Test]
+        public void Search_Works_For_Files_With_Dots_In_Name()
+        {
+            Assert.AreEqual("text/javascript", _mapper.GetMimeFromPath("jquery.min.js"));
+            Assert.AreEqual("text/javascript", _mapper.GetMimeFromPath("http://example.com/jquery.min.js"));
+        }
+
+        [Test]
+        public void It_Returns_Default_Mime_For_Files_Without_Extension()
+        {
+            Assert.AreEqual("application/octet-stream", _mapper.GetMimeFromPath("testfile"));
+            Assert.AreEqual("application/octet-stream", _mapper.GetMimeFromPath("\\\\network\\share\\testfile"));
+        }
+    }
+}

+ 35 - 0
Masuit.Tools.UnitTest/Mvc/Mocks/MockHttpFilesCollection.cs

@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+using Mvc.Stream.Tests.Mocks;
+
+namespace Masuit.Tools.UnitTest.Mvc.Mocks
+{
+    public class MockHttpFilesCollection : HttpFileCollectionBase
+    {
+        private readonly Dictionary<string, MockHttpPostedFileBase> _files
+            = new Dictionary<string, MockHttpPostedFileBase>();
+
+        public MockHttpFilesCollection(MockHttpPostedFileBase file)
+        {
+            if (file != null)
+            {
+                _files.Add(file.FileName, file);
+            }
+        }
+
+        public override int Count => _files.Count;
+
+        public override HttpPostedFileBase this[int index] => _files.Skip(index).Take(1).FirstOrDefault().Value;
+
+        public override HttpPostedFileBase this[string name] => _files[name];
+
+        public override string[] AllKeys
+        {
+            get
+            {
+                return _files.Select(x => x.Key).ToArray();
+            }
+        }
+    }
+}

+ 40 - 0
Masuit.Tools.UnitTest/Mvc/Mocks/MockHttpPostedFileBase.cs

@@ -0,0 +1,40 @@
+using System.IO;
+using System.Web;
+
+namespace Mvc.Stream.Tests.Mocks
+{
+    public class MockHttpPostedFileBase : HttpPostedFileBase
+    {
+        public MockHttpPostedFileBase(int contentLen, string fileName, string contentType, System.IO.Stream stream = null)
+        {
+            ContentLength = contentLen;
+            FileName = fileName;
+            ContentType = contentType;
+            InputStream = stream;
+        }
+
+        public override int ContentLength { get; }
+
+        public override string FileName { get; }
+
+        public override System.IO.Stream InputStream { get; }
+
+        public override string ContentType { get; }
+
+        public override void SaveAs(string filename)
+        {
+            var fileInfo = new FileInfo(filename);
+            var directory = new DirectoryInfo(Path.GetDirectoryName(fileInfo.FullName));
+
+            if (!directory.Exists)
+            {
+                directory.Create();
+            }
+
+            using (var file = fileInfo.CreateText())
+            {
+                file.Write("test");
+            }
+        }
+    }
+}

+ 62 - 0
Masuit.Tools.UnitTest/Mvc/Mocks/MockHttpRequest.cs

@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Specialized;
+using System.IO;
+using System.Web;
+using System.Web.Routing;
+using Mvc.Stream.Tests.Mocks;
+
+namespace Masuit.Tools.UnitTest.Mvc.Mocks
+{
+    public class MockHttpRequest : HttpRequestBase
+    {
+        public override NameValueCollection Headers => _headers;
+
+        public override HttpFileCollectionBase Files { get; }
+
+        public override RequestContext RequestContext => _context;
+        public override string ApplicationPath => _applicationPath;
+
+        public override System.IO.Stream InputStream
+        {
+            get
+            {
+                if (TestInput != null)
+                {
+                    var stream = new MemoryStream();
+                    var chars = TestInput.ToCharArray();
+                    foreach (var c in chars)
+                    {
+                        stream.WriteByte(Convert.ToByte(c));
+                    }
+                    return stream;
+                }
+                return new MemoryStream();
+            }
+        }
+
+        public override string HttpMethod => TestHttpMethod;
+        private readonly NameValueCollection _headers = new NameValueCollection();
+        private readonly MockRequestContext _context = new MockRequestContext();
+        private string _applicationPath;
+        public string TestInput;
+
+        public string TestHttpMethod;
+
+        public MockHttpRequest(MockHttpFilesCollection filesMock)
+        {
+            Files = filesMock;
+        }
+
+        public MockHttpRequest SetHeader(string header, string val)
+        {
+            _headers[header] = val;
+            return this;
+        }
+
+        public MockHttpRequest SetApplicationPath(string path)
+        {
+            _applicationPath = path;
+            return this;
+        }
+    }
+}

+ 84 - 0
Masuit.Tools.UnitTest/Mvc/Mocks/MockHttpResponse.cs

@@ -0,0 +1,84 @@
+using System.Collections.Specialized;
+using System.IO;
+using System.Web;
+
+namespace Masuit.Tools.UnitTest.Mvc.Mocks
+{
+    public class MockHttpResponse : HttpResponseBase
+    {
+        public override NameValueCollection Headers => _headers;
+
+        public bool FileTransmitted { get; set; }
+        public override bool BufferOutput { get; set; }
+
+        public override System.IO.Stream OutputStream => _stream ?? (_stream = new MemoryStream());
+
+        public override bool IsClientConnected => true;
+
+        public override string ContentType { get; set; }
+
+        public override int StatusCode { get; set; }
+
+        private readonly NameValueCollection _headers = new NameValueCollection();
+
+        private MemoryStream _stream;
+
+        public bool IsClosed;
+
+        public override void AppendHeader(string name, string value)
+        {
+            AddHeader(name, value);
+        }
+
+        public override void AddHeader(string name, string value)
+        {
+            _headers.Add(name, value);
+        }
+
+        public void ClearTestResponse()
+        {
+            _stream = new MemoryStream();
+            Headers.Clear();
+            StatusCode = 0;
+        }
+
+        public override void Flush()
+        {
+        }
+
+        public override void Write(string s)
+        {
+        }
+
+        public override void Close()
+        {
+            IsClosed = true;
+        }
+
+        public override void TransmitFile(string filename)
+        {
+            FileTransmitted = true;
+            var fi = new FileInfo(filename);
+            using (var read = fi.OpenRead())
+            {
+                for (var i = 0; i < fi.Length; i++)
+                {
+                    OutputStream.WriteByte((byte)read.ReadByte());
+                }
+            }
+        }
+
+        public override void TransmitFile(string filename, long offset, long length)
+        {
+            var fi = new FileInfo(filename);
+            using (var read = fi.OpenRead())
+            {
+                read.Seek(offset, SeekOrigin.Begin);
+                for (var i = 0; i < length; i++)
+                {
+                    OutputStream.WriteByte((byte)read.ReadByte());
+                }
+            }
+        }
+    }
+}

+ 36 - 0
Masuit.Tools.UnitTest/Mvc/Mocks/MockHttpSession.cs

@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using System.Web;
+
+namespace Masuit.Tools.UnitTest.Mvc.Mocks
+{
+    public class MockHttpSession : HttpSessionStateBase
+    {
+        private readonly Dictionary<string, object> _sessionStorage = new Dictionary<string, object>();
+        private string _sessionId = "0000-0000";
+
+        public override object this[string name]
+        {
+            get
+            {
+                _sessionStorage.TryGetValue(name, out var val);
+                return val;
+            }
+            set => _sessionStorage[name] = value;
+        }
+
+        public void SetSessionId(string sessionId)
+        {
+            _sessionId = sessionId;
+        }
+
+        public override string SessionID => _sessionId;
+
+        public override void Remove(string name)
+        {
+            if (_sessionStorage.ContainsKey(name))
+            {
+                _sessionStorage.Remove(name);
+            }
+        }
+    }
+}

+ 8 - 0
Masuit.Tools.UnitTest/Mvc/Mocks/MockRequestContext.cs

@@ -0,0 +1,8 @@
+using System.Web.Routing;
+
+namespace Masuit.Tools.UnitTest.Mvc.Mocks
+{
+    public class MockRequestContext : RequestContext
+    {
+    }
+}

+ 46 - 0
Masuit.Tools.UnitTest/Mvc/Mocks/MockResumeFileResult.cs

@@ -0,0 +1,46 @@
+using System.Web;
+using Masuit.Tools.Mvc;
+
+namespace Masuit.Tools.UnitTest.Mvc.Mocks
+{
+    public class MockResumeFileResult : ResumeFileResult
+    {
+        public MockResumeFileResult(string fileName, string contentType, HttpRequestBase request) : base(fileName, contentType, request)
+        {
+        }
+
+        public MockResumeFileResult(string fileName, string contentType, HttpRequestBase request, string downloadFileName) : base(fileName, contentType, request, downloadFileName)
+        {
+        }
+
+        public new bool IsNotModified()
+        {
+            return base.IsNotModified();
+        }
+
+        public new bool IsPreconditionFailed()
+        {
+            return base.IsPreconditionFailed();
+        }
+
+        public new bool IsRangeNotSatisfiable()
+        {
+            return base.IsRangeNotSatisfiable();
+        }
+
+        public new bool SendRange()
+        {
+            return base.SendRange();
+        }
+
+        public void WriteFileTest(HttpResponseBase response)
+        {
+            base.WriteFile(response);
+        }
+
+        public void TransmitTest(HttpResponseBase response)
+        {
+            base.TransmitFile(response);
+        }
+    }
+}

+ 516 - 0
Masuit.Tools.UnitTest/Mvc/ResumeFileResultTests.cs

@@ -0,0 +1,516 @@
+using Masuit.Tools.Mvc;
+using Masuit.Tools.UnitTest.Mvc;
+using Masuit.Tools.UnitTest.Mvc.Mocks;
+using NUnit.Framework;
+using System;
+using System.IO;
+using System.Net;
+using System.Threading;
+
+namespace Mvc.Stream.Tests
+{
+    namespace Adstream.Web.Common.test
+    {
+        [TestFixture]
+        public class ResumeFileResultTests : BaseTests
+        {
+            private FileInfo _file;
+            private FileInfo _file2;
+            private string ContentType = "video/mp4";
+
+            [SetUp]
+            public void Setup()
+            {
+                _file = new FileInfo(FilePath("download-test-file.txt"));
+                _file2 = new FileInfo(FilePath("download-test-file2.txt"));
+                Request.Headers.Clear();
+                Response.ClearTestResponse();
+            }
+
+            [Test]
+            public void FormatDateTest()
+            {
+                var dateToTest = new DateTime(2010, 5, 25, 7, 44, 57);
+                Assert.AreEqual(dateToTest.ToString("R"), ResumeFileResult.Util.FormatDate(dateToTest));
+            }
+
+            [Test]
+            public void CanCalculateEtagForFile()
+            {
+                Assert.IsNotNull(ResumeFileResult.Util.Etag(_file));
+            }
+
+            [Test]
+            public void EtagDoesNotDependOnTime()
+            {
+                var etag1 = ResumeFileResult.Util.Etag(_file);
+                Thread.Sleep(100);
+                var etag2 = ResumeFileResult.Util.Etag(_file);
+                Assert.AreEqual(etag1, etag2);
+            }
+
+            [Test]
+            public void EtagDoesDependOnFile()
+            {
+                var etag1 = ResumeFileResult.Util.Etag(_file);
+                Thread.Sleep(100);
+                var etag2 = ResumeFileResult.Util.Etag(_file2);
+                Assert.AreNotEqual(etag1, etag2);
+            }
+
+            [Test]
+            public void IsNotModified_Is_False_If_IfNoneMatch_And_IfModifiedSince_Are_Empty()
+            {
+                Request.Headers[HttpHeaders.IfNoneMatch] = null;
+                Request.Headers[HttpHeaders.IfModifiedSince] = null;
+                Assert.IsFalse(new MockResumeFileResult(_file.FullName, ContentType, Request).IsNotModified());
+            }
+
+            [Test]
+            public void IsNotModified_Is_False_If_Etag_Is_Invalid_And_IfModifiedSince_Is_Null()
+            {
+                var etag = "invalid etag";
+                Request.Headers[HttpHeaders.IfNoneMatch] = etag;
+                Request.Headers[HttpHeaders.IfModifiedSince] = null;
+                Assert.IsFalse(new MockResumeFileResult(_file.FullName, ContentType, Request).IsNotModified());
+            }
+
+            [Test]
+            public void IsNotModified_Is_True_If_Etag_Is_Valid()
+            {
+                var etag = ResumeFileResult.Util.Etag(_file);
+                Request.Headers[HttpHeaders.IfNoneMatch] = etag;
+                Assert.IsTrue(new MockResumeFileResult(_file.FullName, ContentType, Request).IsNotModified());
+            }
+
+            [Test]
+            public void IsNotModified_Is_True_If_Etag_Is_Star()
+            {
+                var etag = "*";
+                Request.Headers[HttpHeaders.IfNoneMatch] = etag;
+                Assert.IsTrue(new MockResumeFileResult(_file.FullName, ContentType, Request).IsNotModified());
+            }
+
+            [Test]
+            public void IsNotModified_Is_False_If_Etag_Is_Empty_And_IfModifiedSince_Is_Invalid()
+            {
+                Request.Headers[HttpHeaders.IfModifiedSince] = ResumeFileResult.Util.FormatDate(DateTime.Now);
+                Assert.IsFalse(new MockResumeFileResult(_file.FullName, ContentType, Request).IsNotModified());
+            }
+
+            [Test]
+            public void IsNotModified_Is_False_If_Etag_Is_Empty_And_IfModifiedSince_Is_LastFileWriteTime()
+            {
+                Request.Headers[HttpHeaders.IfModifiedSince] = ResumeFileResult.Util.FormatDate(_file.LastWriteTime);
+                Assert.IsTrue(new MockResumeFileResult(_file.FullName, ContentType, Request).IsNotModified());
+            }
+
+            [Test]
+            public void IsPreconditionFailedTest_Is_False_If_ifMatch_And_ifUnmodifiedSince_Are_Empty()
+            {
+                Request.Headers[HttpHeaders.IfMatch] = null;
+                Request.Headers[HttpHeaders.IfUnmodifiedSince] = null;
+                Assert.IsFalse(new MockResumeFileResult(_file.FullName, ContentType, Request).IsPreconditionFailed());
+            }
+
+            [Test]
+            public void IsPreconditionFailedTest_Is_IsTrue_If_ifMatch_Doesnot_Match_Etag_Of_The_File()
+            {
+                Request.Headers[HttpHeaders.IfMatch] = "incorrect";
+                Assert.IsTrue(new MockResumeFileResult(_file.FullName, ContentType, Request).IsPreconditionFailed());
+            }
+
+            [Test]
+            public void IsPreconditionFailedTest_Is_IsFalse_If_ifMatch_Matches_Etag_Of_The_File()
+            {
+                Request.Headers[HttpHeaders.IfMatch] = ResumeFileResult.Util.Etag(_file);
+                Assert.IsFalse(new MockResumeFileResult(_file.FullName, ContentType, Request).IsPreconditionFailed());
+            }
+
+            [Test]
+            public void IsPreconditionFailedTest_Is_IsFalse_If_ifMatch_Equals_Star()
+            {
+                Request.Headers[HttpHeaders.IfMatch] = "*";
+                Assert.IsFalse(new MockResumeFileResult(_file.FullName, ContentType, Request).IsPreconditionFailed());
+            }
+
+            [Test]
+            public void IsPreconditionFailedTest_Is_IsTrue_If_ifUnmodifiedSince_Doesnot_Equal_FileLastWriteTime()
+            {
+                Request.Headers[HttpHeaders.IfUnmodifiedSince] = ResumeFileResult.Util.FormatDate(DateTime.Now);
+                Assert.IsTrue(new MockResumeFileResult(_file.FullName, ContentType, Request).IsPreconditionFailed());
+            }
+
+            [Test]
+            public void IsPreconditionFailedTest_Is_IsFalse_If_ifUnmodifiedSince_Equals_FileLastWriteTime()
+            {
+                Request.Headers[HttpHeaders.IfUnmodifiedSince] = ResumeFileResult.Util.FormatDate(_file.LastWriteTime);
+                Assert.IsFalse(new MockResumeFileResult(_file.FullName, ContentType, Request).IsPreconditionFailed());
+            }
+
+            [Test]
+            public void IsRangeNotSatisfiable_Is_True_If_Range_Header_Has_Invalid_Format()
+            {
+                Request.Headers[HttpHeaders.Range] = "blah";
+                Assert.IsTrue(new MockResumeFileResult(_file.FullName, ContentType, Request).IsRangeNotSatisfiable());
+            }
+
+            [Test]
+            public void IsRangeNotSatisfiable_Is_True_If_Start_Greater_Than_End()
+            {
+                Request.Headers[HttpHeaders.Range] = "bytes=100-0";
+                Assert.IsTrue(new MockResumeFileResult(_file.FullName, ContentType, Request).IsRangeNotSatisfiable());
+            }
+
+            [Test]
+            public void IsRangeNotSatisfiable_Is_True_If_End_Equals_Total_File_Size()
+            {
+                Request.Headers[HttpHeaders.Range] = "bytes=0-" + _file.Length;
+                Assert.IsTrue(new MockResumeFileResult(_file.FullName, ContentType, Request).IsRangeNotSatisfiable());
+            }
+
+            [Test]
+            public void IsRangeNotSatisfiable_Is_True_If_End_Greater_Than_Total_File_Size()
+            {
+                Request.Headers[HttpHeaders.Range] = "bytes=0-" + _file.Length + 10;
+                Assert.IsTrue(new MockResumeFileResult(_file.FullName, ContentType, Request).IsRangeNotSatisfiable());
+            }
+
+            [Test]
+            public void IsRangeNotSatisfiable_Is_False_If_Range_Header_Is_Null()
+            {
+                Request.Headers[HttpHeaders.Range] = null;
+                Assert.IsFalse(new MockResumeFileResult(_file.FullName, ContentType, Request).IsRangeNotSatisfiable());
+            }
+
+            [Test]
+            public void IsRangeNotSatisfiable_Is_False_If_Range_Has_StartsWith_Format()
+            {
+                Request.Headers[HttpHeaders.Range] = "bytes=0-";
+                Assert.IsFalse(new MockResumeFileResult(_file.FullName, ContentType, Request).IsRangeNotSatisfiable());
+            }
+
+            [Test]
+            public void IsRangeNotSatisfiable_Is_False_If_Range_Has_LastXbytes_Format()
+            {
+                Request.Headers[HttpHeaders.Range] = "bytes=-100";
+                Assert.IsFalse(new MockResumeFileResult(_file.FullName, ContentType, Request).IsRangeNotSatisfiable());
+            }
+
+            [Test]
+            public void IsRangeNotSatisfiable_Is_False_If_Range_Ends_With_Last_Byte_Position()
+            {
+                Request.Headers[HttpHeaders.Range] = "bytes=100-" + (_file.Length - 1);
+                Assert.IsFalse(new MockResumeFileResult(_file.FullName, ContentType, Request).IsRangeNotSatisfiable());
+            }
+
+            [Test]
+            public void SendRange_Is_False_If_Range_And_ifRange_Headers_Are_Null()
+            {
+                Assert.IsFalse(new MockResumeFileResult(_file.FullName, ContentType, Request).SendRange());
+            }
+
+            [Test]
+            public void SendRange_Is_False_If_Range_Is_Null_And_ifRange_Is_Correct()
+            {
+                Request.Headers[HttpHeaders.IfRange] = ResumeFileResult.Util.Etag(_file);
+                Assert.IsFalse(new MockResumeFileResult(_file.FullName, ContentType, Request).SendRange());
+            }
+
+            [Test]
+            public void SendRange_Is_True_If_Range_Is_Correct_And_ifRange_Is_Null()
+            {
+                Request.Headers[HttpHeaders.Range] = "bytes=0-100";
+                Assert.IsTrue(new MockResumeFileResult(_file.FullName, ContentType, Request).SendRange());
+            }
+
+            [Test]
+            public void SendRange_Is_True_If_Range_And_ifRange_Are_Correct()
+            {
+                Request.Headers[HttpHeaders.IfRange] = ResumeFileResult.Util.Etag(_file);
+                Request.Headers[HttpHeaders.Range] = "bytes=0-100";
+                Assert.IsTrue(new MockResumeFileResult(_file.FullName, ContentType, Request).SendRange());
+            }
+
+            [Test]
+            public void SendRange_Is_False_If_Range_Is_Correct_But_ifRange_Is_InCorrect()
+            {
+                Request.Headers[HttpHeaders.IfRange] = "incorrect etag";
+                Request.Headers[HttpHeaders.Range] = "bytes=0-100";
+                Assert.IsFalse(new MockResumeFileResult(_file.FullName, ContentType, Request).SendRange());
+            }
+
+            [Test]
+            public void HeadersTest_Should_Not_Send_File_If_File_Has_Not_Been_Changed()
+            {
+                Request.Headers[HttpHeaders.IfNoneMatch] = ResumeFileResult.Util.Etag(_file);
+                var result = new MockResumeFileResult(_file.FullName, ContentType, Request);
+                Assert.IsTrue(result.IsNotModified());
+                result.WriteFileTest(Response);
+                Assert.AreEqual((int)HttpStatusCode.NotModified, Response.StatusCode);
+                Assert.IsNotNull(Response.Headers[HttpHeaders.Etag]);
+                Assert.IsNotNull(Response.Headers[HttpHeaders.Expires]);
+                Assert.IsNotNull(Response.Headers[HttpHeaders.LastModified]);
+                Assert.IsNull(Response.Headers[HttpHeaders.ContentRange]);
+                Assert.AreEqual(0, Response.OutputStream.Length);
+            }
+
+            [Test]
+            public void HeadersTest_Should_Not_Send_File_IfPreconditionFailed()
+            {
+                Request.Headers[HttpHeaders.IfMatch] = "invalid";
+                var result = new MockResumeFileResult(_file.FullName, ContentType, Request);
+                Assert.IsTrue(result.IsPreconditionFailed());
+
+                result.WriteFileTest(Response);
+                Assert.AreEqual((int)HttpStatusCode.PreconditionFailed, Response.StatusCode);
+                Assert.IsNotNull(Response.Headers[HttpHeaders.Etag]);
+                Assert.IsNotNull(Response.Headers[HttpHeaders.Expires]);
+                Assert.IsNotNull(Response.Headers[HttpHeaders.LastModified]);
+                Assert.IsNull(Response.Headers[HttpHeaders.ContentRange]);
+                Assert.AreEqual(0, Response.OutputStream.Length);
+            }
+
+            [Test]
+            public void HeadersTest_Should_Not_Send_File_Is_RangeNotSatisfiable()
+            {
+                Request.Headers[HttpHeaders.Range] = "invalid";
+                var result = new MockResumeFileResult(_file.FullName, ContentType, Request);
+                Assert.IsTrue(result.IsRangeNotSatisfiable());
+                result.WriteFileTest(Response);
+                Assert.AreEqual((int)HttpStatusCode.RequestedRangeNotSatisfiable, Response.StatusCode);
+                Assert.IsNotNull(Response.Headers[HttpHeaders.Etag]);
+                Assert.IsNotNull(Response.Headers[HttpHeaders.Expires]);
+                Assert.IsNotNull(Response.Headers[HttpHeaders.LastModified]);
+                Assert.AreEqual("bytes */" + _file.Length, Response.Headers[HttpHeaders.ContentRange]);
+                Assert.AreEqual(0, Response.OutputStream.Length);
+            }
+
+            [Test]
+            public void HeadersTest_Should_Send_File_If_All_Headers_Are_Null()
+            {
+                var result = new MockResumeFileResult(_file.FullName, ContentType, Request);
+                result.WriteFileTest(Response);
+                Assert.AreEqual((int)HttpStatusCode.OK, Response.StatusCode);
+                Assert.IsNotNull(Response.Headers[HttpHeaders.Etag]);
+                Assert.IsNotNull(Response.Headers[HttpHeaders.Expires]);
+                Assert.IsNotNull(Response.Headers[HttpHeaders.LastModified]);
+                Assert.IsNotNull(Response.Headers[HttpHeaders.ContentRange]);
+                Assert.IsNotNull(Response.Headers[HttpHeaders.ContentLength]);
+                Assert.AreEqual(_file.Length, Response.OutputStream.Length);
+            }
+
+            [Test]
+            public void Range_First_500b()
+            {
+                var stream = GetResponseStream("bytes=0-499");
+                Assert.AreEqual(500, stream.Length);
+                Assert.AreEqual(206, Response.StatusCode);
+                Assert.AreEqual($"bytes 0-499/{_file.Length}", Response.Headers[HttpHeaders.ContentRange]);
+            }
+
+            [Test]
+            public void Range_From_500b_to_899b()
+            {
+                var stream = GetResponseStream("bytes=500-899");
+                Assert.AreEqual(400, stream.Length);
+                Assert.AreEqual(206, Response.StatusCode);
+                Assert.AreEqual($"bytes 500-899/{_file.Length}", Response.Headers[HttpHeaders.ContentRange]);
+            }
+
+            [Test]
+            public void Range_Last_300b()
+            {
+                var stream = GetResponseStream("bytes=-300");
+                Assert.AreEqual(300, stream.Length);
+                Assert.AreEqual(206, Response.StatusCode);
+                var from = _file.Length - 300;
+                var to = _file.Length - 1;
+
+                Assert.AreEqual($"bytes {from}-{to}/{_file.Length}", Response.Headers[HttpHeaders.ContentRange]);
+            }
+
+            [Test]
+            public void Range_From_100b_toThe_End()
+            {
+                var stream = GetResponseStream($"bytes={(_file.Length - 100)}-");
+                Assert.AreEqual(100, stream.Length);
+                Assert.AreEqual(206, Response.StatusCode);
+                var from = _file.Length - 100;
+                var to = _file.Length - 1;
+                Assert.AreEqual($"bytes {from}-{to}/{_file.Length}", Response.Headers[HttpHeaders.ContentRange]);
+            }
+
+            [Test]
+            public void Range_First_1b()
+            {
+                var stream = GetResponseStream("bytes=0-0");
+                Assert.AreEqual(1, stream.Length);
+                Assert.AreEqual(206, Response.StatusCode);
+                Assert.AreEqual($"bytes 0-0/{_file.Length}", Response.Headers[HttpHeaders.ContentRange]);
+            }
+
+            [Test]
+            public void Range_Last_1b()
+            {
+                var stream = GetResponseStream("bytes=-1");
+                Assert.AreEqual(1, stream.Length);
+                Assert.AreEqual(206, Response.StatusCode);
+                var from = _file.Length - 1;
+                var to = _file.Length - 1;
+                Assert.AreEqual($"bytes {from}-{to}/{_file.Length}", Response.Headers[HttpHeaders.ContentRange]);
+            }
+
+            [Test]
+            public void Range_Whole_File_With_RangeHeader()
+            {
+                var stream = GetResponseStream("bytes=0-" + (_file.Length - 1));
+                Assert.AreEqual(_file.Length, stream.Length);
+                Assert.AreEqual(206, Response.StatusCode);
+                Assert.AreEqual($"bytes 0-{(_file.Length - 1)}/{_file.Length}", Response.Headers[HttpHeaders.ContentRange]);
+            }
+
+            [Test]
+            public void Range_Whole_File_Without_RangeHeader()
+            {
+                var stream = GetResponseStream(null);
+                Assert.AreEqual(_file.Length, stream.Length);
+                Assert.AreEqual(200, Response.StatusCode);
+                Assert.AreEqual($"bytes 0-{(_file.Length - 1)}/{_file.Length}", Response.Headers[HttpHeaders.ContentRange]);
+            }
+
+            [Test]
+            public void TransmissionRange_From_0_To_0()
+            {
+                Request.Headers[HttpHeaders.Range] = "bytes=0-0";
+                new MockResumeFileResult(_file.FullName, ContentType, Request).WriteFileTest(Response);
+
+                Assert.AreEqual(1, Response.OutputStream.Length);
+                AssertBytes(_file, Response.OutputStream, 0, 1);
+            }
+
+            [Test]
+            public void TransmissionRange_From_1_To_100()
+            {
+                Request.Headers[HttpHeaders.Range] = "bytes=1-100";
+                new MockResumeFileResult(_file.FullName, ContentType, Request).WriteFileTest(Response);
+
+                Assert.AreEqual(100, Response.OutputStream.Length);
+                AssertBytes(_file, Response.OutputStream, 1, 100);
+            }
+
+            [Test]
+            public void TransmissionRange_From_101_To_theEnd()
+            {
+                Request.Headers[HttpHeaders.Range] = "bytes=101-";
+                new MockResumeFileResult(_file.FullName, ContentType, Request).WriteFileTest(Response);
+
+                Assert.AreEqual(_file.Length - 101, Response.OutputStream.Length);
+                AssertBytes(_file, Response.OutputStream, 101, (int)_file.Length);
+            }
+
+            [Test]
+            public void TransmissionRange_WholeFile_WithRangeHeader()
+            {
+                Request.Headers[HttpHeaders.Range] = "bytes=0-";
+                new MockResumeFileResult(_file.FullName, ContentType, Request).WriteFileTest(Response);
+
+                Assert.AreEqual(_file.Length, Response.OutputStream.Length);
+                AssertBytes(_file, Response.OutputStream, 0, (int)_file.Length);
+            }
+
+            [Test]
+            public void TransmissionRange_WholeFile_WithoutRangeHeader()
+            {
+                Request.Headers[HttpHeaders.Range] = null;
+                new MockResumeFileResult(_file.FullName, ContentType, Request).WriteFileTest(Response);
+
+                Assert.AreEqual(_file.Length, Response.OutputStream.Length);
+                AssertBytes(_file, Response.OutputStream, 0, (int)_file.Length);
+            }
+
+            [Test]
+            public void ShouldSend206If_Range_HeaderExists()
+            {
+                Request.Headers[HttpHeaders.Range] = "bytes=0-";
+                new MockResumeFileResult(_file.FullName, ContentType, Request).WriteFileTest(Response);
+                Assert.AreEqual(206, Response.StatusCode);
+            }
+
+            [Test]
+            public void ShouldSend200If_Range_HeaderDoesNotExist()
+            {
+                Request.Headers[HttpHeaders.Range] = null;
+                new MockResumeFileResult(_file.FullName, ContentType, Request).WriteFileTest(Response);
+                Assert.AreEqual(200, Response.StatusCode);
+            }
+
+            [Test]
+            public void IfRangeHeader_Should_Be_Ignored_If_ItNotEquals_Etag()
+            {
+                Request.Headers[HttpHeaders.IfRange] = "ifRange fake header";
+                var mock = new MockResumeFileResult(_file.FullName, ContentType, Request);
+                mock.WriteFileTest(Response);
+
+                Assert.AreNotEqual(ResumeFileResult.Util.Etag(_file), Request.Headers[HttpHeaders.IfRange]);
+                Assert.AreEqual(200, Response.StatusCode);
+            }
+
+            [Test]
+            public void Etag_Should_Be_Added_To_Response_If_It_Equals_With_IfRange_In_Request()
+            {
+                var etag = ResumeFileResult.Util.Etag(_file);
+                Request.Headers[HttpHeaders.IfRange] = etag;
+                var mock = new MockResumeFileResult(_file.FullName, ContentType, Request);
+                mock.WriteFileTest(Response);
+                Assert.AreEqual(Response.Headers[HttpHeaders.Etag], etag);
+                Assert.AreEqual(200, Response.StatusCode);
+            }
+
+            [Test]
+            public void Etag_Should_Be_Added_To_Response_If_It_Equals_With_IfRange_In_Request__PartialResponse()
+            {
+                var etag = ResumeFileResult.Util.Etag(_file);
+                Request.Headers[HttpHeaders.IfRange] = etag;
+                Request.Headers[HttpHeaders.Range] = "bytes=0-";
+                new MockResumeFileResult(_file.FullName, ContentType, Request).WriteFileTest(Response);
+                Assert.AreEqual(Response.Headers[HttpHeaders.Etag], etag);
+                Assert.IsNotNull(Response.Headers[HttpHeaders.ContentRange]);
+                Assert.AreEqual(206, Response.StatusCode);
+            }
+
+            [Test]
+            public void It_Should_Attach_Content_Disposition_If_There_Is_Download_File_Name()
+            {
+                new MockResumeFileResult(_file.FullName, ContentType, Request, "test.name").WriteFileTest(Response);
+                Assert.IsNotNull(Response.Headers[HttpHeaders.ContentDisposition]);
+            }
+
+            private System.IO.Stream GetResponseStream(string range)
+            {
+                Response.ClearTestResponse();
+                Response.StatusCode = 500;
+
+                Request.Headers[HttpHeaders.Range] = range;
+                new MockResumeFileResult(_file.FullName, ContentType, Request).WriteFileTest(Response);
+
+                return Response.OutputStream;
+            }
+
+            private void AssertBytes(FileInfo file, System.IO.Stream responseStream, int from, int to)
+            {
+                using (var fileStream = file.OpenRead())
+                {
+                    responseStream.Seek(0, SeekOrigin.Begin);
+                    fileStream.Seek(from, SeekOrigin.Begin);
+                    for (var byteIndex = from; byteIndex < to; byteIndex++)
+                    {
+                        var responseByte = responseStream.ReadByte();
+                        var fileByte = fileStream.ReadByte();
+                        Assert.AreEqual(responseByte, fileByte);
+                    }
+                }
+            }
+        }
+    }
+}

+ 101 - 0
Masuit.Tools.UnitTest/Resources/download-test-file.txt

@@ -0,0 +1,101 @@
+This file has been created just for test. Please, do not remove it.
+00000000-0000-0000-0000-000000000000
+6F73BE88-F7C7-4E1A-87A7-00065CF6E89A
+5CE496D9-404C-495A-85B8-0009C5138958
+09C1297A-018E-45AB-B553-000BB8F162F9
+F177BD62-D095-419B-B7FA-000DB0D1892F
+51332D10-6790-4CC8-9C37-000ED0DB0083
+833E1983-7733-4C2D-B80D-0012C254CE81
+AB839D03-EC0C-4407-92FE-00199ACFD67B
+2E16F160-8A37-406A-AF6D-001A8CF332E8
+1D4E04D8-6087-4E60-8FE4-001B86DB3D57
+B52B65A6-013F-4A8C-80A2-001CB4F59E0E
+E0FBB808-F0F3-4812-8047-00207907B015
+58AC5AA0-E298-45F2-8785-00227DC7FB87
+E59B7950-65A8-4CE8-B6F3-00255EEEACE2
+37F172C7-5FCE-47E8-846E-00296C4FF216
+A788DE28-2BAF-48CD-A07D-00307BF4186D
+F9E01B9F-05F5-4CE3-B4B7-0031E65D6F58
+36EFC066-4B2A-4E97-B169-003CBA3C3214
+F1A45C6F-0A5A-4C1D-99DF-003E087D4A4A
+443210D7-5F78-4C89-9FE7-0043B39A92D4
+EDF5F2F1-EE31-4B47-B422-00478A6E5180
+2508BDCA-D0CB-4FD9-9E62-0049103F96E2
+1B679CE3-9F2C-4377-893B-004A97FE23E7
+351B8869-558D-4537-9B3B-004B2805869B
+26DC5ECD-E801-4F12-9BEE-004DAC6646B6
+8394074B-48EB-46C1-9C03-00503A8CC622
+A31CFF47-BDBD-4ADE-B3CB-0053D2F7CC84
+A794AC36-188B-461D-AD3D-00589AFCA911
+75549534-B55E-4DD8-A536-006224D96110
+B6982FA1-A6B3-4302-8E03-0062B05FD781
+C059086E-1142-4C29-9DA8-00645674B9D0
+4D9DBAA3-05D4-47CF-A755-00668A0EAC22
+29D6A3EF-280D-48DE-9C6A-0067F264F3EA
+86AA543B-5BF3-42D9-B91D-006AC5551E63
+FB62F575-C8AC-498D-9C99-006C2D4DAF1C
+018350D4-BDBC-4471-8101-006D0B99E6BA
+E9621011-9EE5-4205-AD5E-006D30B95FF0
+810C102C-B727-4F94-BC0D-006D74A19A02
+730486D5-2D54-4005-AE67-006E7895FA8C
+09BF434C-B4E6-414B-B4D2-00739E406221
+8F5906D7-A9D0-4594-83FE-0073A4190B6D
+3FC7EF6F-8E08-41D4-A36B-007A71ABC63D
+14E28E37-6C92-4219-BAF5-007E5BEBB6CF
+2FF432C4-DF05-45C4-A6CA-0080CC62D070
+4E98B6A2-36A2-403B-A9FB-008589C37AF9
+776A53F7-49BF-4DAD-BAEA-0086F38FB7DE
+99A66723-5D26-4013-8066-008733118814
+CF8094F2-4D35-4CE9-B577-0087E43AB7CB
+DA403697-83D8-4C35-80F7-008A44644A80
+E66600F6-93F9-4832-9AB7-008EB19E47D6
+1A9B4EFD-2B2C-4F05-A8FE-0091E8E114DB
+425D495F-A440-400E-B945-0092A8594052
+B0381374-B36F-4EF8-BC13-0093FD811712
+11FA240E-0DCE-4E22-A303-0094A1C6FEAD
+A2CA89FD-C6F8-4082-9882-0094E56F3EC7
+59820C12-2B73-4CC8-8520-0098319953BD
+2DE350E0-F84A-4810-9C29-0099607E02EB
+D1167E7C-42EC-4396-B7F9-0099A40FEC3C
+6A2774F0-9F50-4AF6-8B86-009C4CFBF5E1
+757402EC-252B-4C92-9B0B-009E5A5CEC4C
+A5623EF4-E476-45B8-8D0D-00A0FB9DEE76
+3BB1154A-D3C9-47D8-A73D-00A53992CBB9
+F37488BE-F600-491F-B462-00A559A262E6
+608B451B-4E9B-4DCD-99FE-00A5782FC9B0
+FF4F8389-5047-42EA-9218-00A610A2F67B
+3F738079-5301-4DE5-A696-00A621B8898D
+89F30700-3059-4FB1-A6F4-00A6D47CD68C
+BF29DCA4-F260-470A-B3E2-00A766FC1666
+700C29D8-C55F-474F-8636-00A827918F11
+8E91FE60-7A60-476E-A439-00A87E3FC2F5
+AF8D9AB9-47C8-4416-B844-00A889C5B3D8
+60F9F4A1-EEE4-4968-9D1F-00AF3E4DF785
+2FD09FB9-10B9-42EF-8CF6-00B2CCD7B389
+ED2C080D-9677-4AEE-854B-00B3318B72B0
+4F736871-12EA-4551-BFDF-00B69E8B2145
+24AB299B-7A45-4358-96E1-00B98A740218
+A5EFF2C8-7A19-4411-93DB-00B99D63BE66
+0B2352D9-3E61-46F8-83D5-00BAD0488768
+DD0FA0AF-4452-44AA-B201-00BF45534926
+7DBC363F-538F-4C4E-A289-00C030EC6752
+723F7306-1961-4621-AAA5-00C0EE79AE13
+139C07C4-29C2-4BD0-B076-00C15186D1E9
+4AD0A31D-6DE6-4CDA-84C2-00C241DE17B4
+3E5CFEE0-1766-49A5-AD1B-00C35AFC02E8
+1AE381CA-B4B3-459E-849F-00C5347B998D
+31C99B51-A867-4C7E-8A55-00CBCEE2F5AB
+6C1AE16E-78FF-40CA-9BE0-00D1E018A1D0
+24DE3E95-F798-435F-83D1-00D25CCDFB97
+6C7B94D0-E87E-4D4E-98E2-00D4CE4F8F58
+E22CF6FD-4F14-4300-BD39-00DA155264DC
+2D090519-6EDF-4378-9290-00DAB484432F
+5DFAA4F1-74A7-4583-B8DB-00E1A0D12E8F
+5F66986F-31AA-472B-A19E-00E1C839EE5C
+C3363650-FF03-4D88-BF5A-00E4AC4AAAF1
+735637B9-5B74-457F-BAC0-00E8C9EA4A35
+3CBCEFC2-7381-4905-9757-00E9A5246D95
+8DFE8B49-716D-4414-BC05-00EA6D24D278
+592F9F3A-C114-4467-AB7B-00EF119EA0AB
+DD2314EB-A3DF-48CF-8048-00F047D39889
+7021D545-489C-45BF-BE86-00F61719339E

+ 101 - 0
Masuit.Tools.UnitTest/Resources/download-test-file2.txt

@@ -0,0 +1,101 @@
+This file has been created just for test. Please, do not remove it.
+00000000-0000-0000-0000-000000000000
+6F73BE88-F7C7-4E1A-87A7-00065CF6E89A
+5CE496D9-404C-495A-85B8-0009C5138958
+09C1297A-018E-45AB-B553-000BB8F162F9
+F177BD62-D095-419B-B7FA-000DB0D1892F
+51332D10-6790-4CC8-9C37-000ED0DB0083
+833E1983-7733-4C2D-B80D-0012C254CE81
+AB839D03-EC0C-4407-92FE-00199ACFD67B
+2E16F160-8A37-406A-AF6D-001A8CF332E8
+1D4E04D8-6087-4E60-8FE4-001B86DB3D57
+B52B65A6-013F-4A8C-80A2-001CB4F59E0E
+E0FBB808-F0F3-4812-8047-00207907B015
+58AC5AA0-E298-45F2-8785-00227DC7FB87
+E59B7950-65A8-4CE8-B6F3-00255EEEACE2
+37F172C7-5FCE-47E8-846E-00296C4FF216
+A788DE28-2BAF-48CD-A07D-00307BF4186D
+F9E01B9F-05F5-4CE3-B4B7-0031E65D6F58
+36EFC066-4B2A-4E97-B169-003CBA3C3214
+F1A45C6F-0A5A-4C1D-99DF-003E087D4A4A
+443210D7-5F78-4C89-9FE7-0043B39A92D4
+EDF5F2F1-EE31-4B47-B422-00478A6E5180
+2508BDCA-D0CB-4FD9-9E62-0049103F96E2
+1B679CE3-9F2C-4377-893B-004A97FE23E7
+351B8869-558D-4537-9B3B-004B2805869B
+26DC5ECD-E801-4F12-9BEE-004DAC6646B6
+8394074B-48EB-46C1-9C03-00503A8CC622
+A31CFF47-BDBD-4ADE-B3CB-0053D2F7CC84
+A794AC36-188B-461D-AD3D-00589AFCA911
+75549534-B55E-4DD8-A536-006224D96110
+B6982FA1-A6B3-4302-8E03-0062B05FD781
+C059086E-1142-4C29-9DA8-00645674B9D0
+4D9DBAA3-05D4-47CF-A755-00668A0EAC22
+29D6A3EF-280D-48DE-9C6A-0067F264F3EA
+86AA543B-5BF3-42D9-B91D-006AC5551E63
+FB62F575-C8AC-498D-9C99-006C2D4DAF1C
+018350D4-BDBC-4471-8101-006D0B99E6BA
+E9621011-9EE5-4205-AD5E-006D30B95FF0
+810C102C-B727-4F94-BC0D-006D74A19A02
+730486D5-2D54-4005-AE67-006E7895FA8C
+09BF434C-B4E6-414B-B4D2-00739E406221
+8F5906D7-A9D0-4594-83FE-0073A4190B6D
+3FC7EF6F-8E08-41D4-A36B-007A71ABC63D
+14E28E37-6C92-4219-BAF5-007E5BEBB6CF
+2FF432C4-DF05-45C4-A6CA-0080CC62D070
+4E98B6A2-36A2-403B-A9FB-008589C37AF9
+776A53F7-49BF-4DAD-BAEA-0086F38FB7DE
+99A66723-5D26-4013-8066-008733118814
+CF8094F2-4D35-4CE9-B577-0087E43AB7CB
+DA403697-83D8-4C35-80F7-008A44644A80
+E66600F6-93F9-4832-9AB7-008EB19E47D6
+1A9B4EFD-2B2C-4F05-A8FE-0091E8E114DB
+425D495F-A440-400E-B945-0092A8594052
+B0381374-B36F-4EF8-BC13-0093FD811712
+11FA240E-0DCE-4E22-A303-0094A1C6FEAD
+A2CA89FD-C6F8-4082-9882-0094E56F3EC7
+59820C12-2B73-4CC8-8520-0098319953BD
+2DE350E0-F84A-4810-9C29-0099607E02EB
+D1167E7C-42EC-4396-B7F9-0099A40FEC3C
+6A2774F0-9F50-4AF6-8B86-009C4CFBF5E1
+757402EC-252B-4C92-9B0B-009E5A5CEC4C
+A5623EF4-E476-45B8-8D0D-00A0FB9DEE76
+3BB1154A-D3C9-47D8-A73D-00A53992CBB9
+F37488BE-F600-491F-B462-00A559A262E6
+608B451B-4E9B-4DCD-99FE-00A5782FC9B0
+FF4F8389-5047-42EA-9218-00A610A2F67B
+3F738079-5301-4DE5-A696-00A621B8898D
+89F30700-3059-4FB1-A6F4-00A6D47CD68C
+BF29DCA4-F260-470A-B3E2-00A766FC1666
+700C29D8-C55F-474F-8636-00A827918F11
+8E91FE60-7A60-476E-A439-00A87E3FC2F5
+AF8D9AB9-47C8-4416-B844-00A889C5B3D8
+60F9F4A1-EEE4-4968-9D1F-00AF3E4DF785
+2FD09FB9-10B9-42EF-8CF6-00B2CCD7B389
+ED2C080D-9677-4AEE-854B-00B3318B72B0
+4F736871-12EA-4551-BFDF-00B69E8B2145
+24AB299B-7A45-4358-96E1-00B98A740218
+A5EFF2C8-7A19-4411-93DB-00B99D63BE66
+0B2352D9-3E61-46F8-83D5-00BAD0488768
+DD0FA0AF-4452-44AA-B201-00BF45534926
+7DBC363F-538F-4C4E-A289-00C030EC6752
+723F7306-1961-4621-AAA5-00C0EE79AE13
+139C07C4-29C2-4BD0-B076-00C15186D1E9
+4AD0A31D-6DE6-4CDA-84C2-00C241DE17B4
+3E5CFEE0-1766-49A5-AD1B-00C35AFC02E8
+1AE381CA-B4B3-459E-849F-00C5347B998D
+31C99B51-A867-4C7E-8A55-00CBCEE2F5AB
+6C1AE16E-78FF-40CA-9BE0-00D1E018A1D0
+24DE3E95-F798-435F-83D1-00D25CCDFB97
+6C7B94D0-E87E-4D4E-98E2-00D4CE4F8F58
+E22CF6FD-4F14-4300-BD39-00DA155264DC
+2D090519-6EDF-4378-9290-00DAB484432F
+5DFAA4F1-74A7-4583-B8DB-00E1A0D12E8F
+5F66986F-31AA-472B-A19E-00E1C839EE5C
+C3363650-FF03-4D88-BF5A-00E4AC4AAAF1
+735637B9-5B74-457F-BAC0-00E8C9EA4A35
+3CBCEFC2-7381-4905-9757-00E9A5246D95
+8DFE8B49-716D-4414-BC05-00EA6D24D278
+592F9F3A-C114-4467-AB7B-00EF119EA0AB
+DD2314EB-A3DF-48CF-8048-00F047D39889
+7021D545-489C-45BF-BE86-00F61719339E

+ 9 - 0
Masuit.Tools.UnitTest/packages.config

@@ -1,7 +1,16 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
+  <package id="Castle.Core" version="4.3.1" targetFramework="net461" />
+  <package id="Microsoft.AspNet.Mvc" version="5.2.7" targetFramework="net461" />
+  <package id="Microsoft.AspNet.Razor" version="3.2.7" targetFramework="net461" />
+  <package id="Microsoft.AspNet.WebPages" version="3.2.7" targetFramework="net461" />
+  <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net461" />
+  <package id="Moq" version="4.10.1" targetFramework="net461" />
   <package id="MSTest.TestAdapter" version="1.4.0" targetFramework="net461" />
   <package id="MSTest.TestFramework" version="1.4.0" targetFramework="net461" />
+  <package id="NUnit" version="3.11.0" targetFramework="net461" />
+  <package id="System.Runtime.CompilerServices.Unsafe" version="4.5.0" targetFramework="net461" />
+  <package id="System.Threading.Tasks.Extensions" version="4.5.1" targetFramework="net461" />
   <package id="xunit" version="2.4.1" targetFramework="net461" />
   <package id="xunit.abstractions" version="2.0.3" targetFramework="net461" />
   <package id="xunit.analyzers" version="0.10.0" targetFramework="net461" />

+ 15 - 0
Masuit.Tools/Extensions.cs

@@ -1155,6 +1155,21 @@ namespace Masuit.Tools
             return (int)Math.Floor(num);
         }
 
+        /// <summary>
+        /// 字符串转long类型
+        /// </summary>
+        /// <param name="str"></param>
+        /// <param name="defaultResult">转换失败的默认值</param>
+        /// <returns></returns>
+        public static long ToLong(this string str, long defaultResult)
+        {
+            if (!long.TryParse(str, out var result))
+            {
+                result = defaultResult;
+            }
+            return result;
+        }
+
         /// <summary>
         /// 将int转换成double
         /// </summary>

+ 29 - 0
Masuit.Tools/Masuit.Tools.csproj

@@ -48,6 +48,9 @@
       <HintPath>..\packages\SharpZipLib.1.1.0\lib\net45\ICSharpCode.SharpZipLib.dll</HintPath>
     </Reference>
     <Reference Include="Microsoft.CSharp" />
+    <Reference Include="Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath>
+    </Reference>
     <Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
       <HintPath>..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
     </Reference>
@@ -65,6 +68,24 @@
     <Reference Include="System.Net" />
     <Reference Include="System.Net.Http" />
     <Reference Include="System.Web" />
+    <Reference Include="System.Web.Helpers, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.Helpers.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.Mvc, Version=5.2.7.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.Mvc.5.2.7\lib\net45\System.Web.Mvc.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.Razor.3.2.7\lib\net45\System.Web.Razor.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.WebPages, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.WebPages.Deployment, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.Deployment.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.Razor.dll</HintPath>
+    </Reference>
     <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
@@ -101,6 +122,14 @@
     <Compile Include="Models\IspInfo.cs" />
     <Compile Include="Models\PhysicsAddress.cs" />
     <Compile Include="Models\TaobaoIP.cs" />
+    <Compile Include="Mvc\ControllerExtension.cs" />
+    <Compile Include="Mvc\HttpHeaders.cs" />
+    <Compile Include="Mvc\Internal\Range.cs" />
+    <Compile Include="Mvc\Mime\DefaultMimeItems.cs" />
+    <Compile Include="Mvc\Mime\IMimeMapper.cs" />
+    <Compile Include="Mvc\Mime\MimeMapper.cs" />
+    <Compile Include="Mvc\Mime\MimeMappingItem.cs" />
+    <Compile Include="Mvc\ResumeFileResult.cs" />
     <Compile Include="Net\CacheHelper.cs" />
     <Compile Include="Net\CookieHelper.cs" />
     <Compile Include="Net\FtpClient.cs" />

+ 92 - 0
Masuit.Tools/Mvc/ControllerExtension.cs

@@ -0,0 +1,92 @@
+using System.Web.Mvc;
+
+namespace Masuit.Tools.Mvc
+{
+    public static class ControllerExtension
+    {
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="virtualPath">服务端本地文件的虚拟路径</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <returns></returns>
+        public static ResumeFileResult ResumeFile(this ControllerBase controller, string virtualPath, string contentType)
+        {
+            return ResumeFile(controller, virtualPath, contentType, fileDownloadName: null);
+        }
+
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="virtualPath">服务端本地文件的虚拟路径</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="fileDownloadName">下载的文件名</param>
+        /// <returns></returns>
+        public static ResumeFileResult ResumeFile(this ControllerBase controller, string virtualPath, string contentType, string fileDownloadName)
+        {
+            return ResumeFile(controller, virtualPath, contentType, fileDownloadName, etag: null);
+        }
+
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="virtualPath">服务端本地文件的虚拟路径</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="fileDownloadName">下载的文件名</param>
+        /// <param name="etag">ETag</param>
+        /// <returns></returns>
+        public static ResumeFileResult ResumeFile(this ControllerBase controller, string virtualPath, string contentType, string fileDownloadName, string etag)
+        {
+            string physicalPath = controller.ControllerContext.HttpContext.Request.MapPath(virtualPath);
+            return new ResumeFileResult(physicalPath, contentType, controller.ControllerContext.HttpContext.Request)
+            {
+                FileDownloadName = fileDownloadName
+            };
+        }
+
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="physicalPath">服务端本地文件的物理路径</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <returns></returns>
+        public static ResumeFileResult ResumePhysicalFile(this ControllerBase controller, string physicalPath, string contentType)
+        {
+            return ResumePhysicalFile(controller, physicalPath, contentType, fileDownloadName: null, etag: null);
+        }
+
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="physicalPath">服务端本地文件的物理路径</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="fileDownloadName">下载的文件名</param>
+        /// <returns></returns>
+        public static ResumeFileResult ResumePhysicalFile(this ControllerBase controller, string physicalPath, string contentType, string fileDownloadName)
+        {
+            return ResumePhysicalFile(controller, physicalPath, contentType, fileDownloadName, etag: null);
+        }
+
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="physicalPath">服务端本地文件的物理路径</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="fileDownloadName">下载的文件名</param>
+        /// <param name="etag">ETag</param>
+        /// <returns></returns>
+        public static ResumeFileResult ResumePhysicalFile(this ControllerBase controller, string physicalPath, string contentType, string fileDownloadName, string etag)
+        {
+            return new ResumeFileResult(physicalPath, contentType, controller.ControllerContext.HttpContext.Request)
+            {
+                FileDownloadName = fileDownloadName
+            };
+        }
+    }
+}

+ 21 - 0
Masuit.Tools/Mvc/HttpHeaders.cs

@@ -0,0 +1,21 @@
+namespace Masuit.Tools.Mvc
+{
+    public static class HttpHeaders
+    {
+        public static readonly string AcceptRanges = "Accept-Ranges";
+        public static readonly string CacheControl = "Cache-Control";
+        public static readonly string ContentLength = "Content-Length";
+        public static readonly string ContentDisposition = "Content-Disposition";
+        public static readonly string ContentType = "Content-Type";
+        public static readonly string Expires = "Expires";
+        public static readonly string LastModified = "Last-Modified";
+        public static readonly string Etag = "ETag";
+        public static readonly string IfNoneMatch = "If-None-Match";
+        public static readonly string IfModifiedSince = "If-Modified-Since";
+        public static readonly string IfMatch = "If-Match";
+        public static readonly string IfUnmodifiedSince = "If-Unmodified-Since";
+        public static readonly string Range = "Range";
+        public static readonly string IfRange = "If-Range";
+        public static readonly string ContentRange = "Content-Range";
+    }
+}

+ 8 - 0
Masuit.Tools/Mvc/Internal/Range.cs

@@ -0,0 +1,8 @@
+namespace Masuit.Tools.Mvc.Internal
+{
+    internal class Range
+    {
+        public long Start { get; set; }
+        public long End { get; set; }
+    }
+}

+ 214 - 0
Masuit.Tools/Mvc/Mime/DefaultMimeItems.cs

@@ -0,0 +1,214 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace Masuit.Tools.Mvc.Mime
+{
+    internal static class DefaultMimeItems
+    {
+        public static ReadOnlyCollection<MimeMappingItem> Items = new List<MimeMappingItem>
+        {
+            new MimeMappingItem{Extension = "abs", MimeType = "audio/x-mpeg"},
+            new MimeMappingItem{Extension = "ai", MimeType = "application/postscript"},
+            new MimeMappingItem{Extension = "aif", MimeType = "audio/x-aiff"},
+            new MimeMappingItem{Extension = "aifc", MimeType = "audio/x-aiff"},
+            new MimeMappingItem{Extension = "aiff", MimeType = "audio/x-aiff"},
+            new MimeMappingItem{Extension = "aim", MimeType = "application/x-aim"},
+            new MimeMappingItem{Extension = "art", MimeType = "image/x-jg"},
+            new MimeMappingItem{Extension = "asf", MimeType = "video/x-ms-asf"},
+            new MimeMappingItem{Extension = "asx", MimeType = "video/x-ms-asf"},
+            new MimeMappingItem{Extension = "au", MimeType = "audio/basic"},
+            new MimeMappingItem{Extension = "avi", MimeType = "video/x-msvideo"},
+            new MimeMappingItem{Extension = "avx", MimeType = "video/x-rad-screenplay"},
+            new MimeMappingItem{Extension = "bcpio", MimeType = "application/x-bcpio"},
+            new MimeMappingItem{Extension = "bin", MimeType = "application/octet-stream"},
+            new MimeMappingItem{Extension = "bmp", MimeType = "image/bmp"},
+            new MimeMappingItem{Extension = "body", MimeType = "text/html"},
+            new MimeMappingItem{Extension = "cdf", MimeType = "application/x-cdf"},
+            new MimeMappingItem{Extension = "cer", MimeType = "application/x-x509-ca-cert"},
+            new MimeMappingItem{Extension = "class", MimeType = "application/java"},
+            new MimeMappingItem{Extension = "cpio", MimeType = "application/x-cpio"},
+            new MimeMappingItem{Extension = "csh", MimeType = "application/x-csh"},
+            new MimeMappingItem{Extension = "css", MimeType = "text/css"},
+            new MimeMappingItem{Extension = "dib", MimeType = "image/bmp"},
+            new MimeMappingItem{Extension = "doc", MimeType = "application/msword"},
+            new MimeMappingItem{Extension = "dtd", MimeType = "application/xml-dtd"},
+            new MimeMappingItem{Extension = "dv", MimeType = "video/x-dv"},
+            new MimeMappingItem{Extension = "dvi", MimeType = "application/x-dvi"},
+            new MimeMappingItem{Extension = "eps", MimeType = "application/postscript"},
+            new MimeMappingItem{Extension = "etx", MimeType = "text/x-setext"},
+            new MimeMappingItem{Extension = "exe", MimeType = "application/octet-stream"},
+            new MimeMappingItem{Extension = "gif", MimeType = "image/gif"},
+            new MimeMappingItem{Extension = "gtar", MimeType = "application/x-gtar"},
+            new MimeMappingItem{Extension = "gz", MimeType = "application/x-gzip"},
+            new MimeMappingItem{Extension = "ogv", MimeType = "video/ogg"},
+            new MimeMappingItem{Extension = "oga", MimeType = "audio/ogg"},
+            new MimeMappingItem{Extension = "ogg", MimeType = "audio/ogg"},
+            new MimeMappingItem{Extension = "hdf", MimeType = "application/x-hdf"},
+            new MimeMappingItem{Extension = "htc", MimeType = "text/x-component"},
+            new MimeMappingItem{Extension = "htm", MimeType = "text/html"},
+            new MimeMappingItem{Extension = "html", MimeType = "text/html"},
+            new MimeMappingItem{Extension = "hqx", MimeType = "application/mac-binhex40"},
+            new MimeMappingItem{Extension = "ief", MimeType = "image/ief"},
+            new MimeMappingItem{Extension = "jad", MimeType = "text/vnd.sun.j2me.app-descriptor"},
+            new MimeMappingItem{Extension = "jar", MimeType = "application/java-archive"},
+            new MimeMappingItem{Extension = "java", MimeType = "text/plain"},
+            new MimeMappingItem{Extension = "jnlp", MimeType = "application/x-java-jnlp-file"},
+            new MimeMappingItem{Extension = "jpe", MimeType = "image/jpeg"},
+            new MimeMappingItem{Extension = "jpeg", MimeType = "image/jpeg"},
+            new MimeMappingItem{Extension = "jpg", MimeType = "image/jpeg"},
+            new MimeMappingItem{Extension = "js", MimeType = "text/javascript"},
+            new MimeMappingItem{Extension = "jsf", MimeType = "text/plain"},
+            new MimeMappingItem{Extension = "jspf", MimeType = "text/plain"},
+            new MimeMappingItem{Extension = "kar", MimeType = "audio/x-midi"},
+            new MimeMappingItem{Extension = "latex", MimeType = "application/x-latex"},
+            new MimeMappingItem{Extension = "m3u", MimeType = "audio/x-mpegurl"},
+            new MimeMappingItem{Extension = "mac", MimeType = "image/x-macpaint"},
+            new MimeMappingItem{Extension = "man", MimeType = "application/x-troff-man"},
+            new MimeMappingItem{Extension = "mathml", MimeType = "application/mathml+xml"},
+            new MimeMappingItem{Extension = "me", MimeType = "application/x-troff-me"},
+            new MimeMappingItem{Extension = "mid", MimeType = "audio/x-midi"},
+            new MimeMappingItem{Extension = "midi", MimeType = "audio/x-midi"},
+            new MimeMappingItem{Extension = "mif", MimeType = "application/x-mif"},
+            new MimeMappingItem{Extension = "mov", MimeType = "video/quicktime"},
+            new MimeMappingItem{Extension = "movie", MimeType = "video/x-sgi-movie"},
+            new MimeMappingItem{Extension = "mp1", MimeType = "audio/x-mpeg"},
+            new MimeMappingItem{Extension = "mp2", MimeType = "audio/x-mpeg"},
+            new MimeMappingItem{Extension = "mp3", MimeType = "audio/x-mpeg"},
+            new MimeMappingItem{Extension = "mp4", MimeType = "video/mp4"},
+            new MimeMappingItem{Extension = "mpa", MimeType = "audio/x-mpeg"},
+            new MimeMappingItem{Extension = "mpe", MimeType = "video/mpeg"},
+            new MimeMappingItem{Extension = "mpeg", MimeType = "video/mpeg"},
+            new MimeMappingItem{Extension = "mpega", MimeType = "audio/x-mpeg"},
+            new MimeMappingItem{Extension = "mpg", MimeType = "video/mpeg"},
+            new MimeMappingItem{Extension = "mpv2", MimeType = "video/mpeg2"},
+            new MimeMappingItem{Extension = "ms", MimeType = "application/x-wais-source"},
+            new MimeMappingItem{Extension = "nc", MimeType = "application/x-netcdf"},
+            new MimeMappingItem{Extension = "oda", MimeType = "application/oda"},
+            
+            //<!-- OpenDocument Database -->
+            new MimeMappingItem{Extension = "odb", MimeType = "application/vnd.oasis.opendocument.database"},
+            
+            //<!-- OpenDocument Chart -->
+            new MimeMappingItem{Extension = "odc", MimeType = "application/vnd.oasis.opendocument.chart"},
+    
+            //<!-- OpenDocument Formula -->
+            new MimeMappingItem{Extension = "odf", MimeType = "application/vnd.oasis.opendocument.formula"},
+    
+            //<!-- OpenDocument Drawing -->
+            new MimeMappingItem{Extension = "odg", MimeType = "application/vnd.oasis.opendocument.graphics"},
+    
+            //<!-- OpenDocument Image -->
+            new MimeMappingItem{Extension = "odi", MimeType = "application/vnd.oasis.opendocument.image"},
+    
+            //<!-- OpenDocument Master Document -->
+            new MimeMappingItem{Extension = "odm", MimeType = "application/vnd.oasis.opendocument.text-master"},
+    
+            //<!-- OpenDocument Presentation -->
+            new MimeMappingItem{Extension = "odp", MimeType = "application/vnd.oasis.opendocument.presentation"},
+    
+            //<!-- OpenDocument Spreadsheet -->
+            new MimeMappingItem{Extension = "ods", MimeType = "application/vnd.oasis.opendocument.spreadsheet"},
+    
+            //<!-- OpenDocument Text -->
+            new MimeMappingItem{Extension = "odt", MimeType = "application/vnd.oasis.opendocument.text"},
+    
+            //<!-- OpenDocument Drawing Template -->
+            new MimeMappingItem{Extension = "otg ", MimeType = "application/vnd.oasis.opendocument.graphics-template"},
+    
+            //<!-- HTML Document Template -->
+            new MimeMappingItem{Extension = "oth", MimeType = "application/vnd.oasis.opendocument.text-web"},
+    
+            //<!-- OpenDocument Presentation Template -->
+            new MimeMappingItem{Extension = "otp", MimeType = "application/vnd.oasis.opendocument.presentation-template"},
+    
+            //<!-- OpenDocument Spreadsheet Template -->
+            new MimeMappingItem{Extension = "ots", MimeType = "application/vnd.oasis.opendocument.spreadsheet-template "},
+    
+            //<!-- OpenDocument Text Template -->
+            new MimeMappingItem{Extension = "ott", MimeType = "application/vnd.oasis.opendocument.text-template"},
+            new MimeMappingItem{Extension = "pbm", MimeType = "image/x-portable-bitmap"},
+            new MimeMappingItem{Extension = "pct", MimeType = "image/pict"},
+            new MimeMappingItem{Extension = "pdf", MimeType = "application/pdf"},
+            new MimeMappingItem{Extension = "pgm", MimeType = "image/x-portable-graymap"},
+            new MimeMappingItem{Extension = "pic", MimeType = "image/pict"},
+            new MimeMappingItem{Extension = "pict", MimeType = "image/pict"},
+            new MimeMappingItem{Extension = "pls", MimeType = "audio/x-scpls"},
+            new MimeMappingItem{Extension = "png", MimeType = "image/png"},
+            new MimeMappingItem{Extension = "pnm", MimeType = "image/x-portable-anymap"},
+            new MimeMappingItem{Extension = "pnt", MimeType = "image/x-macpaint"},
+            new MimeMappingItem{Extension = "ppm", MimeType = "image/x-portable-pixmap"},
+            new MimeMappingItem{Extension = "ppt", MimeType = "application/vnd.ms-powerpoint"},
+            new MimeMappingItem{Extension = "pps", MimeType = "application/vnd.ms-powerpoint"},
+            new MimeMappingItem{Extension = "ps", MimeType = "application/postscript"},
+            new MimeMappingItem{Extension = "psd", MimeType = "image/x-photoshop"},
+            new MimeMappingItem{Extension = "qt", MimeType = "video/quicktime"},
+            new MimeMappingItem{Extension = "qti", MimeType = "image/x-quicktime"},
+            new MimeMappingItem{Extension = "qtif", MimeType = "image/x-quicktime"},
+            new MimeMappingItem{Extension = "ras", MimeType = "image/x-cmu-raster"},
+            new MimeMappingItem{Extension = "rdf", MimeType = "application/rdf+xml"},
+            new MimeMappingItem{Extension = "rgb", MimeType = "image/x-rgb"},
+            new MimeMappingItem{Extension = "rm", MimeType = "application/vnd.rn-realmedia"},
+            new MimeMappingItem{Extension = "roff", MimeType = "application/x-troff"},
+            new MimeMappingItem{Extension = "rtf", MimeType = "application/rtf"},
+            new MimeMappingItem{Extension = "rtx", MimeType = "text/richtext"},
+            new MimeMappingItem{Extension = "sh", MimeType = "application/x-sh"},
+            new MimeMappingItem{Extension = "shar", MimeType = "application/x-shar"},
+            new MimeMappingItem{Extension = "smf", MimeType = "audio/x-midi"},
+            new MimeMappingItem{Extension = "sit", MimeType = "application/x-stuffit"},
+            new MimeMappingItem{Extension = "snd", MimeType = "audio/basic"},
+            new MimeMappingItem{Extension = "src", MimeType = "application/x-wais-source"},
+            new MimeMappingItem{Extension = "sv4cpio", MimeType = "application/x-sv4cpio"},
+            new MimeMappingItem{Extension = "sv4crc", MimeType = "application/x-sv4crc"},
+            new MimeMappingItem{Extension = "svg", MimeType = "image/svg+xml"},
+            new MimeMappingItem{Extension = "svgz", MimeType = "image/svg+xml"},
+            new MimeMappingItem{Extension = "swf", MimeType = "application/x-shockwave-flash"},
+            new MimeMappingItem{Extension = "t", MimeType = "application/x-troff"},
+            new MimeMappingItem{Extension = "tar", MimeType = "application/x-tar"},
+            new MimeMappingItem{Extension = "tcl", MimeType = "application/x-tcl"},
+            new MimeMappingItem{Extension = "tex", MimeType = "application/x-tex"},
+            new MimeMappingItem{Extension = "texi", MimeType = "application/x-texinfo"},
+            new MimeMappingItem{Extension = "texinfo", MimeType = "application/x-texinfo"},
+            new MimeMappingItem{Extension = "tif", MimeType = "image/tiff"},
+            new MimeMappingItem{Extension = "tiff", MimeType = "image/tiff"},
+            new MimeMappingItem{Extension = "tr", MimeType = "application/x-troff"},
+            new MimeMappingItem{Extension = "tsv", MimeType = "text/tab-separated-values"},
+            new MimeMappingItem{Extension = "txt", MimeType = "text/plain"},
+            new MimeMappingItem{Extension = "ulw", MimeType = "audio/basic"},
+            new MimeMappingItem{Extension = "ustar", MimeType = "application/x-ustar"},
+            new MimeMappingItem{Extension = "vxml", MimeType = "application/voicexml+xml"},
+            new MimeMappingItem{Extension = "xbm", MimeType = "image/x-xbitmap"},
+            new MimeMappingItem{Extension = "xht", MimeType = "application/xhtml+xml"},
+            new MimeMappingItem{Extension = "xhtml", MimeType = "application/xhtml+xml"},
+            new MimeMappingItem{Extension = "xls", MimeType = "application/vnd.ms-excel"},
+            new MimeMappingItem{Extension = "xml", MimeType = "application/xml"},
+            new MimeMappingItem{Extension = "xpm", MimeType = "image/x-xpixmap"},
+            new MimeMappingItem{Extension = "xsl", MimeType = "application/xml"},
+            new MimeMappingItem{Extension = "xslt", MimeType = "application/xslt+xml"},
+            new MimeMappingItem{Extension = "xul", MimeType = "application/vnd.mozilla.xul+xml"},
+            new MimeMappingItem{Extension = "xwd", MimeType = "image/x-xwindowdump"},
+            new MimeMappingItem{Extension = "vsd", MimeType = "application/x-visio"},
+            new MimeMappingItem{Extension = "wav", MimeType = "audio/x-wav"},
+            
+            //<!-- Wireless Bitmap -->
+            new MimeMappingItem{Extension = "wbmp", MimeType = "image/vnd.wap.wbmp"},
+
+            //<!-- WML Source -->
+            new MimeMappingItem{Extension = "wml", MimeType = "text/vnd.wap.wml"},
+
+            //<!-- Compiled WML -->
+            new MimeMappingItem{Extension = "wmlc", MimeType = "application/vnd.wap.wmlc"},
+
+            //<!-- WML Script Source -->
+            new MimeMappingItem{Extension = "wmls", MimeType = "text/vnd.wap.wmlscript"},
+
+            //<!-- Compiled WML Script -->
+            new MimeMappingItem{Extension = "wmlscriptc", MimeType = "application/vnd.wap.wmlscriptc"},
+            new MimeMappingItem{Extension = "wmv", MimeType = "video/x-ms-wmv"},
+            new MimeMappingItem{Extension = "wrl", MimeType = "x-world/x-vrml"},
+            new MimeMappingItem{Extension = "wspolicy", MimeType = "application/wspolicy+xml"},
+            new MimeMappingItem{Extension = "Z", MimeType = "application/x-compress"},
+            new MimeMappingItem{Extension = "z", MimeType = "application/x-compress"},
+            new MimeMappingItem{Extension = "zip", MimeType = "application/zip"},
+        }.AsReadOnly();
+    }
+}

+ 9 - 0
Masuit.Tools/Mvc/Mime/IMimeMapper.cs

@@ -0,0 +1,9 @@
+namespace Masuit.Tools.Mvc.Mime
+{
+    public interface IMimeMapper
+    {
+        IMimeMapper Extend(params MimeMappingItem[] extensions);
+        string GetMimeFromExtension(string fileExtension);
+        string GetMimeFromPath(string filePath);
+    }
+}

+ 97 - 0
Masuit.Tools/Mvc/Mime/MimeMapper.cs

@@ -0,0 +1,97 @@
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace Masuit.Tools.Mvc.Mime
+{
+    /// <summary>
+    /// 默认MIME映射器,可以根据文件扩展名获取标准内容类型。
+    /// </summary>
+    public class MimeMapper : IMimeMapper
+    {
+        /// <summary>
+        /// 默认Mime  - 如果没有找到任何其他映射则作为默认的Mime-Type
+        /// </summary>
+        private const string DefaultMime = "application/octet-stream";
+
+        /// <summary>
+        /// 在文件路径中搜索文件扩展名的默认正则表达式
+        /// </summary>
+        private readonly Regex _pathExtensionPattern = new Regex("\\.(\\w*)$");
+
+        /// <summary>
+        /// 扩展的Mime类型的默认字典(Content types)
+        /// </summary>
+        private static Dictionary<string, string> _items;
+
+        public MimeMapper() : this(null)
+        {
+        }
+
+        public MimeMapper(params MimeMappingItem[] extensions)
+        {
+            _items = new Dictionary<string, string>();
+            foreach (var mapping in DefaultMimeItems.Items)
+            {
+                _items.Add(mapping.Extension, mapping.MimeType);
+            }
+            Extend(extensions);
+        }
+
+        /// <summary>
+        /// 扩展mime映射规则的标准列表。扩展的具有更高的优先级 - 如果扩展具有与标准项相同的扩展名,则会覆盖默认的mime
+        /// </summary>
+        /// <param name="extensions"></param>
+        /// <returns></returns>
+        public IMimeMapper Extend(params MimeMappingItem[] extensions)
+        {
+            if (extensions != null)
+            {
+                foreach (var mapping in extensions)
+                {
+                    if (_items.ContainsKey(mapping.Extension))
+                    {
+                        _items[mapping.Extension] = mapping.MimeType;
+                    }
+                    else
+                    {
+                        _items.Add(mapping.Extension, mapping.MimeType);
+                    }
+                }
+            }
+            return this;
+        }
+
+        /// <summary>
+        /// 返回特定文件扩展名的Content-Type,如果未找到任何对应关系,则返回默认值
+        /// </summary>
+        /// <param name="fileExtension"></param>
+        /// <returns></returns>
+        public string GetMimeFromExtension(string fileExtension)
+        {
+            fileExtension = (fileExtension ?? string.Empty).ToLower();
+            fileExtension = fileExtension.Trim().StartsWith(".") ? fileExtension.Replace(".", "") : fileExtension;
+
+            return _items.ContainsKey(fileExtension) ? _items[fileExtension] : DefaultMime;
+        }
+
+        /// <summary>
+        /// </summary>
+        /// <param name="path"></param>
+        /// <returns></returns>
+        public string GetMimeFromPath(string path)
+        {
+            var extension = GetExtension(path);
+            return GetMimeFromExtension(extension);
+        }
+
+        protected string GetExtension(string path)
+        {
+            var match = _pathExtensionPattern.Match(path ?? string.Empty);
+            if (match.Groups.Count > 1)
+            {
+                return match.Groups[1].Value;
+            }
+            return null;
+        }
+    }
+}

+ 8 - 0
Masuit.Tools/Mvc/Mime/MimeMappingItem.cs

@@ -0,0 +1,8 @@
+namespace Masuit.Tools.Mvc.Mime
+{
+    public class MimeMappingItem
+    {
+        public string Extension { get; set; }
+        public string MimeType { get; set; }
+    }
+}

+ 303 - 0
Masuit.Tools/Mvc/ResumeFileResult.cs

@@ -0,0 +1,303 @@
+using Masuit.Tools.Mvc.Internal;
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text.RegularExpressions;
+using System.Web;
+using System.Web.Mvc;
+
+namespace Masuit.Tools.Mvc
+{
+    /// <summary>
+    /// 扩展自带的FilePathResult来支持断点续传
+    /// </summary>
+    public class ResumeFileResult : FilePathResult
+    {
+        /// <summary>
+        /// 由于附加依赖性,所以没使用logger.Log4net。
+        /// </summary>
+        public static Action<Exception> LogException;
+
+        private readonly Regex _rangePattern = new Regex("bytes=(\\d*)-(\\d*)");
+        private readonly string _ifNoneMatch;
+        private readonly string _ifModifiedSince;
+        private readonly string _ifMatch;
+        private readonly string _ifUnmodifiedSince;
+        private readonly string _ifRange;
+        private readonly string _etag;
+        private readonly Range _range;
+        private readonly FileInfo _file;
+        private readonly string _lastModified;
+        private readonly bool _rangeRequest;
+        private readonly string _downloadFileName;
+
+        public ResumeFileResult(string fileName, string contentType, HttpRequestBase request) : this(fileName, contentType, request, null)
+        {
+        }
+
+        public ResumeFileResult(string fileName, string contentType, HttpRequestBase request, string downloadFileName) : this(fileName, contentType, request.Headers[HttpHeaders.IfNoneMatch], request.Headers[HttpHeaders.IfModifiedSince], request.Headers[HttpHeaders.IfMatch], request.Headers[HttpHeaders.IfUnmodifiedSince], request.Headers[HttpHeaders.IfRange], request.Headers[HttpHeaders.Range], downloadFileName)
+        {
+        }
+
+
+        public ResumeFileResult(string fileName, string contentType, string ifNoneMatch, string ifModifiedSince, string ifMatch, string ifUnmodifiedSince, string ifRange, string range, string downloadFileName) : base(fileName, contentType)
+        {
+            _file = new FileInfo(fileName);
+            _lastModified = Util.FormatDate(_file.LastWriteTime);
+            _rangeRequest = range != null;
+            _range = Range(range);
+            _etag = Etag();
+            _ifNoneMatch = ifNoneMatch;
+            _ifModifiedSince = ifModifiedSince;
+            _ifMatch = ifMatch;
+            _ifUnmodifiedSince = ifUnmodifiedSince;
+            _ifRange = ifRange;
+            _downloadFileName = downloadFileName;
+        }
+
+        /// <summary>
+        /// 检查请求中的标头,为响应添加适当的标头
+        /// </summary>
+        /// <param name="response"></param>
+        protected override void WriteFile(HttpResponseBase response)
+        {
+            response.AppendHeader(HttpHeaders.Etag, _etag);
+            response.AppendHeader(HttpHeaders.LastModified, _lastModified);
+            response.AppendHeader(HttpHeaders.Expires, Util.FormatDate(DateTime.Now));
+
+            if (IsNotModified())
+            {
+                response.StatusCode = (int)HttpStatusCode.NotModified;
+            }
+            else if (IsPreconditionFailed())
+            {
+                response.StatusCode = (int)HttpStatusCode.PreconditionFailed;
+            }
+            else if (IsRangeNotSatisfiable())
+            {
+                response.AppendHeader(HttpHeaders.ContentRange, "bytes */" + _file.Length);
+                response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable;
+            }
+            else
+            {
+                TransmitFile(response);
+            }
+        }
+
+        /// <summary>
+        /// 计算要写入Response的总字节长度
+        /// </summary>
+        /// <returns></returns>
+        protected long ContentLength()
+        {
+            return _range.End - _range.Start + 1;
+        }
+
+        /// <summary>
+        /// 分析If-Range标头并返回:
+        ///     true - 如果必须发送部分内容
+        ///     false - 如果必须发送整个文件
+        /// spec: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.27
+        /// </summary>
+        /// <returns></returns>
+        protected bool SendRange()
+        {
+            return _rangeRequest && _ifRange == null || _rangeRequest && _ifRange == _etag;
+        }
+
+        /// <summary>
+        /// 将文件写入响应流,根据请求标头和文件属性添加正确的标头
+        /// </summary>
+        /// <param name="response"></param>
+        protected virtual void TransmitFile(HttpResponseBase response)
+        {
+            var contentLength = ContentLength();
+            response.StatusCode = SendRange() ? (int)HttpStatusCode.PartialContent : (int)HttpStatusCode.OK;
+
+            response.AppendHeader(HttpHeaders.ContentLength, contentLength.ToString(CultureInfo.InvariantCulture));
+            response.AppendHeader(HttpHeaders.AcceptRanges, "bytes");
+            response.AppendHeader(HttpHeaders.ContentRange, $"bytes {_range.Start}-{_range.End}/{_file.Length}");
+
+            if (!string.IsNullOrWhiteSpace(_downloadFileName))
+            {
+                response.AddHeader("Content-Disposition", $"attachment;filename=\"{_downloadFileName}\"");
+            }
+
+            try
+            {
+                response.TransmitFile(FileName, _range.Start, contentLength);
+            }
+            catch (Exception ex)
+            {
+                LogException?.Invoke(ex);
+            }
+        }
+
+        /// <summary>
+        /// 在以下情况下,范围不可满足:
+        /// 起点大于文件的总大小
+        /// 起点小于0
+        /// 端点等于或大于文件的大小
+        /// 起点大于终点
+        /// spec: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.17
+        /// </summary>
+        /// <returns></returns>
+        protected bool IsRangeNotSatisfiable()
+        {
+            return _range.Start >= _file.Length || _range.Start < 0 || _range.End >= _file.Length || _range.Start > _range.End;
+        }
+
+        /// <summary>
+        /// 在以下情况下,前提可能会失败
+        /// 如果匹配(http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24)
+        ///     标题为空,与etag不匹配
+        /// 如果未经修改则(http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.28)
+        ///     header不为空,与File.LastWriteTime不匹配。
+        ///     在下载过程中更改文件时可能会发生这种情况。
+        /// </summary>
+        /// <returns></returns>
+        protected bool IsPreconditionFailed()
+        {
+            if (_ifMatch != null)
+            {
+                return !IsMatch(_ifMatch, _etag);
+            }
+
+            return _ifUnmodifiedSince != null && _ifUnmodifiedSince != _lastModified;
+        }
+
+        /// <summary>
+        /// 如果有的话,该方法返回true
+        /// 如果 - 无匹配(http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26)或
+        /// 或者如果未经修改则(http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25)
+        /// 已验证
+        /// </summary>
+        /// <returns></returns>
+        protected bool IsNotModified()
+        {
+            if (_ifNoneMatch != null)
+            {
+                return IsMatch(_ifNoneMatch, _etag);
+            }
+
+            return _ifModifiedSince != null && _ifModifiedSince == _lastModified;
+        }
+
+        /// <summary>
+        /// 当前文件的Etag响应头
+        /// </summary>
+        /// <returns></returns>
+        private string Etag()
+        {
+            return Util.Etag(_file);
+        }
+
+        private bool IsMatch(string values, string etag)
+        {
+            var matches = (values ?? string.Empty).Split(new[]
+            {
+                ","
+            }, StringSplitOptions.RemoveEmptyEntries);
+            return matches.Any(s => s.Equals("*") || s.Equals(etag));
+        }
+
+        /// <summary>
+        /// 根据Range标头计算起点和终点
+        /// Spec: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
+        /// </summary>
+        /// <param name="range"></param>
+        /// <returns></returns>
+        private Range Range(string range)
+        {
+            var lastByte = _file.Length - 1;
+
+            if (!string.IsNullOrWhiteSpace(range))
+            {
+                var matches = _rangePattern.Matches(range);
+                if (matches.Count != 0)
+                {
+                    var start = matches[0].Groups[1].Value.ToLong(-1);
+                    var end = matches[0].Groups[2].Value.ToLong(-1);
+
+                    if (start != -1 || end != -1)
+                    {
+                        if (start == -1)
+                        {
+                            start = _file.Length - end;
+                            end = lastByte;
+                        }
+                        else if (end == -1)
+                        {
+                            end = lastByte;
+                        }
+
+                        return new Range
+                        {
+                            Start = start,
+                            End = end
+                        };
+                    }
+                }
+                return new Range
+                {
+                    Start = -1,
+                    End = -1
+                };
+            }
+            return new Range
+            {
+                Start = 0,
+                End = lastByte
+            };
+        }
+
+        /// <summary>
+        /// 用于支持ResumeFileResult功能的帮助类
+        /// </summary>
+        public static class Util
+        {
+            /// <summary>
+            /// Etag响应头
+            /// </summary>
+            /// <returns></returns>
+            public static string Etag(FileInfo file)
+            {
+                return Etag(file.FullName, FormatDate(file.LastWriteTime));
+            }
+
+            /// <summary>
+            /// <see cref="Etag(System.IO.FileInfo)"/>
+            /// </summary>
+            /// <param name="fullName"></param>
+            /// <param name="lastModified"></param>
+            /// <returns></returns>
+            public static string Etag(string fullName, string lastModified)
+            {
+                return "\"mvc-streaming-" + fullName.GetHashCode() + "-" + fullName.GetHashCode() + "\"";
+            }
+
+            /// <summary>
+            /// <see cref="Etag(System.IO.FileInfo)"/>
+            /// </summary>
+            /// <param name="fullName"></param>
+            /// <param name="lastWriteTime"></param>
+            /// <returns></returns>
+            public static string Etag(string fullName, DateTime lastWriteTime)
+            {
+                return Etag(fullName, FormatDate(lastWriteTime));
+            }
+
+            /// <summary>
+            /// 格式是绝对日期和时间。它必须是RFC 1123日期格式。
+            /// </summary>
+            /// <param name="date"></param>
+            /// <returns></returns>
+            public static string FormatDate(DateTime date)
+            {
+                return date.ToString("R");
+            }
+        }
+    }
+}

+ 4 - 0
Masuit.Tools/packages.config

@@ -2,6 +2,10 @@
 <packages>
   <package id="AngleSharp" version="0.9.11" targetFramework="net45" />
   <package id="HtmlSanitizer" version="4.0.199" targetFramework="net45" />
+  <package id="Microsoft.AspNet.Mvc" version="5.2.7" targetFramework="net45" />
+  <package id="Microsoft.AspNet.Razor" version="3.2.7" targetFramework="net45" />
+  <package id="Microsoft.AspNet.WebPages" version="3.2.7" targetFramework="net45" />
+  <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net45" />
   <package id="Newtonsoft.Json" version="12.0.1" targetFramework="net45" />
   <package id="SharpZipLib" version="1.1.0" targetFramework="net45" />
   <package id="StackExchange.Redis" version="1.2.6" targetFramework="net45" />

+ 30 - 3
README.md

@@ -1,18 +1,45 @@
 # Masuit.Tools
 包含一些常用的操作类,大都是静态类,加密解密,反射操作,硬件信息,字符串扩展方法,日期时间扩展操作,大文件拷贝,图像裁剪,验证码等常用封装。
 [官网教程](http://masuit.com/55)
-
-# Masuit.Tools.AspNetCore.ResumeFileResults
+# Masuit.Tools.Mvc.ResumeFileResult和Masuit.Tools.AspNetCore.ResumeFileResults
 
 允许你在ASP.NET Core中通过MVC/WebAPI应用程序传输文件数据时使用断点续传以及多线程下载。
 
 它允许提供`ETag`标题以及`Last-Modified`标题。 它还支持以下前置条件标题:`If-Match`,`If-None-Match`,`If-Modified-Since`,`If-Unmodified-Since`,`If-Range`。
-
 ## 支持 ASP.NET Core 2.0
 从.NET Core2.0开始,ASP.NET Core内部支持断点续传。 因此删除了与断点续传相关的所有代码。 只留下了“Content-Disposition” Inline的一部分。 现在所有代码都依赖于基础.NET类。
 还删除了对多部分请求的支持。 为了支持我将不得不复制很多原始代码,因为目前没有办法简单地覆盖基类的某些部分。
 
 ## 如何使用 
+### .NET Framework
+在你的控制器中,你可以像在`FileResult`帮助器中构建一样使用它。
+```csharp
+        using Masuit.Tools.Mvc;
+        using Masuit.Tools.Mvc.ResumeFileResult;
+```
+
+```csharp
+
+        private readonly MimeMapper mimeMapper=new MimeMapper(); // 推荐使用依赖注入
+
+        public ActionResult ResumeFileResult()
+        {
+            var path = Server.MapPath("~/Content/test.mp4");
+            return new ResumeFileResult(path, mimeMapper.GetMimeFromPath(path), Request);
+        }
+
+        public ActionResult ResumeFile()
+        {
+            return this.ResumeFile("~/Content/test.mp4", mimeMapper.GetMimeFromPath(path), "test.mp4");
+        }
+
+        public ActionResult ResumePhysicalFile()
+        {
+            return this.ResumePhysicalFile(@"D:/test.mp4", mimeMapper.GetMimeFromPath(@"D:/test.mp4"), "test.mp4");
+        }
+```
+
+### Asp.Net Core
 要使用ResumeFileResults,必须在`Startup.cs`的`ConfigureServices`方法调用中配置服务:
 
 ```csharp