From 86b1ac8f392774de2abf58c556e34966cf8b2f58 Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Thu, 6 Feb 2014 15:30:21 -0800 Subject: [PATCH] Initial static files port. --- .gitattributes | 50 ++ .gitignore | 22 + Microsoft.AspNet.StaticFiles.sln | 50 ++ NuGet.Config | 13 + build.cmd | 23 + global.json | 3 + makefile.shade | 7 + samples/StaticFileSample/Program.cs | 28 ++ samples/StaticFileSample/Startup.cs | 32 ++ samples/StaticFileSample/project.json | 21 + src/Microsoft.AspNet.StaticFiles/Constants.cs | 43 ++ .../FileExtensionContentTypeProvider.cs | 432 ++++++++++++++++++ .../ContentTypes/IContentTypeProvider.cs | 18 + .../CustomDictionary.xml | 10 + .../DefaultFilesExtensions.cs | 51 +++ .../DefaultFilesMiddleware.cs | 89 ++++ .../DefaultFilesOptions.cs | 45 ++ .../DirectoryBrowserExtensions.cs | 51 +++ .../DirectoryBrowserMiddleware.cs | 83 ++++ .../DirectoryBrowserOptions.cs | 36 ++ .../HtmlDirectoryFormatter.cs | 157 +++++++ .../IDirectoryFormatter.cs | 22 + .../FileServerExtensions.cs | 75 +++ .../FileServerOptions.cs | 49 ++ src/Microsoft.AspNet.StaticFiles/Helpers.cs | 52 +++ .../Infrastructure/RangeHelpers.cs | 138 ++++++ .../Infrastructure/SharedOptions.cs | 45 ++ .../Infrastructure/SharedOptionsBase.cs | 52 +++ .../Properties/AssemblyInfo.cs | 27 ++ .../Resources.Designer.cs | 144 ++++++ .../Resources.resx | 147 ++++++ .../SendFileExtensions.cs | 65 +++ .../SendFileMiddleware.cs | 100 ++++ .../SendFileResponseExtensions.cs | 70 +++ .../StaticFileContext.cs | 393 ++++++++++++++++ .../StaticFileExtensions.cs | 50 ++ .../StaticFileMiddleware.cs | 91 ++++ .../StaticFileOptions.cs | 56 +++ .../StaticFileResponseContext.cs | 24 + .../StreamCopyOperation.cs | 60 +++ src/Microsoft.AspNet.StaticFiles/project.json | 16 + 41 files changed, 2940 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Microsoft.AspNet.StaticFiles.sln create mode 100644 NuGet.Config create mode 100644 build.cmd create mode 100644 global.json create mode 100644 makefile.shade create mode 100644 samples/StaticFileSample/Program.cs create mode 100644 samples/StaticFileSample/Startup.cs create mode 100644 samples/StaticFileSample/project.json create mode 100644 src/Microsoft.AspNet.StaticFiles/Constants.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/ContentTypes/FileExtensionContentTypeProvider.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/ContentTypes/IContentTypeProvider.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/CustomDictionary.xml create mode 100644 src/Microsoft.AspNet.StaticFiles/DefaultFilesExtensions.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/DefaultFilesMiddleware.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/DefaultFilesOptions.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/DirectoryBrowserExtensions.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/DirectoryBrowserMiddleware.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/DirectoryBrowserOptions.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/DirectoryFormatters/HtmlDirectoryFormatter.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/DirectoryFormatters/IDirectoryFormatter.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/FileServerExtensions.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/FileServerOptions.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/Helpers.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/Infrastructure/RangeHelpers.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/Infrastructure/SharedOptions.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/Infrastructure/SharedOptionsBase.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/Resources.resx create mode 100644 src/Microsoft.AspNet.StaticFiles/SendFileExtensions.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/SendFileMiddleware.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/SendFileResponseExtensions.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/StaticFileContext.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/StaticFileExtensions.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/StaticFileMiddleware.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/StaticFileOptions.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/StaticFileResponseContext.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/StreamCopyOperation.cs create mode 100644 src/Microsoft.AspNet.StaticFiles/project.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..bdaa5ba982 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,50 @@ +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain + +*.jpg binary +*.png binary +*.gif binary + +*.cs text=auto diff=csharp +*.vb text=auto +*.resx text=auto +*.c text=auto +*.cpp text=auto +*.cxx text=auto +*.h text=auto +*.hxx text=auto +*.py text=auto +*.rb text=auto +*.java text=auto +*.html text=auto +*.htm text=auto +*.css text=auto +*.scss text=auto +*.sass text=auto +*.less text=auto +*.js text=auto +*.lisp text=auto +*.clj text=auto +*.sql text=auto +*.php text=auto +*.lua text=auto +*.m text=auto +*.asm text=auto +*.erl text=auto +*.fs text=auto +*.fsx text=auto +*.hs text=auto + +*.csproj text=auto +*.vbproj text=auto +*.fsproj text=auto +*.dbproj text=auto +*.sln text=auto eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..2554a1fc23 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +[Oo]bj/ +[Bb]in/ +TestResults/ +.nuget/ +_ReSharper.*/ +packages/ +artifacts/ +PublishProfiles/ +*.user +*.suo +*.cache +*.docstates +_ReSharper.* +nuget.exe +*net45.csproj +*k10.csproj +*.psess +*.vsp +*.pidb +*.userprefs +*DS_Store +*.ncrunchsolution diff --git a/Microsoft.AspNet.StaticFiles.sln b/Microsoft.AspNet.StaticFiles.sln new file mode 100644 index 0000000000..1c760aa655 --- /dev/null +++ b/Microsoft.AspNet.StaticFiles.sln @@ -0,0 +1,50 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.30110.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.StaticFiles.net45", "src\Microsoft.AspNet.StaticFiles\Microsoft.AspNet.StaticFiles.net45.csproj", "{49278B83-CC12-49EA-8F61-D143863DD21B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.StaticFiles.k10", "src\Microsoft.AspNet.StaticFiles\Microsoft.AspNet.StaticFiles.k10.csproj", "{297551FE-7539-4E43-A6B8-165C7789F48D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{40EE0889-960E-41B4-A3D3-9CE963EB0797}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{8B21A3A9-9CA6-4857-A6E0-1A3203404B60}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticFileSample.k10", "samples\StaticFileSample\StaticFileSample.k10.csproj", "{8C5384FC-24F3-47F2-897C-A151604F011C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticFileSample.net45", "samples\StaticFileSample\StaticFileSample.net45.csproj", "{742E16CD-8217-4386-AAF0-5F116E62CD82}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {49278B83-CC12-49EA-8F61-D143863DD21B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49278B83-CC12-49EA-8F61-D143863DD21B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49278B83-CC12-49EA-8F61-D143863DD21B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49278B83-CC12-49EA-8F61-D143863DD21B}.Release|Any CPU.Build.0 = Release|Any CPU + {297551FE-7539-4E43-A6B8-165C7789F48D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {297551FE-7539-4E43-A6B8-165C7789F48D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {297551FE-7539-4E43-A6B8-165C7789F48D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {297551FE-7539-4E43-A6B8-165C7789F48D}.Release|Any CPU.Build.0 = Release|Any CPU + {8C5384FC-24F3-47F2-897C-A151604F011C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C5384FC-24F3-47F2-897C-A151604F011C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C5384FC-24F3-47F2-897C-A151604F011C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C5384FC-24F3-47F2-897C-A151604F011C}.Release|Any CPU.Build.0 = Release|Any CPU + {742E16CD-8217-4386-AAF0-5F116E62CD82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {742E16CD-8217-4386-AAF0-5F116E62CD82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {742E16CD-8217-4386-AAF0-5F116E62CD82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {742E16CD-8217-4386-AAF0-5F116E62CD82}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {297551FE-7539-4E43-A6B8-165C7789F48D} = {40EE0889-960E-41B4-A3D3-9CE963EB0797} + {49278B83-CC12-49EA-8F61-D143863DD21B} = {40EE0889-960E-41B4-A3D3-9CE963EB0797} + {8C5384FC-24F3-47F2-897C-A151604F011C} = {8B21A3A9-9CA6-4857-A6E0-1A3203404B60} + {742E16CD-8217-4386-AAF0-5F116E62CD82} = {8B21A3A9-9CA6-4857-A6E0-1A3203404B60} + EndGlobalSection +EndGlobal diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000000..ab583b0ff7 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000000..7045ee1f84 --- /dev/null +++ b/build.cmd @@ -0,0 +1,23 @@ +@echo off +cd %~dp0 + +SETLOCAL +SET CACHED_NUGET=%LocalAppData%\NuGet\NuGet.exe + +IF EXIST %CACHED_NUGET% goto copynuget +echo Downloading latest version of NuGet.exe... +IF NOT EXIST %LocalAppData%\NuGet md %LocalAppData%\NuGet +@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://www.nuget.org/nuget.exe' -OutFile '%CACHED_NUGET%'" + +:copynuget +IF EXIST .nuget\nuget.exe goto restore +md .nuget +copy %CACHED_NUGET% .nuget\nuget.exe > nul + +:restore +IF EXIST packages\KoreBuild goto run +.nuget\NuGet.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre +.nuget\NuGet.exe install Sake -version 0.2 -o packages -ExcludeVersion + +:run +packages\Sake\tools\Sake.exe -I packages\KoreBuild\build -f makefile.shade %* diff --git a/global.json b/global.json new file mode 100644 index 0000000000..840c36f6ad --- /dev/null +++ b/global.json @@ -0,0 +1,3 @@ +{ + "sources": ["src"] +} \ No newline at end of file diff --git a/makefile.shade b/makefile.shade new file mode 100644 index 0000000000..6357ea2841 --- /dev/null +++ b/makefile.shade @@ -0,0 +1,7 @@ + +var VERSION='0.1' +var FULL_VERSION='0.1' +var AUTHORS='Microsoft' + +use-standard-lifecycle +k-standard-goals diff --git a/samples/StaticFileSample/Program.cs b/samples/StaticFileSample/Program.cs new file mode 100644 index 0000000000..1a7745c155 --- /dev/null +++ b/samples/StaticFileSample/Program.cs @@ -0,0 +1,28 @@ +using System; +#if NET45 +using System.Diagnostics; +using Microsoft.Owin.Hosting; +#endif + +namespace StaticFilesSample +{ + public class Program + { + const string baseUrl = "http://localhost:9001/"; + + public static void Main() + { +#if NET45 + using (WebApp.Start(new StartOptions(baseUrl))) + { + Console.WriteLine("Listening at {0}", baseUrl); + Process.Start(baseUrl); + Console.WriteLine("Press any key to exit"); + Console.ReadKey(); + } +#else + Console.WriteLine("Hello World"); +#endif + } + } +} \ No newline at end of file diff --git a/samples/StaticFileSample/Startup.cs b/samples/StaticFileSample/Startup.cs new file mode 100644 index 0000000000..37d1667ab3 --- /dev/null +++ b/samples/StaticFileSample/Startup.cs @@ -0,0 +1,32 @@ +#if NET45 +using System; +using System.IO; +using Microsoft.AspNet.Abstractions; +using Microsoft.Owin.FileSystems; +using Microsoft.AspNet; +using Owin; +using Microsoft.AspNet.StaticFiles; + +namespace StaticFilesSample +{ + public class Startup + { + public void Configuration(IAppBuilder app) + { + app.UseErrorPage(); + + // Temporary bridge from katana to Owin + app.UseBuilder(ConfigurePK); + } + + private void ConfigurePK(IBuilder builder) + { + builder.UseFileServer(new FileServerOptions() + { + EnableDirectoryBrowsing = true, + FileSystem = new PhysicalFileSystem(@"c:\temp") + }); + } + } +} +#endif \ No newline at end of file diff --git a/samples/StaticFileSample/project.json b/samples/StaticFileSample/project.json new file mode 100644 index 0000000000..39a19efd65 --- /dev/null +++ b/samples/StaticFileSample/project.json @@ -0,0 +1,21 @@ +{ + "version" : "0.1-alpha-*", + "dependencies": { + "Microsoft.AspNet.FileSystems": "0.1-alpha-*", + "Microsoft.AspNet.StaticFiles": "", + "Microsoft.AspNet.Abstractions": "0.1-alpha-*" + }, + "configurations": { + "net45": { + "dependencies": { + "Owin": "1.0", + "Microsoft.Owin": "2.1.0", + "Microsoft.Owin.Diagnostics": "2.1.0", + "Microsoft.Owin.Hosting": "2.1.0", + "Microsoft.Owin.Host.HttpListener": "2.1.0", + "Microsoft.AspNet.AppBuilderSupport": "0.1-alpha-*" + } + }, + "k10" : { } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.StaticFiles/Constants.cs b/src/Microsoft.AspNet.StaticFiles/Constants.cs new file mode 100644 index 0000000000..df9c9fb1fc --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/Constants.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNet.StaticFiles +{ + internal static class Constants + { + internal const string ServerCapabilitiesKey = "server.Capabilities"; + internal const string SendFileVersionKey = "sendfile.Version"; + internal const string SendFileVersion = "1.0"; + + internal const string Location = "Location"; + internal const string IfMatch = "If-Match"; + internal const string IfNoneMatch = "If-None-Match"; + internal const string IfModifiedSince = "If-Modified-Since"; + internal const string IfUnmodifiedSince = "If-Unmodified-Since"; + internal const string IfRange = "If-Range"; + internal const string Range = "Range"; + internal const string ContentRange = "Content-Range"; + internal const string LastModified = "Last-Modified"; + internal const string ETag = "ETag"; + + internal const string HttpDateFormat = "r"; + + internal const string TextHtmlUtf8 = "text/html; charset=utf-8"; + + internal const int Status200Ok = 200; + internal const int Status206PartialContent = 206; + internal const int Status304NotModified = 304; + internal const int Status412PreconditionFailed = 412; + internal const int Status416RangeNotSatisfiable = 416; + + internal static readonly Task CompletedTask = CreateCompletedTask(); + + private static Task CreateCompletedTask() + { + var tcs = new TaskCompletionSource(); + tcs.SetResult(null); + return tcs.Task; + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/ContentTypes/FileExtensionContentTypeProvider.cs b/src/Microsoft.AspNet.StaticFiles/ContentTypes/FileExtensionContentTypeProvider.cs new file mode 100644 index 0000000000..205da9adc1 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/ContentTypes/FileExtensionContentTypeProvider.cs @@ -0,0 +1,432 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.AspNet.StaticFiles.ContentTypes +{ + /// + /// Provides a mapping between file extensions and MIME types. + /// + public class FileExtensionContentTypeProvider : IContentTypeProvider + { +#region Extension mapping table + /// + /// Creates a new provider with a set of default mappings. + /// + public FileExtensionContentTypeProvider() + : this(new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { ".323", "text/h323" }, + { ".3g2", "video/3gpp2" }, + { ".3gp2", "video/3gpp2" }, + { ".3gp", "video/3gpp" }, + { ".3gpp", "video/3gpp" }, + { ".aac", "audio/aac" }, + { ".aaf", "application/octet-stream" }, + { ".aca", "application/octet-stream" }, + { ".accdb", "application/msaccess" }, + { ".accde", "application/msaccess" }, + { ".accdt", "application/msaccess" }, + { ".acx", "application/internet-property-stream" }, + { ".adt", "audio/vnd.dlna.adts" }, + { ".adts", "audio/vnd.dlna.adts" }, + { ".afm", "application/octet-stream" }, + { ".ai", "application/postscript" }, + { ".aif", "audio/x-aiff" }, + { ".aifc", "audio/aiff" }, + { ".aiff", "audio/aiff" }, + { ".application", "application/x-ms-application" }, + { ".art", "image/x-jg" }, + { ".asd", "application/octet-stream" }, + { ".asf", "video/x-ms-asf" }, + { ".asi", "application/octet-stream" }, + { ".asm", "text/plain" }, + { ".asr", "video/x-ms-asf" }, + { ".asx", "video/x-ms-asf" }, + { ".atom", "application/atom+xml" }, + { ".au", "audio/basic" }, + { ".avi", "video/x-msvideo" }, + { ".axs", "application/olescript" }, + { ".bas", "text/plain" }, + { ".bcpio", "application/x-bcpio" }, + { ".bin", "application/octet-stream" }, + { ".bmp", "image/bmp" }, + { ".c", "text/plain" }, + { ".cab", "application/vnd.ms-cab-compressed" }, + { ".calx", "application/vnd.ms-office.calx" }, + { ".cat", "application/vnd.ms-pki.seccat" }, + { ".cdf", "application/x-cdf" }, + { ".chm", "application/octet-stream" }, + { ".class", "application/x-java-applet" }, + { ".clp", "application/x-msclip" }, + { ".cmx", "image/x-cmx" }, + { ".cnf", "text/plain" }, + { ".cod", "image/cis-cod" }, + { ".cpio", "application/x-cpio" }, + { ".cpp", "text/plain" }, + { ".crd", "application/x-mscardfile" }, + { ".crl", "application/pkix-crl" }, + { ".crt", "application/x-x509-ca-cert" }, + { ".csh", "application/x-csh" }, + { ".css", "text/css" }, + { ".csv", "application/octet-stream" }, + { ".cur", "application/octet-stream" }, + { ".dcr", "application/x-director" }, + { ".deploy", "application/octet-stream" }, + { ".der", "application/x-x509-ca-cert" }, + { ".dib", "image/bmp" }, + { ".dir", "application/x-director" }, + { ".disco", "text/xml" }, + { ".dlm", "text/dlm" }, + { ".doc", "application/msword" }, + { ".docm", "application/vnd.ms-word.document.macroEnabled.12" }, + { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, + { ".dot", "application/msword" }, + { ".dotm", "application/vnd.ms-word.template.macroEnabled.12" }, + { ".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" }, + { ".dsp", "application/octet-stream" }, + { ".dtd", "text/xml" }, + { ".dvi", "application/x-dvi" }, + { ".dvr-ms", "video/x-ms-dvr" }, + { ".dwf", "drawing/x-dwf" }, + { ".dwp", "application/octet-stream" }, + { ".dxr", "application/x-director" }, + { ".eml", "message/rfc822" }, + { ".emz", "application/octet-stream" }, + { ".eot", "application/vnd.ms-fontobject" }, + { ".eps", "application/postscript" }, + { ".etx", "text/x-setext" }, + { ".evy", "application/envoy" }, + { ".fdf", "application/vnd.fdf" }, + { ".fif", "application/fractals" }, + { ".fla", "application/octet-stream" }, + { ".flr", "x-world/x-vrml" }, + { ".flv", "video/x-flv" }, + { ".gif", "image/gif" }, + { ".gtar", "application/x-gtar" }, + { ".gz", "application/x-gzip" }, + { ".h", "text/plain" }, + { ".hdf", "application/x-hdf" }, + { ".hdml", "text/x-hdml" }, + { ".hhc", "application/x-oleobject" }, + { ".hhk", "application/octet-stream" }, + { ".hhp", "application/octet-stream" }, + { ".hlp", "application/winhlp" }, + { ".hqx", "application/mac-binhex40" }, + { ".hta", "application/hta" }, + { ".htc", "text/x-component" }, + { ".htm", "text/html" }, + { ".html", "text/html" }, + { ".htt", "text/webviewhtml" }, + { ".hxt", "text/html" }, + { ".ical", "text/calendar" }, + { ".icalendar", "text/calendar" }, + { ".ico", "image/x-icon" }, + { ".ics", "text/calendar" }, + { ".ief", "image/ief" }, + { ".ifb", "text/calendar" }, + { ".iii", "application/x-iphone" }, + { ".inf", "application/octet-stream" }, + { ".ins", "application/x-internet-signup" }, + { ".isp", "application/x-internet-signup" }, + { ".IVF", "video/x-ivf" }, + { ".jar", "application/java-archive" }, + { ".java", "application/octet-stream" }, + { ".jck", "application/liquidmotion" }, + { ".jcz", "application/liquidmotion" }, + { ".jfif", "image/pjpeg" }, + { ".jpb", "application/octet-stream" }, + { ".jpe", "image/jpeg" }, + { ".jpeg", "image/jpeg" }, + { ".jpg", "image/jpeg" }, + { ".js", "application/javascript" }, + { ".jsx", "text/jscript" }, + { ".latex", "application/x-latex" }, + { ".lit", "application/x-ms-reader" }, + { ".lpk", "application/octet-stream" }, + { ".lsf", "video/x-la-asf" }, + { ".lsx", "video/x-la-asf" }, + { ".lzh", "application/octet-stream" }, + { ".m13", "application/x-msmediaview" }, + { ".m14", "application/x-msmediaview" }, + { ".m1v", "video/mpeg" }, + { ".m2ts", "video/vnd.dlna.mpeg-tts" }, + { ".m3u", "audio/x-mpegurl" }, + { ".m4a", "audio/mp4" }, + { ".m4v", "video/mp4" }, + { ".man", "application/x-troff-man" }, + { ".manifest", "application/x-ms-manifest" }, + { ".map", "text/plain" }, + { ".mdb", "application/x-msaccess" }, + { ".mdp", "application/octet-stream" }, + { ".me", "application/x-troff-me" }, + { ".mht", "message/rfc822" }, + { ".mhtml", "message/rfc822" }, + { ".mid", "audio/mid" }, + { ".midi", "audio/mid" }, + { ".mix", "application/octet-stream" }, + { ".mmf", "application/x-smaf" }, + { ".mno", "text/xml" }, + { ".mny", "application/x-msmoney" }, + { ".mov", "video/quicktime" }, + { ".movie", "video/x-sgi-movie" }, + { ".mp2", "video/mpeg" }, + { ".mp3", "audio/mpeg" }, + { ".mp4", "video/mp4" }, + { ".mp4v", "video/mp4" }, + { ".mpa", "video/mpeg" }, + { ".mpe", "video/mpeg" }, + { ".mpeg", "video/mpeg" }, + { ".mpg", "video/mpeg" }, + { ".mpp", "application/vnd.ms-project" }, + { ".mpv2", "video/mpeg" }, + { ".ms", "application/x-troff-ms" }, + { ".msi", "application/octet-stream" }, + { ".mso", "application/octet-stream" }, + { ".mvb", "application/x-msmediaview" }, + { ".mvc", "application/x-miva-compiled" }, + { ".nc", "application/x-netcdf" }, + { ".nsc", "video/x-ms-asf" }, + { ".nws", "message/rfc822" }, + { ".ocx", "application/octet-stream" }, + { ".oda", "application/oda" }, + { ".odc", "text/x-ms-odc" }, + { ".ods", "application/oleobject" }, + { ".oga", "audio/ogg" }, + { ".ogg", "video/ogg" }, + { ".ogv", "video/ogg" }, + { ".ogx", "application/ogg" }, + { ".one", "application/onenote" }, + { ".onea", "application/onenote" }, + { ".onetoc", "application/onenote" }, + { ".onetoc2", "application/onenote" }, + { ".onetmp", "application/onenote" }, + { ".onepkg", "application/onenote" }, + { ".osdx", "application/opensearchdescription+xml" }, + { ".otf", "font/otf" }, + { ".p10", "application/pkcs10" }, + { ".p12", "application/x-pkcs12" }, + { ".p7b", "application/x-pkcs7-certificates" }, + { ".p7c", "application/pkcs7-mime" }, + { ".p7m", "application/pkcs7-mime" }, + { ".p7r", "application/x-pkcs7-certreqresp" }, + { ".p7s", "application/pkcs7-signature" }, + { ".pbm", "image/x-portable-bitmap" }, + { ".pcx", "application/octet-stream" }, + { ".pcz", "application/octet-stream" }, + { ".pdf", "application/pdf" }, + { ".pfb", "application/octet-stream" }, + { ".pfm", "application/octet-stream" }, + { ".pfx", "application/x-pkcs12" }, + { ".pgm", "image/x-portable-graymap" }, + { ".pko", "application/vnd.ms-pki.pko" }, + { ".pma", "application/x-perfmon" }, + { ".pmc", "application/x-perfmon" }, + { ".pml", "application/x-perfmon" }, + { ".pmr", "application/x-perfmon" }, + { ".pmw", "application/x-perfmon" }, + { ".png", "image/png" }, + { ".pnm", "image/x-portable-anymap" }, + { ".pnz", "image/png" }, + { ".pot", "application/vnd.ms-powerpoint" }, + { ".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" }, + { ".potx", "application/vnd.openxmlformats-officedocument.presentationml.template" }, + { ".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12" }, + { ".ppm", "image/x-portable-pixmap" }, + { ".pps", "application/vnd.ms-powerpoint" }, + { ".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" }, + { ".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" }, + { ".ppt", "application/vnd.ms-powerpoint" }, + { ".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" }, + { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, + { ".prf", "application/pics-rules" }, + { ".prm", "application/octet-stream" }, + { ".prx", "application/octet-stream" }, + { ".ps", "application/postscript" }, + { ".psd", "application/octet-stream" }, + { ".psm", "application/octet-stream" }, + { ".psp", "application/octet-stream" }, + { ".pub", "application/x-mspublisher" }, + { ".qt", "video/quicktime" }, + { ".qtl", "application/x-quicktimeplayer" }, + { ".qxd", "application/octet-stream" }, + { ".ra", "audio/x-pn-realaudio" }, + { ".ram", "audio/x-pn-realaudio" }, + { ".rar", "application/octet-stream" }, + { ".ras", "image/x-cmu-raster" }, + { ".rf", "image/vnd.rn-realflash" }, + { ".rgb", "image/x-rgb" }, + { ".rm", "application/vnd.rn-realmedia" }, + { ".rmi", "audio/mid" }, + { ".roff", "application/x-troff" }, + { ".rpm", "audio/x-pn-realaudio-plugin" }, + { ".rtf", "application/rtf" }, + { ".rtx", "text/richtext" }, + { ".scd", "application/x-msschedule" }, + { ".sct", "text/scriptlet" }, + { ".sea", "application/octet-stream" }, + { ".setpay", "application/set-payment-initiation" }, + { ".setreg", "application/set-registration-initiation" }, + { ".sgml", "text/sgml" }, + { ".sh", "application/x-sh" }, + { ".shar", "application/x-shar" }, + { ".sit", "application/x-stuffit" }, + { ".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12" }, + { ".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide" }, + { ".smd", "audio/x-smd" }, + { ".smi", "application/octet-stream" }, + { ".smx", "audio/x-smd" }, + { ".smz", "audio/x-smd" }, + { ".snd", "audio/basic" }, + { ".snp", "application/octet-stream" }, + { ".spc", "application/x-pkcs7-certificates" }, + { ".spl", "application/futuresplash" }, + { ".spx", "audio/ogg" }, + { ".src", "application/x-wais-source" }, + { ".ssm", "application/streamingmedia" }, + { ".sst", "application/vnd.ms-pki.certstore" }, + { ".stl", "application/vnd.ms-pki.stl" }, + { ".sv4cpio", "application/x-sv4cpio" }, + { ".sv4crc", "application/x-sv4crc" }, + { ".svg", "image/svg+xml" }, + { ".svgz", "image/svg+xml" }, + { ".swf", "application/x-shockwave-flash" }, + { ".t", "application/x-troff" }, + { ".tar", "application/x-tar" }, + { ".tcl", "application/x-tcl" }, + { ".tex", "application/x-tex" }, + { ".texi", "application/x-texinfo" }, + { ".texinfo", "application/x-texinfo" }, + { ".tgz", "application/x-compressed" }, + { ".thmx", "application/vnd.ms-officetheme" }, + { ".thn", "application/octet-stream" }, + { ".tif", "image/tiff" }, + { ".tiff", "image/tiff" }, + { ".toc", "application/octet-stream" }, + { ".tr", "application/x-troff" }, + { ".trm", "application/x-msterminal" }, + { ".ts", "video/vnd.dlna.mpeg-tts" }, + { ".tsv", "text/tab-separated-values" }, + { ".ttf", "application/octet-stream" }, + { ".tts", "video/vnd.dlna.mpeg-tts" }, + { ".txt", "text/plain" }, + { ".u32", "application/octet-stream" }, + { ".uls", "text/iuls" }, + { ".ustar", "application/x-ustar" }, + { ".vbs", "text/vbscript" }, + { ".vcf", "text/x-vcard" }, + { ".vcs", "text/plain" }, + { ".vdx", "application/vnd.ms-visio.viewer" }, + { ".vml", "text/xml" }, + { ".vsd", "application/vnd.visio" }, + { ".vss", "application/vnd.visio" }, + { ".vst", "application/vnd.visio" }, + { ".vsto", "application/x-ms-vsto" }, + { ".vsw", "application/vnd.visio" }, + { ".vsx", "application/vnd.visio" }, + { ".vtx", "application/vnd.visio" }, + { ".wav", "audio/wav" }, + { ".wax", "audio/x-ms-wax" }, + { ".wbmp", "image/vnd.wap.wbmp" }, + { ".wcm", "application/vnd.ms-works" }, + { ".wdb", "application/vnd.ms-works" }, + { ".webm", "video/webm" }, + { ".wks", "application/vnd.ms-works" }, + { ".wm", "video/x-ms-wm" }, + { ".wma", "audio/x-ms-wma" }, + { ".wmd", "application/x-ms-wmd" }, + { ".wmf", "application/x-msmetafile" }, + { ".wml", "text/vnd.wap.wml" }, + { ".wmlc", "application/vnd.wap.wmlc" }, + { ".wmls", "text/vnd.wap.wmlscript" }, + { ".wmlsc", "application/vnd.wap.wmlscriptc" }, + { ".wmp", "video/x-ms-wmp" }, + { ".wmv", "video/x-ms-wmv" }, + { ".wmx", "video/x-ms-wmx" }, + { ".wmz", "application/x-ms-wmz" }, + { ".woff", "application/font-woff" }, + { ".wps", "application/vnd.ms-works" }, + { ".wri", "application/x-mswrite" }, + { ".wrl", "x-world/x-vrml" }, + { ".wrz", "x-world/x-vrml" }, + { ".wsdl", "text/xml" }, + { ".wtv", "video/x-ms-wtv" }, + { ".wvx", "video/x-ms-wvx" }, + { ".x", "application/directx" }, + { ".xaf", "x-world/x-vrml" }, + { ".xaml", "application/xaml+xml" }, + { ".xap", "application/x-silverlight-app" }, + { ".xbap", "application/x-ms-xbap" }, + { ".xbm", "image/x-xbitmap" }, + { ".xdr", "text/plain" }, + { ".xht", "application/xhtml+xml" }, + { ".xhtml", "application/xhtml+xml" }, + { ".xla", "application/vnd.ms-excel" }, + { ".xlam", "application/vnd.ms-excel.addin.macroEnabled.12" }, + { ".xlc", "application/vnd.ms-excel" }, + { ".xlm", "application/vnd.ms-excel" }, + { ".xls", "application/vnd.ms-excel" }, + { ".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" }, + { ".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" }, + { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, + { ".xlt", "application/vnd.ms-excel" }, + { ".xltm", "application/vnd.ms-excel.template.macroEnabled.12" }, + { ".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" }, + { ".xlw", "application/vnd.ms-excel" }, + { ".xml", "text/xml" }, + { ".xof", "x-world/x-vrml" }, + { ".xpm", "image/x-xpixmap" }, + { ".xps", "application/vnd.ms-xpsdocument" }, + { ".xsd", "text/xml" }, + { ".xsf", "text/xml" }, + { ".xsl", "text/xml" }, + { ".xslt", "text/xml" }, + { ".xsn", "application/octet-stream" }, + { ".xtp", "application/octet-stream" }, + { ".xwd", "image/x-xwindowdump" }, + { ".z", "application/x-compress" }, + { ".zip", "application/x-zip-compressed" }, + }) + { + } +#endregion + + /// + /// Creates a lookup engine using the provided mapping. + /// It is recommended that the IDictionary instance use StringComparer.OrdinalIgnoreCase. + /// + /// + public FileExtensionContentTypeProvider(IDictionary mapping) + { + if (mapping == null) + { + throw new ArgumentNullException("mapping"); + } + Mappings = mapping; + } + + /// + /// The cross reference table of file extensions and content-types. + /// + public IDictionary Mappings { get; private set; } + + /// + /// Given a file path, determine the MIME type + /// + /// A file path + /// The resulting MIME type + /// True if MIME type could be determined + public bool TryGetContentType(string subpath, out string contentType) + { + string extension = Path.GetExtension(subpath); + if (extension == null) + { + contentType = null; + return false; + } + return Mappings.TryGetValue(extension, out contentType); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/ContentTypes/IContentTypeProvider.cs b/src/Microsoft.AspNet.StaticFiles/ContentTypes/IContentTypeProvider.cs new file mode 100644 index 0000000000..cd0263c1ab --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/ContentTypes/IContentTypeProvider.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.StaticFiles.ContentTypes +{ + /// + /// Used to look up MIME types given a file path + /// + public interface IContentTypeProvider + { + /// + /// Given a file path, determine the MIME type + /// + /// A file path + /// The resulting MIME type + /// True if MIME type could be determined + bool TryGetContentType(string subpath, out string contentType); + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/CustomDictionary.xml b/src/Microsoft.AspNet.StaticFiles/CustomDictionary.xml new file mode 100644 index 0000000000..78a76142f7 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/CustomDictionary.xml @@ -0,0 +1,10 @@ + + + + + Owin + + + + + diff --git a/src/Microsoft.AspNet.StaticFiles/DefaultFilesExtensions.cs b/src/Microsoft.AspNet.StaticFiles/DefaultFilesExtensions.cs new file mode 100644 index 0000000000..ac89ad46ca --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/DefaultFilesExtensions.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.StaticFiles; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet +{ + /// + /// Extension methods for the DefaultFilesMiddleware + /// + public static class DefaultFilesExtensions + { + /// + /// Enables default file mapping on the current path from the current directory + /// + /// + /// + public static IBuilder UseDefaultFiles(this IBuilder builder) + { + return builder.UseDefaultFiles(new DefaultFilesOptions()); + } + + /// + /// Enables default file mapping for the given request path from the directory of the same name + /// + /// + /// The relative request path and physical path. + /// + public static IBuilder UseDefaultFiles(this IBuilder builder, string requestPath) + { + return UseDefaultFiles(builder, new DefaultFilesOptions() { RequestPath = new PathString(requestPath) }); + } + + /// + /// Enables default file mapping with the given options + /// + /// + /// + /// + public static IBuilder UseDefaultFiles(this IBuilder builder, DefaultFilesOptions options) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + return builder.Use(next => new DefaultFilesMiddleware(next, options).Invoke); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.StaticFiles/DefaultFilesMiddleware.cs b/src/Microsoft.AspNet.StaticFiles/DefaultFilesMiddleware.cs new file mode 100644 index 0000000000..cccaeb047f --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/DefaultFilesMiddleware.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; +using Microsoft.Owin.FileSystems; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// This examines a directory path and determines if there is a default file present. + /// If so the file name is appended to the path and execution continues. + /// Note we don't just serve the file because it may require interpretation. + /// + public class DefaultFilesMiddleware + { + private readonly DefaultFilesOptions _options; + private readonly PathString _matchUrl; + private readonly RequestDelegate _next; + + /// + /// Creates a new instance of the DefaultFilesMiddleware. + /// + /// The next middleware in the pipeline. + /// The configuration options for this middleware. + public DefaultFilesMiddleware(RequestDelegate next, DefaultFilesOptions options) + { + if (next == null) + { + throw new ArgumentNullException("next"); + } + if (options == null) + { + throw new ArgumentNullException("options"); + } + if (options.FileSystem == null) + { + options.FileSystem = new PhysicalFileSystem("." + options.RequestPath.Value); + } + + _next = next; + _options = options; + _matchUrl = options.RequestPath; + } + + /// + /// This examines the request to see if it matches a configured directory, and if there are any files with the + /// configured default names in that directory. If so this will append the corresponding file name to the request + /// path for a later middleware to handle. + /// + /// + /// + public Task Invoke(HttpContext context) + { + IEnumerable dirContents; + PathString subpath; + if (Helpers.IsGetOrHeadMethod(context.Request.Method) + && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out subpath) + && _options.FileSystem.TryGetDirectoryContents(subpath.Value, out dirContents)) + { + // Check if any of our default files exist. + for (int matchIndex = 0; matchIndex < _options.DefaultFileNames.Count; matchIndex++) + { + string defaultFile = _options.DefaultFileNames[matchIndex]; + IFileInfo file; + // TryMatchPath will make sure subpath always ends with a "/" by adding it if needed. + if (_options.FileSystem.TryGetFileInfo(subpath + defaultFile, out file)) + { + // If the path matches a directory but does not end in a slash, redirect to add the slash. + // This prevents relative links from breaking. + if (!Helpers.PathEndsInSlash(context.Request.Path)) + { + context.Response.StatusCode = 301; + context.Response.Headers[Constants.Location] = context.Request.PathBase + context.Request.Path + "/"; + return Constants.CompletedTask; + } + + // Match found, re-write the url. A later middleware will actually serve the file. + context.Request.Path = new PathString(context.Request.Path.Value + defaultFile); + break; + } + } + } + + return _next(context); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/DefaultFilesOptions.cs b/src/Microsoft.AspNet.StaticFiles/DefaultFilesOptions.cs new file mode 100644 index 0000000000..d7d5069a8c --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/DefaultFilesOptions.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.StaticFiles.Infrastructure; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// Options for selecting default file names. + /// + public class DefaultFilesOptions : SharedOptionsBase + { + /// + /// Configuration for the DefaultFilesMiddleware. + /// + public DefaultFilesOptions() + : this(new SharedOptions()) + { + } + + /// + /// Configuration for the DefaultFilesMiddleware. + /// + /// + public DefaultFilesOptions(SharedOptions sharedOptions) + : base(sharedOptions) + { + // Prioritized list + DefaultFileNames = new List() + { + "default.htm", + "default.html", + "index.htm", + "index.html", + }; + } + + /// + /// An ordered list of file names to select by default. List length and ordering may affect performance. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Improves usability")] + public IList DefaultFileNames { get; set; } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserExtensions.cs b/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserExtensions.cs new file mode 100644 index 0000000000..74bf255f45 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserExtensions.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.StaticFiles; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet +{ + /// + /// Extension methods for the DirectoryBrowserMiddleware + /// + public static class DirectoryBrowserExtensions + { + /// + /// Enable directory browsing on the current path for the current directory + /// + /// + /// + public static IBuilder UseDirectoryBrowser(this IBuilder builder) + { + return builder.UseDirectoryBrowser(new DirectoryBrowserOptions()); + } + + /// + /// Enables directory browsing for the given request path from the directory of the same name + /// + /// + /// The relative request path and physical path. + /// + public static IBuilder UseDirectoryBrowser(this IBuilder builder, string requestPath) + { + return UseDirectoryBrowser(builder, new DirectoryBrowserOptions() { RequestPath = new PathString(requestPath) }); + } + + /// + /// Enable directory browsing with the given options + /// + /// + /// + /// + public static IBuilder UseDirectoryBrowser(this IBuilder builder, DirectoryBrowserOptions options) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + return builder.Use(next => new DirectoryBrowserMiddleware(next, options).Invoke); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserMiddleware.cs b/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserMiddleware.cs new file mode 100644 index 0000000000..87d56cb3a0 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserMiddleware.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Owin.FileSystems; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// Enables directory browsing + /// + public class DirectoryBrowserMiddleware + { + private readonly DirectoryBrowserOptions _options; + private readonly PathString _matchUrl; + private readonly RequestDelegate _next; + + /// + /// Creates a new instance of the SendFileMiddleware. + /// + /// The next middleware in the pipeline. + /// The configuration for this middleware. + public DirectoryBrowserMiddleware(RequestDelegate next, DirectoryBrowserOptions options) + { + if (next == null) + { + throw new ArgumentNullException("next"); + } + if (options == null) + { + throw new ArgumentNullException("options"); + } + if (options.Formatter == null) + { + throw new ArgumentException(Resources.Args_NoFormatter); + } + if (options.FileSystem == null) + { + options.FileSystem = new PhysicalFileSystem("." + options.RequestPath.Value); + } + + _next = next; + _options = options; + _matchUrl = options.RequestPath; + } + + /// + /// Examines the request to see if it matches a configured directory. If so, a view of the directory contents is returned. + /// + /// + /// + public Task Invoke(HttpContext context) + { + // Check if the URL matches any expected paths + PathString subpath; + IEnumerable contents; + if (Helpers.IsGetOrHeadMethod(context.Request.Method) + && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out subpath) + && TryGetDirectoryInfo(subpath, out contents)) + { + // If the path matches a directory but does not end in a slash, redirect to add the slash. + // This prevents relative links from breaking. + if (!Helpers.PathEndsInSlash(context.Request.Path)) + { + context.Response.StatusCode = 301; + context.Response.Headers[Constants.Location] = context.Request.PathBase + context.Request.Path + "/"; + return Constants.CompletedTask; + } + + return _options.Formatter.GenerateContentAsync(context, contents); + } + + return _next(context); + } + + private bool TryGetDirectoryInfo(PathString subpath, out IEnumerable contents) + { + return _options.FileSystem.TryGetDirectoryContents(subpath.Value, out contents); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserOptions.cs b/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserOptions.cs new file mode 100644 index 0000000000..8c24bb11c8 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserOptions.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.StaticFiles.DirectoryFormatters; +using Microsoft.AspNet.StaticFiles.Infrastructure; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// Directory browsing options + /// + public class DirectoryBrowserOptions : SharedOptionsBase + { + /// + /// Enabled directory browsing in the current physical directory for all request paths + /// + public DirectoryBrowserOptions() + : this(new SharedOptions()) + { + } + + /// + /// Enabled directory browsing in the current physical directory for all request paths + /// + /// + public DirectoryBrowserOptions(SharedOptions sharedOptions) + : base(sharedOptions) + { + Formatter = new HtmlDirectoryFormatter(); + } + + /// + /// The component that generates the view. + /// + public IDirectoryFormatter Formatter { get; set; } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/DirectoryFormatters/HtmlDirectoryFormatter.cs b/src/Microsoft.AspNet.StaticFiles/DirectoryFormatters/HtmlDirectoryFormatter.cs new file mode 100644 index 0000000000..8a5056d189 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/DirectoryFormatters/HtmlDirectoryFormatter.cs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Owin.FileSystems; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.StaticFiles.DirectoryFormatters +{ + /// + /// Generates an HTML view for a directory. + /// + public class HtmlDirectoryFormatter : IDirectoryFormatter + { + /// + /// Generates an HTML view for a directory. + /// + public virtual Task GenerateContentAsync(HttpContext context, IEnumerable contents) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + if (contents == null) + { + throw new ArgumentNullException("contents"); + } + + context.Response.ContentType = Constants.TextHtmlUtf8; + + if (Helpers.IsHeadMethod(context.Request.Method)) + { + // HEAD, no response body + return Constants.CompletedTask; + } + + PathString requestPath = context.Request.PathBase + context.Request.Path; + + var builder = new StringBuilder(); + + builder.AppendFormat( +@" +", CultureInfo.CurrentUICulture.TwoLetterISOLanguageName); + + builder.AppendFormat(@" + + {0} {1}", HtmlEncode(Resources.HtmlDir_IndexOf), HtmlEncode(requestPath.Value)); + + builder.Append(@" + + + +
"); + builder.AppendFormat(@" +

{0} /", HtmlEncode(Resources.HtmlDir_IndexOf)); + + string cumulativePath = "/"; + foreach (var segment in requestPath.Value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)) + { + cumulativePath = cumulativePath + segment + "/"; + builder.AppendFormat(@"{1}/", + HtmlEncode(cumulativePath), HtmlEncode(segment)); + } + + builder.AppendFormat(CultureInfo.CurrentUICulture, + @"

+ + + + + ", + HtmlEncode(Resources.HtmlDir_TableSummary), + HtmlEncode(Resources.HtmlDir_Name), + HtmlEncode(Resources.HtmlDir_Size), + HtmlEncode(Resources.HtmlDir_Modified), + HtmlEncode(Resources.HtmlDir_LastModified)); + + foreach (var subdir in contents.Where(info => info.IsDirectory)) + { + builder.AppendFormat(@" + + + + + ", + HtmlEncode(subdir.Name), + HtmlEncode(subdir.LastModified.ToString(CultureInfo.CurrentCulture))); + } + + foreach (var file in contents.Where(info => !info.IsDirectory)) + { + builder.AppendFormat(@" + + + + + ", + HtmlEncode(file.Name), + HtmlEncode(file.Length.ToString("n0", CultureInfo.CurrentCulture)), + HtmlEncode(file.LastModified.ToString(CultureInfo.CurrentCulture))); + } + + builder.Append(@" + +
{1}{2}{4}
{0}/{1}
{0}{1}{2}
+
+ +"); + string data = builder.ToString(); + byte[] bytes = Encoding.UTF8.GetBytes(data); + context.Response.ContentLength = bytes.Length; + return context.Response.Body.WriteAsync(bytes, 0, bytes.Length); + } + + private static string HtmlEncode(string body) + { + return WebUtility.HtmlEncode(body); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/DirectoryFormatters/IDirectoryFormatter.cs b/src/Microsoft.AspNet.StaticFiles/DirectoryFormatters/IDirectoryFormatter.cs new file mode 100644 index 0000000000..48c2b833a6 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/DirectoryFormatters/IDirectoryFormatter.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Owin.FileSystems; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.StaticFiles.DirectoryFormatters +{ + /// + /// Generates the view for a directory + /// + public interface IDirectoryFormatter + { + /// + /// Generates the view for a directory. + /// Implementers should properly handle HEAD requests. + /// Implementers should set all necessary response headers (e.g. Content-Type, Content-Length, etc.). + /// + Task GenerateContentAsync(HttpContext context, IEnumerable contents); + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/FileServerExtensions.cs b/src/Microsoft.AspNet.StaticFiles/FileServerExtensions.cs new file mode 100644 index 0000000000..f647704465 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/FileServerExtensions.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.StaticFiles; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet +{ + /// + /// Extension methods that combine all of the static file middleware components: + /// Default files, directory browsing, send file, and static files + /// + public static class FileServerExtensions + { + /// + /// Enable all static file middleware (except directory browsing) for the current request path in the current directory. + /// + /// + /// + public static IBuilder UseFileServer(this IBuilder builder) + { + return UseFileServer(builder, new FileServerOptions()); + } + + /// + /// Enable all static file middleware on for the current request path in the current directory. + /// + /// + /// Should directory browsing be enabled? + /// + public static IBuilder UseFileServer(this IBuilder builder, bool enableDirectoryBrowsing) + { + return UseFileServer(builder, new FileServerOptions() { EnableDirectoryBrowsing = enableDirectoryBrowsing }); + } + + /// + /// Enables all static file middleware (except directory browsing) for the given request path from the directory of the same name + /// + /// + /// The relative request path and physical path. + /// + public static IBuilder UseFileServer(this IBuilder builder, string requestPath) + { + return UseFileServer(builder, new FileServerOptions() { RequestPath = new PathString(requestPath) }); + } + + /// + /// Enable all static file middleware with the given options + /// + /// + /// + /// + public static IBuilder UseFileServer(this IBuilder builder, FileServerOptions options) + { + if (options == null) + { + throw new ArgumentNullException("options"); + } + + if (options.EnableDefaultFiles) + { + builder = builder.UseDefaultFiles(options.DefaultFilesOptions); + } + + if (options.EnableDirectoryBrowsing) + { + builder = builder.UseDirectoryBrowser(options.DirectoryBrowserOptions); + } + + return builder + .UseSendFileFallback() + .UseStaticFiles(options.StaticFileOptions); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.StaticFiles/FileServerOptions.cs b/src/Microsoft.AspNet.StaticFiles/FileServerOptions.cs new file mode 100644 index 0000000000..22f4e17184 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/FileServerOptions.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.StaticFiles.Infrastructure; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// Options for all of the static file middleware components + /// + public class FileServerOptions : SharedOptionsBase + { + /// + /// Creates a combined options class for all of the static file middleware components. + /// + public FileServerOptions() + : base(new SharedOptions()) + { + StaticFileOptions = new StaticFileOptions(SharedOptions); + DirectoryBrowserOptions = new DirectoryBrowserOptions(SharedOptions); + DefaultFilesOptions = new DefaultFilesOptions(SharedOptions); + EnableDefaultFiles = true; + } + + /// + /// Options for configuring the StaticFileMiddleware. + /// + public StaticFileOptions StaticFileOptions { get; private set; } + + /// + /// Options for configuring the DirectoryBrowserMiddleware. + /// + public DirectoryBrowserOptions DirectoryBrowserOptions { get; private set; } + + /// + /// Options for configuring the DefaultFilesMiddleware. + /// + public DefaultFilesOptions DefaultFilesOptions { get; private set; } + + /// + /// Directory browsing is disabled by default. + /// + public bool EnableDirectoryBrowsing { get; set; } + + /// + /// Default files are enabled by default. + /// + public bool EnableDefaultFiles { get; set; } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/Helpers.cs b/src/Microsoft.AspNet.StaticFiles/Helpers.cs new file mode 100644 index 0000000000..040ebf1dfd --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/Helpers.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.StaticFiles +{ + internal static class Helpers + { + internal static bool IsGetOrHeadMethod(string method) + { + return IsGetMethod(method) || IsHeadMethod(method); + } + + internal static bool IsGetMethod(string method) + { + return string.Equals("GET", method, StringComparison.OrdinalIgnoreCase); + } + + internal static bool IsHeadMethod(string method) + { + return string.Equals("HEAD", method, StringComparison.OrdinalIgnoreCase); + } + + internal static bool PathEndsInSlash(PathString path) + { + return path.Value.EndsWith("/", StringComparison.Ordinal); + } + + internal static bool TryMatchPath(HttpContext context, PathString matchUrl, bool forDirectory, out PathString subpath) + { + var path = context.Request.Path; + + if (forDirectory && !PathEndsInSlash(path)) + { + path += new PathString("/"); + } + + if (path.StartsWithSegments(matchUrl, out subpath)) + { + return true; + } + return false; + } + + internal static bool TryParseHttpDate(string dateString, out DateTime parsedDate) + { + return DateTime.TryParseExact(dateString, Constants.HttpDateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsedDate); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/Infrastructure/RangeHelpers.cs b/src/Microsoft.AspNet.StaticFiles/Infrastructure/RangeHelpers.cs new file mode 100644 index 0000000000..c5c355cbde --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/Infrastructure/RangeHelpers.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Microsoft.AspNet.StaticFiles.Infrastructure +{ + internal static class RangeHelpers + { + // Examples: + // bytes=0-499 + // bytes=500- + // bytes=-500 + // bytes=0-0,-1 + // bytes=500-600,601-999 + // Any individual bad range fails the whole parse and the header should be ignored. + internal static bool TryParseRanges(string rangeHeader, out IList> parsedRanges) + { + parsedRanges = null; + if (string.IsNullOrWhiteSpace(rangeHeader) + || !rangeHeader.StartsWith("bytes=", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + string[] subRanges = rangeHeader.Substring("bytes=".Length).Replace(" ", string.Empty).Split(','); + + List> ranges = new List>(); + + for (int i = 0; i < subRanges.Length; i++) + { + long? first = null, second = null; + string subRange = subRanges[i]; + int dashIndex = subRange.IndexOf('-'); + if (dashIndex < 0) + { + return false; + } + else if (dashIndex == 0) + { + // -500 + string remainder = subRange.Substring(1); + if (!TryParseLong(remainder, out second)) + { + return false; + } + } + else if (dashIndex == (subRange.Length - 1)) + { + // 500- + string remainder = subRange.Substring(0, subRange.Length - 1); + if (!TryParseLong(remainder, out first)) + { + return false; + } + } + else + { + // 0-499 + string firstString = subRange.Substring(0, dashIndex); + string secondString = subRange.Substring(dashIndex + 1, subRange.Length - dashIndex - 1); + if (!TryParseLong(firstString, out first) || !TryParseLong(secondString, out second) + || first.Value > second.Value) + { + return false; + } + } + + ranges.Add(new Tuple(first, second)); + } + + if (ranges.Count > 0) + { + parsedRanges = ranges; + return true; + } + return false; + } + + private static bool TryParseLong(string input, out long? result) + { + int temp; + if (!string.IsNullOrWhiteSpace(input) + && int.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out temp)) + { + result = temp; + return true; + } + result = null; + return false; + } + + // 14.35.1 Byte Ranges - If a syntactically valid byte-range-set includes at least one byte-range-spec whose + // first-byte-pos is less than the current length of the entity-body, or at least one suffix-byte-range-spec + // with a non-zero suffix-length, then the byte-range-set is satisfiable. + // Adjusts ranges to be absolute and within bounds. + internal static IList> NormalizeRanges(IList> ranges, long length) + { + IList> normalizedRanges = new List>(ranges.Count); + for (int i = 0; i < ranges.Count; i++) + { + Tuple range = ranges[i]; + long? start = range.Item1, end = range.Item2; + + // X-[Y] + if (start.HasValue) + { + if (start.Value >= length) + { + // Not satisfiable, skip/discard. + continue; + } + if (!end.HasValue || end.Value >= length) + { + end = length - 1; + } + } + else + { + // suffix range "-X" e.g. the last X bytes, resolve + if (end.Value == 0) + { + // Not satisfiable, skip/discard. + continue; + } + + long bytes = Math.Min(end.Value, length); + start = length - bytes; + end = start + bytes - 1; + } + normalizedRanges.Add(new Tuple(start.Value, end.Value)); + } + return normalizedRanges; + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/Infrastructure/SharedOptions.cs b/src/Microsoft.AspNet.StaticFiles/Infrastructure/SharedOptions.cs new file mode 100644 index 0000000000..06ff75c429 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/Infrastructure/SharedOptions.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.Owin.FileSystems; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.StaticFiles.Infrastructure +{ + /// + /// Options common to several middleware components + /// + public class SharedOptions + { + private PathString _requestPath; + + /// + /// Defaults to all request paths and the current physical directory. + /// + public SharedOptions() + { + RequestPath = PathString.Empty; + } + + /// + /// The request path that maps to static resources + /// + public PathString RequestPath + { + get { return _requestPath; } + set + { + if (value.HasValue && value.Value.EndsWith("/", StringComparison.Ordinal)) + { + throw new ArgumentException("Request path must not end in a slash"); + } + _requestPath = value; + } + } + + /// + /// The file system used to locate resources + /// + public IFileSystem FileSystem { get; set; } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/Infrastructure/SharedOptionsBase.cs b/src/Microsoft.AspNet.StaticFiles/Infrastructure/SharedOptionsBase.cs new file mode 100644 index 0000000000..7715dcc6ce --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/Infrastructure/SharedOptionsBase.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.Owin.FileSystems; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.StaticFiles.Infrastructure +{ + /// + /// Options common to several middleware components + /// + /// The type of the subclass + public abstract class SharedOptionsBase + { + /// + /// Creates an new instance of the SharedOptionsBase. + /// + /// + protected SharedOptionsBase(SharedOptions sharedOptions) + { + if (sharedOptions == null) + { + throw new ArgumentNullException("sharedOptions"); + } + + SharedOptions = sharedOptions; + } + + /// + /// Options common to several middleware components + /// + protected SharedOptions SharedOptions { get; private set; } + + /// + /// The relative request path that maps to static resources. + /// + public PathString RequestPath + { + get { return SharedOptions.RequestPath; } + set { SharedOptions.RequestPath = value; } + } + + /// + /// The file system used to locate resources + /// + public IFileSystem FileSystem + { + get { return SharedOptions.FileSystem; } + set { SharedOptions.FileSystem = value; } + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.StaticFiles/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..a373b20da5 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/Properties/AssemblyInfo.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using System.Resources; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. + +[assembly: AssemblyTitle("Microsoft.AspNet.StaticFiles")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM + +[assembly: Guid("310c92f5-1719-4616-9ca8-a5788fcb86f8")] +[assembly: CLSCompliant(true)] +[assembly: NeutralResourcesLanguage("en-US")] diff --git a/src/Microsoft.AspNet.StaticFiles/Resources.Designer.cs b/src/Microsoft.AspNet.StaticFiles/Resources.Designer.cs new file mode 100644 index 0000000000..d6a4919ce7 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/Resources.Designer.cs @@ -0,0 +1,144 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.34006 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.StaticFiles { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.StaticFiles.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to No IContentTypeProvider was specified.. + /// + internal static string Args_NoContentTypeProvider { + get { + return ResourceManager.GetString("Args_NoContentTypeProvider", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No formatter provided.. + /// + internal static string Args_NoFormatter { + get { + return ResourceManager.GetString("Args_NoFormatter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This server does not support the sendfile.SendAsync extension.. + /// + internal static string Exception_SendFileNotSupported { + get { + return ResourceManager.GetString("Exception_SendFileNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Index of. + /// + internal static string HtmlDir_IndexOf { + get { + return ResourceManager.GetString("HtmlDir_IndexOf", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Last Modified. + /// + internal static string HtmlDir_LastModified { + get { + return ResourceManager.GetString("HtmlDir_LastModified", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Modified. + /// + internal static string HtmlDir_Modified { + get { + return ResourceManager.GetString("HtmlDir_Modified", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Name. + /// + internal static string HtmlDir_Name { + get { + return ResourceManager.GetString("HtmlDir_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Size. + /// + internal static string HtmlDir_Size { + get { + return ResourceManager.GetString("HtmlDir_Size", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The list of files in the given directory. Column headers are listed in the first row.. + /// + internal static string HtmlDir_TableSummary { + get { + return ResourceManager.GetString("HtmlDir_TableSummary", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/Resources.resx b/src/Microsoft.AspNet.StaticFiles/Resources.resx new file mode 100644 index 0000000000..6c58583e87 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/Resources.resx @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + No IContentTypeProvider was specified. + + + No formatter provided. + + + This server does not support the sendfile.SendAsync extension. + + + Index of + + + Last Modified + + + Modified + + + Name + + + Size + + + The list of files in the given directory. Column headers are listed in the first row. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.StaticFiles/SendFileExtensions.cs b/src/Microsoft.AspNet.StaticFiles/SendFileExtensions.cs new file mode 100644 index 0000000000..5b62726b9d --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/SendFileExtensions.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// Extension methods for the SendFileMiddleware + /// + public static class SendFileExtensions + { + /// + /// Provide a SendFile fallback if another component does not. + /// + /// + /// + public static IBuilder UseSendFileFallback(this IBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + /* TODO builder.GetItem(typeof(ISendFile)) + + // Check for advertised support + if (IsSendFileSupported(builder.Properties)) + { + return builder; + } + + // Otherwise, insert a fallback SendFile middleware and advertise support + SetSendFileCapability(builder.Properties); + */ + return builder.Use(next => new SendFileMiddleware(next).Invoke); + } + + private static bool IsSendFileSupported(IDictionary properties) + { + object obj; + if (properties.TryGetValue(Constants.ServerCapabilitiesKey, out obj)) + { + var capabilities = (IDictionary)obj; + if (capabilities.TryGetValue(Constants.SendFileVersionKey, out obj) + && Constants.SendFileVersion.Equals((string)obj, StringComparison.Ordinal)) + { + return true; + } + } + return false; + } + + private static void SetSendFileCapability(IDictionary properties) + { + object obj; + if (properties.TryGetValue(Constants.ServerCapabilitiesKey, out obj)) + { + var capabilities = (IDictionary)obj; + capabilities[Constants.SendFileVersionKey] = Constants.SendFileVersion; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.StaticFiles/SendFileMiddleware.cs b/src/Microsoft.AspNet.StaticFiles/SendFileMiddleware.cs new file mode 100644 index 0000000000..18901d981b --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/SendFileMiddleware.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.HttpFeature; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// This middleware provides an efficient fallback mechanism for sending static files + /// when the server does not natively support such a feature. + /// The caller is responsible for setting all headers in advance. + /// The caller is responsible for performing the correct impersonation to give access to the file. + /// + public class SendFileMiddleware + { + private readonly RequestDelegate _next; + + /// + /// Creates a new instance of the SendFileMiddleware. + /// + /// The next middleware in the pipeline. + public SendFileMiddleware(RequestDelegate next) + { + if (next == null) + { + throw new ArgumentNullException("next"); + } + + _next = next; + } + + public Task Invoke(HttpContext context) + { + // Check if there is a SendFile feature already present + if (context.GetFeature() == null) + { + context.SetFeature(new SendFileWrapper(context.Response.Body)); + } + + return _next(context); + } + + private class SendFileWrapper : IHttpSendFile + { + private readonly Stream _output; + + internal SendFileWrapper(Stream output) + { + _output = output; + } + + // Not safe for overlapped writes. + public async Task SendFileAsync(string fileName, long offset, long? length, CancellationToken cancel) + { + cancel.ThrowIfCancellationRequested(); + + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException("fileName"); + } + if (!File.Exists(fileName)) + { + throw new FileNotFoundException(string.Empty, fileName); + } + + var fileInfo = new FileInfo(fileName); + if (offset < 0 || offset > fileInfo.Length) + { + throw new ArgumentOutOfRangeException("offset", offset, string.Empty); + } + + if (length.HasValue && + (length.Value < 0 || length.Value > fileInfo.Length - offset)) + { + throw new ArgumentOutOfRangeException("length", length, string.Empty); + } + + Stream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 1024 * 64, +#if NET45 + FileOptions.Asynchronous | FileOptions.SequentialScan); +#else + useAsync: true); +#endif + try + { + fileStream.Seek(offset, SeekOrigin.Begin); + await StreamCopyOperation.CopyToAsync(fileStream, _output, length, cancel); + } + finally + { + fileStream.Dispose(); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/SendFileResponseExtensions.cs b/src/Microsoft.AspNet.StaticFiles/SendFileResponseExtensions.cs new file mode 100644 index 0000000000..4ea40268d3 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/SendFileResponseExtensions.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.StaticFiles; +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.HttpFeature; + +namespace Microsoft.Owin +{ + /// + /// Provides extensions for HttpResponse exposing the SendFile extension. + /// + public static class SendFileResponseExtensions + { + /// + /// Checks if the SendFile extension is supported. + /// + /// + /// True if sendfile.SendAsync is defined in the environment. + public static bool SupportsSendFile(this HttpResponse response) + { + if (response == null) + { + throw new ArgumentNullException("response"); + } + return response.HttpContext.GetFeature() != null; + } + + /// + /// Sends the given file using the SendFile extension. + /// + /// + /// + /// + public static Task SendFileAsync(this HttpResponse response, string fileName) + { + if (response == null) + { + throw new ArgumentNullException("response"); + } + return response.SendFileAsync(fileName, 0, null, CancellationToken.None); + } + + /// + /// Sends the given file using the SendFile extension. + /// + /// + /// The full or relative path to the file. + /// The offset in the file. + /// The number of types to send, or null to send the remainder of the file. + /// + /// + public static Task SendFileAsync(this HttpResponse response, string fileName, long offset, long? count, CancellationToken cancellationToken) + { + if (response == null) + { + throw new ArgumentNullException("response"); + } + IHttpSendFile sendFile = response.HttpContext.GetFeature(); + if (sendFile == null) + { + throw new NotSupportedException(Resources.Exception_SendFileNotSupported); + } + + return sendFile.SendFileAsync(fileName, offset, count, cancellationToken); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/StaticFileContext.cs b/src/Microsoft.AspNet.StaticFiles/StaticFileContext.cs new file mode 100644 index 0000000000..6cbd30a93e --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/StaticFileContext.cs @@ -0,0 +1,393 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.StaticFiles.Infrastructure; +using Microsoft.Owin.FileSystems; + +namespace Microsoft.AspNet.StaticFiles +{ + internal struct StaticFileContext + { + private readonly HttpContext _context; + private readonly StaticFileOptions _options; + private readonly PathString _matchUrl; + private readonly HttpRequest _request; + private readonly HttpResponse _response; + private string _method; + private bool _isGet; + private bool _isHead; + private PathString _subPath; + private string _contentType; + private IFileInfo _fileInfo; + private long _length; + private DateTime _lastModified; + private string _lastModifiedString; + private string _etag; + private string _etagQuoted; + + private PreconditionState _ifMatchState; + private PreconditionState _ifNoneMatchState; + private PreconditionState _ifModifiedSinceState; + private PreconditionState _ifUnmodifiedSinceState; + + private IList> _ranges; + + public StaticFileContext(HttpContext context, StaticFileOptions options, PathString matchUrl) + { + _context = context; + _options = options; + _matchUrl = matchUrl; + _request = context.Request; + _response = context.Response; + + _method = null; + _isGet = false; + _isHead = false; + _subPath = PathString.Empty; + _contentType = null; + _fileInfo = null; + _length = 0; + _lastModified = new DateTime(); + _etag = null; + _etagQuoted = null; + _lastModifiedString = null; + _ifMatchState = PreconditionState.Unspecified; + _ifNoneMatchState = PreconditionState.Unspecified; + _ifModifiedSinceState = PreconditionState.Unspecified; + _ifUnmodifiedSinceState = PreconditionState.Unspecified; + _ranges = null; + } + + internal enum PreconditionState + { + Unspecified, + NotModified, + ShouldProcess, + PreconditionFailed, + } + + public bool IsHeadMethod + { + get { return _isHead; } + } + + public bool IsRangeRequest + { + get { return _ranges != null; } + } + + public bool ValidateMethod() + { + _method = _request.Method; + _isGet = Helpers.IsGetMethod(_method); + _isHead = Helpers.IsHeadMethod(_method); + return _isGet || _isHead; + } + + // Check if the URL matches any expected paths + public bool ValidatePath() + { + return Helpers.TryMatchPath(_context, _matchUrl, forDirectory: false, subpath: out _subPath); + } + + public bool LookupContentType() + { + if (_options.ContentTypeProvider.TryGetContentType(_subPath.Value, out _contentType)) + { + return true; + } + + if (_options.ServeUnknownFileTypes) + { + _contentType = _options.DefaultContentType; + return true; + } + + return false; + } + + public bool LookupFileInfo() + { + bool found = _options.FileSystem.TryGetFileInfo(_subPath.Value, out _fileInfo); + if (found) + { + _length = _fileInfo.Length; + + DateTime last = _fileInfo.LastModified; + // Truncate to the second. + _lastModified = new DateTime(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Kind); + _lastModifiedString = _lastModified.ToString(Constants.HttpDateFormat, CultureInfo.InvariantCulture); + + long etagHash = _lastModified.ToFileTimeUtc() ^ _length; + _etag = Convert.ToString(etagHash, 16); + _etagQuoted = '\"' + _etag + '\"'; + } + return found; + } + + public void ComprehendRequestHeaders() + { + ComputeIfMatch(); + + ComputeIfModifiedSince(); + + ComputeRange(); + } + + private void ComputeIfMatch() + { + // 14.24 If-Match + IList ifMatch = _request.Headers.GetCommaSeparatedValues(Constants.IfMatch); // Removes quotes + if (ifMatch != null) + { + _ifMatchState = PreconditionState.PreconditionFailed; + foreach (var segment in ifMatch) + { + if (segment.Equals("*", StringComparison.Ordinal) + || segment.Equals(_etag, StringComparison.Ordinal)) + { + _ifMatchState = PreconditionState.ShouldProcess; + break; + } + } + } + + // 14.26 If-None-Match + IList ifNoneMatch = _request.Headers.GetCommaSeparatedValues(Constants.IfNoneMatch); + if (ifNoneMatch != null) + { + _ifNoneMatchState = PreconditionState.ShouldProcess; + foreach (var segment in ifNoneMatch) + { + if (segment.Equals("*", StringComparison.Ordinal) + || segment.Equals(_etag, StringComparison.Ordinal)) + { + _ifNoneMatchState = PreconditionState.NotModified; + break; + } + } + } + } + + private void ComputeIfModifiedSince() + { + // 14.25 If-Modified-Since + string ifModifiedSinceString = _request.Headers.Get(Constants.IfModifiedSince); + DateTime ifModifiedSince; + if (Helpers.TryParseHttpDate(ifModifiedSinceString, out ifModifiedSince)) + { + bool modified = ifModifiedSince < _lastModified; + _ifModifiedSinceState = modified ? PreconditionState.ShouldProcess : PreconditionState.NotModified; + } + + // 14.28 If-Unmodified-Since + string ifUnmodifiedSinceString = _request.Headers.Get(Constants.IfUnmodifiedSince); + DateTime ifUnmodifiedSince; + if (Helpers.TryParseHttpDate(ifUnmodifiedSinceString, out ifUnmodifiedSince)) + { + bool unmodified = ifUnmodifiedSince >= _lastModified; + _ifUnmodifiedSinceState = unmodified ? PreconditionState.ShouldProcess : PreconditionState.PreconditionFailed; + } + } + + private void ComputeRange() + { + // 14.35 Range + // http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-24 + + // A server MUST ignore a Range header field received with a request method other + // than GET. + if (!_isGet) + { + return; + } + + string rangeHeader = _request.Headers.Get(Constants.Range); + IList> ranges; + if (!RangeHelpers.TryParseRanges(rangeHeader, out ranges)) + { + return; + } + + if (ranges.Count > 1) + { + // multiple range headers not yet supported + return; + } + + // 14.27 If-Range + string ifRangeHeader = _request.Headers.Get(Constants.IfRange); + if (!string.IsNullOrWhiteSpace(ifRangeHeader)) + { + // If the validator given in the If-Range header field matches the + // current validator for the selected representation of the target + // resource, then the server SHOULD process the Range header field as + // requested. If the validator does not match, the server MUST ignore + // the Range header field. + DateTime ifRangeLastModified; + bool ignoreRangeHeader = false; + if (Helpers.TryParseHttpDate(ifRangeHeader, out ifRangeLastModified)) + { + if (_lastModified > ifRangeLastModified) + { + ignoreRangeHeader = true; + } + } + else + { + if (!_etagQuoted.Equals(ifRangeHeader)) + { + ignoreRangeHeader = true; + } + } + if (ignoreRangeHeader) + { + return; + } + } + + _ranges = RangeHelpers.NormalizeRanges(ranges, _length); + } + + public void ApplyResponseHeaders(int statusCode) + { + _response.StatusCode = statusCode; + if (statusCode < 400) + { + // these headers are returned for 200, 206, and 304 + // they are not returned for 412 and 416 + if (!string.IsNullOrEmpty(_contentType)) + { + _response.ContentType = _contentType; + } + _response.Headers.Set(Constants.LastModified, _lastModifiedString); + _response.Headers.Set(Constants.ETag, _etagQuoted); + } + if (statusCode == Constants.Status200Ok) + { + // this header is only returned here for 200 + // it already set to the returned range for 206 + // it is not returned for 304, 412, and 416 + _response.ContentLength = _length; + } + _options.OnPrepareResponse(new StaticFileResponseContext() + { + Context = _context, + File = _fileInfo, + }); + } + + public PreconditionState GetPreconditionState() + { + return GetMaxPreconditionState(_ifMatchState, _ifNoneMatchState, + _ifModifiedSinceState, _ifUnmodifiedSinceState); + } + + private static PreconditionState GetMaxPreconditionState(params PreconditionState[] states) + { + PreconditionState max = PreconditionState.Unspecified; + for (int i = 0; i < states.Length; i++) + { + if (states[i] > max) + { + max = states[i]; + } + } + return max; + } + + public Task SendStatusAsync(int statusCode) + { + ApplyResponseHeaders(statusCode); + + return Constants.CompletedTask; + } + + public async Task SendAsync() + { + ApplyResponseHeaders(Constants.Status200Ok); + + string physicalPath = _fileInfo.PhysicalPath; + IHttpSendFile sendFile = _context.GetFeature(); + if (sendFile != null && !string.IsNullOrEmpty(physicalPath)) + { + await sendFile.SendFileAsync(physicalPath, 0, _length, _request.CallCanceled); + return; + } + + Stream readStream = _fileInfo.CreateReadStream(); + try + { + await StreamCopyOperation.CopyToAsync(readStream, _response.Body, _length, _request.CallCanceled); + } + finally + { + readStream.Dispose(); + } + } + + // When there is only a single range the bytes are sent directly in the body. + internal async Task SendRangeAsync() + { + bool rangeNotSatisfiable = false; + if (_ranges.Count == 0) + { + rangeNotSatisfiable = true; + } + + if (rangeNotSatisfiable) + { + // 14.16 Content-Range - A server sending a response with status code 416 (Requested range not satisfiable) + // SHOULD include a Content-Range field with a byte-range-resp-spec of "*". The instance-length specifies + // the current length of the selected resource. e.g. */length + _response.Headers[Constants.ContentRange] = "bytes */" + _length.ToString(CultureInfo.InvariantCulture); + ApplyResponseHeaders(Constants.Status416RangeNotSatisfiable); + return; + } + + // Multi-range is not supported. + Debug.Assert(_ranges.Count == 1); + + long start, length; + _response.Headers[Constants.ContentRange] = ComputeContentRange(_ranges[0], out start, out length); + _response.ContentLength = length; + ApplyResponseHeaders(Constants.Status206PartialContent); + + string physicalPath = _fileInfo.PhysicalPath; + IHttpSendFile sendFile = _context.GetFeature(); + if (sendFile != null && !string.IsNullOrEmpty(physicalPath)) + { + await sendFile.SendFileAsync(physicalPath, start, length, _request.CallCanceled); + return; + } + + Stream readStream = _fileInfo.CreateReadStream(); + try + { + readStream.Seek(start, SeekOrigin.Begin); // TODO: What if !CanSeek? + await StreamCopyOperation.CopyToAsync(readStream, _response.Body, length, _request.CallCanceled); + } + finally + { + readStream.Dispose(); + } + } + + // Note: This assumes ranges have been normalized to absolute byte offsets. + private string ComputeContentRange(Tuple range, out long start, out long length) + { + start = range.Item1; + long end = range.Item2; + length = end - start + 1; + return string.Format(CultureInfo.InvariantCulture, "bytes {0}-{1}/{2}", start, end, _length); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/StaticFileExtensions.cs b/src/Microsoft.AspNet.StaticFiles/StaticFileExtensions.cs new file mode 100644 index 0000000000..be625dcc60 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/StaticFileExtensions.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.StaticFiles; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet +{ + /// + /// Extension methods for the StaticFileMiddleware + /// + public static class StaticFileExtensions + { + /// + /// Enables static file serving for the current request path from the current directory + /// + /// + /// + public static IBuilder UseStaticFiles(this IBuilder builder) + { + return UseStaticFiles(builder, new StaticFileOptions()); + } + + /// + /// Enables static file serving for the given request path from the directory of the same name + /// + /// + /// The relative request path and physical path. + /// + public static IBuilder UseStaticFiles(this IBuilder builder, string requestPath) + { + return UseStaticFiles(builder, new StaticFileOptions() { RequestPath = new PathString(requestPath) }); + } + + /// + /// Enables static file serving with the given options + /// + /// + /// + /// + public static IBuilder UseStaticFiles(this IBuilder builder, StaticFileOptions options) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + return builder.Use(next => new StaticFileMiddleware(next, options).Invoke); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/StaticFileMiddleware.cs b/src/Microsoft.AspNet.StaticFiles/StaticFileMiddleware.cs new file mode 100644 index 0000000000..f1bc8aa17f --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/StaticFileMiddleware.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; +using Microsoft.Owin.FileSystems; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// Enables serving static files for a given request path + /// + public class StaticFileMiddleware + { + private readonly StaticFileOptions _options; + private readonly PathString _matchUrl; + private readonly RequestDelegate _next; + + /// + /// Creates a new instance of the StaticFileMiddleware. + /// + /// The next middleware in the pipeline. + /// The configuration options. + public StaticFileMiddleware(RequestDelegate next, StaticFileOptions options) + { + if (next == null) + { + throw new ArgumentNullException("next"); + } + if (options == null) + { + throw new ArgumentNullException("options"); + } + if (options.ContentTypeProvider == null) + { + throw new ArgumentException(Resources.Args_NoContentTypeProvider); + } + if (options.FileSystem == null) + { + options.FileSystem = new PhysicalFileSystem("." + options.RequestPath.Value); + } + + _next = next; + _options = options; + _matchUrl = options.RequestPath; + } + + /// + /// Processes a request to determine if it matches a known file, and if so, serves it. + /// + /// + /// + public Task Invoke(HttpContext context) + { + var fileContext = new StaticFileContext(context, _options, _matchUrl); + if (fileContext.ValidateMethod() + && fileContext.ValidatePath() + && fileContext.LookupContentType() + && fileContext.LookupFileInfo()) + { + fileContext.ComprehendRequestHeaders(); + + switch (fileContext.GetPreconditionState()) + { + case StaticFileContext.PreconditionState.Unspecified: + case StaticFileContext.PreconditionState.ShouldProcess: + if (fileContext.IsHeadMethod) + { + return fileContext.SendStatusAsync(Constants.Status200Ok); + } + if (fileContext.IsRangeRequest) + { + return fileContext.SendRangeAsync(); + } + return fileContext.SendAsync(); + + case StaticFileContext.PreconditionState.NotModified: + return fileContext.SendStatusAsync(Constants.Status304NotModified); + + case StaticFileContext.PreconditionState.PreconditionFailed: + return fileContext.SendStatusAsync(Constants.Status412PreconditionFailed); + + default: + throw new NotImplementedException(fileContext.GetPreconditionState().ToString()); + } + } + + return _next(context); + } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/StaticFileOptions.cs b/src/Microsoft.AspNet.StaticFiles/StaticFileOptions.cs new file mode 100644 index 0000000000..3e6b0ee7d5 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/StaticFileOptions.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.StaticFiles.ContentTypes; +using Microsoft.AspNet.StaticFiles.Infrastructure; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// Options for serving static files + /// + public class StaticFileOptions : SharedOptionsBase + { + /// + /// Defaults to all request paths in the current physical directory + /// + public StaticFileOptions() : this(new SharedOptions()) + { + } + + /// + /// Defaults to all request paths in the current physical directory + /// + /// + public StaticFileOptions(SharedOptions sharedOptions) : base(sharedOptions) + { + ContentTypeProvider = new FileExtensionContentTypeProvider(); + + OnPrepareResponse = _ => { }; + } + + /// + /// Used to map files to content-types. + /// + public IContentTypeProvider ContentTypeProvider { get; set; } + + /// + /// The default content type for a request if the ContentTypeProvider cannot determine one. + /// None is provided by default, so the client must determine the format themselves. + /// http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7 + /// + public string DefaultContentType { get; set; } + + /// + /// If the file is not a recognized content-type should it be served? + /// Default: false. + /// + public bool ServeUnknownFileTypes { get; set; } + + /// + /// Called after the status code and headers have been set, but before the body has been written. + /// This can be used to add or change the response headers. + /// + public Action OnPrepareResponse { get; set; } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/StaticFileResponseContext.cs b/src/Microsoft.AspNet.StaticFiles/StaticFileResponseContext.cs new file mode 100644 index 0000000000..2f681722be --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/StaticFileResponseContext.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.Owin.FileSystems; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.StaticFiles +{ + /// + /// Contains information about the request and the file that will be served in response. + /// + public class StaticFileResponseContext + { + /// + /// The request and response information. + /// + public HttpContext Context { get; internal set; } + + /// + /// The file to be served. + /// + public IFileInfo File { get; internal set; } + } +} diff --git a/src/Microsoft.AspNet.StaticFiles/StreamCopyOperation.cs b/src/Microsoft.AspNet.StaticFiles/StreamCopyOperation.cs new file mode 100644 index 0000000000..aed649e5f8 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/StreamCopyOperation.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.Contracts; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.StaticFiles +{ + // FYI: In most cases the source will be a FileStream and the destination will be to the network. + internal static class StreamCopyOperation + { + private const int DefaultBufferSize = 1024 * 16; + + internal static async Task CopyToAsync(Stream source, Stream destination, long? length, CancellationToken cancel) + { + long? bytesRemaining = length; + byte[] buffer = new byte[DefaultBufferSize]; + + Contract.Assert(source != null); + Contract.Assert(destination != null); + Contract.Assert(!bytesRemaining.HasValue || bytesRemaining.Value >= 0); + Contract.Assert(buffer != null); + + while (true) + { + // The natural end of the range. + if (bytesRemaining.HasValue && bytesRemaining.Value <= 0) + { + return; + } + + cancel.ThrowIfCancellationRequested(); + + int readLength = buffer.Length; + if (bytesRemaining.HasValue) + { + readLength = (int)Math.Min(bytesRemaining.Value, (long)readLength); + } + int count = await source.ReadAsync(buffer, 0, readLength, cancel); + + if (bytesRemaining.HasValue) + { + bytesRemaining -= count; + } + + // End of the source stream. + if (count == 0) + { + return; + } + + cancel.ThrowIfCancellationRequested(); + + await destination.WriteAsync(buffer, 0, count, cancel); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.StaticFiles/project.json b/src/Microsoft.AspNet.StaticFiles/project.json new file mode 100644 index 0000000000..eed3dc1834 --- /dev/null +++ b/src/Microsoft.AspNet.StaticFiles/project.json @@ -0,0 +1,16 @@ +{ + "version": "0.1-alpha-*", + "dependencies": { + "Microsoft.AspNet.Abstractions" : "0.1-alpha-*", + "Microsoft.AspNet.HttpFeature" : "0.1-alpha-*", + "Microsoft.AspNet.FileSystems" : "0.1-alpha-*" + }, + "configurations": { + "net45": { + "dependencies": { + "Owin" : "1.0" + } + }, + "k10" : { } + } +} \ No newline at end of file